complete-tutorial browser-grading allowlist is stale — omits RoslynCSharp, DuckDbSql, ClangC, ClangCpp #259

Closed
opened 2026-06-26 18:46:10 +00:00 by spikerj · 2 comments
Owner

Follow-up to spikersoft-issues#253 (out-of-scope note). The graded-lesson path is now server-verifiable for C# In-Browser Compile (backend PR #17 + frontend PR #77), but the tutorial completion path has a parallel, narrower allowlist that was never kept in sync.

The gap

POST /Lessons/{n}/complete-tutorial (LessonsController.CompleteTutorial) decides whether to consult the offline-attempt store using its own supportsBrowserGrading check:

// LessonsController.cs (~L599)
var supportsBrowserGrading = strategy.Metadata.GradingRuntime is
    LessonGradingRuntime.CPython or LessonGradingRuntime.NodeJavaScript
    or LessonGradingRuntime.RegexBrowser or LessonGradingRuntime.BlinkX86;

That list is stale relative to GetLessonAttemptQueryHandler, which already treats a broader set as browser-graded:

// GetLessonAttemptQueryHandler.cs (~L66)
var alwaysBrowserGraded = strategy.Metadata.GradingRuntime is
    LessonGradingRuntime.CPython or LessonGradingRuntime.NodeJavaScript
    or LessonGradingRuntime.RegexBrowser or LessonGradingRuntime.BlinkX86
    or LessonGradingRuntime.DuckDbSql
    or LessonGradingRuntime.ClangC or LessonGradingRuntime.ClangCpp;
// + RoslynCSharp when browserGrading=true (opt-in, #253)

So complete-tutorial omits DuckDbSql, ClangC, ClangCpp outright, and RoslynCSharp (the #253 opt-in case).

Impact

For a Tutorial-kind lesson in those runtimes, when the attempt token only lives in the offline store (issued via the browser-grading path, not the Redis _attemptCache), the offline-store lookup is gated behind supportsBrowserGrading and is skipped:

if (!hasValidAttempt && supportsBrowserGrading && _offlineStore is not null) { /* consult offline store */ }

Net effect: a legitimately server-issued (or offline-issued) browser-grading tutorial attempt isn't recognized as valid, so the completion can be wrongly rejected / not credited (or only saved by the lenient "already complete" no-op branch), and the single-use offline state isn't cleaned up (the post-completion RemoveAsync is also gated on the same flag). This is the tutorial analogue of the bug #253 fixed for graded lessons.

Proposed fix

  • Extract the browser-graded-runtime predicate into one shared helper so the two endpoints can't drift again (e.g. on LessonStrategyBase/strategy metadata or a small static helper), covering CPython, NodeJavaScript, RegexBrowser, BlinkX86, DuckDbSql, ClangC, ClangCpp.
  • Include RoslynCSharp for complete-tutorial on the same basis the attempt handler uses. Tutorial completion doesn't currently send a browserGrading flag; either (a) consult the offline store for RoslynCSharp unconditionally (safe — a C# entry only exists there when browserGrading=true was requested at attempt time), or (b) thread the flag through if we want symmetry. Option (a) is simplest.
  • Unit tests: a C# (and one Clang/SQL) Tutorial completion whose token is only in the offline store is accepted and credited, and the offline state is removed afterward.

Notes

  • Frontend already routes C# tutorial completion through completeTutorial in csharp-runner.service.ts; no SPA change expected.
  • Backend-only change in spikersoft-backend.
Follow-up to spikersoft-issues#253 (out-of-scope note). The graded-lesson path is now server-verifiable for C# In-Browser Compile (backend PR #17 + frontend PR #77), but the **tutorial** completion path has a parallel, narrower allowlist that was never kept in sync. ## The gap `POST /Lessons/{n}/complete-tutorial` (`LessonsController.CompleteTutorial`) decides whether to consult the offline-attempt store using its own `supportsBrowserGrading` check: ```csharp // LessonsController.cs (~L599) var supportsBrowserGrading = strategy.Metadata.GradingRuntime is LessonGradingRuntime.CPython or LessonGradingRuntime.NodeJavaScript or LessonGradingRuntime.RegexBrowser or LessonGradingRuntime.BlinkX86; ``` That list is **stale** relative to `GetLessonAttemptQueryHandler`, which already treats a broader set as browser-graded: ```csharp // GetLessonAttemptQueryHandler.cs (~L66) var alwaysBrowserGraded = strategy.Metadata.GradingRuntime is LessonGradingRuntime.CPython or LessonGradingRuntime.NodeJavaScript or LessonGradingRuntime.RegexBrowser or LessonGradingRuntime.BlinkX86 or LessonGradingRuntime.DuckDbSql or LessonGradingRuntime.ClangC or LessonGradingRuntime.ClangCpp; // + RoslynCSharp when browserGrading=true (opt-in, #253) ``` So `complete-tutorial` omits **`DuckDbSql`, `ClangC`, `ClangCpp`** outright, and **`RoslynCSharp`** (the #253 opt-in case). ## Impact For a **Tutorial**-kind lesson in those runtimes, when the attempt token only lives in the offline store (issued via the browser-grading path, not the Redis `_attemptCache`), the offline-store lookup is gated behind `supportsBrowserGrading` and is skipped: ```csharp if (!hasValidAttempt && supportsBrowserGrading && _offlineStore is not null) { /* consult offline store */ } ``` Net effect: a legitimately server-issued (or offline-issued) browser-grading tutorial attempt isn't recognized as valid, so the completion can be wrongly rejected / not credited (or only saved by the lenient "already complete" no-op branch), and the single-use offline state isn't cleaned up (the post-completion `RemoveAsync` is also gated on the same flag). This is the tutorial analogue of the bug #253 fixed for graded lessons. ## Proposed fix - Extract the browser-graded-runtime predicate into one shared helper so the two endpoints can't drift again (e.g. on `LessonStrategyBase`/strategy metadata or a small static helper), covering CPython, NodeJavaScript, RegexBrowser, BlinkX86, DuckDbSql, ClangC, ClangCpp. - Include `RoslynCSharp` for `complete-tutorial` on the same basis the attempt handler uses. Tutorial completion doesn't currently send a `browserGrading` flag; either (a) consult the offline store for `RoslynCSharp` unconditionally (safe — a C# entry only exists there when `browserGrading=true` was requested at attempt time), or (b) thread the flag through if we want symmetry. Option (a) is simplest. - Unit tests: a C# (and one Clang/SQL) Tutorial completion whose token is only in the offline store is accepted and credited, and the offline state is removed afterward. ## Notes - Frontend already routes C# tutorial completion through `completeTutorial` in `csharp-runner.service.ts`; no SPA change expected. - Backend-only change in `spikersoft-backend`.
Author
Owner

Fix opened: spikersoft-backend PR #19. Extracts a single LessonGradingRuntimeExtensions predicate used by both GET /attempt and POST /complete-tutorial (so they can't drift), and complete-tutorial now includes RoslynCSharp + DuckDbSql/ClangC/ClangCpp. Adds predicate + C#/Clang tutorial offline-fallback tests (29 passed). Will close once merged.

Fix opened: spikersoft-backend PR #19. Extracts a single `LessonGradingRuntimeExtensions` predicate used by both `GET /attempt` and `POST /complete-tutorial` (so they can't drift), and `complete-tutorial` now includes `RoslynCSharp` + `DuckDbSql`/`ClangC`/`ClangCpp`. Adds predicate + C#/Clang tutorial offline-fallback tests (29 passed). Will close once merged.
Author
Owner

Resolved in spikersoft-backend PR #19 (merged to master). Aligned the complete-tutorial browser-grading allowlist with the shared LessonGradingRuntimeExtensions predicate so offline tutorial completions for all browser-graded runtimes (incl. RoslynCSharp, DuckDbSql, ClangC/ClangCpp) are handled consistently. Closing.

Resolved in spikersoft-backend PR #19 (merged to `master`). Aligned the `complete-tutorial` browser-grading allowlist with the shared `LessonGradingRuntimeExtensions` predicate so offline tutorial completions for all browser-graded runtimes (incl. RoslynCSharp, DuckDbSql, ClangC/ClangCpp) are handled consistently. Closing.
Sign in to join this conversation.