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:
That list is stale relative to GetLessonAttemptQueryHandler, which already treats a broader set as browser-graded:
// GetLessonAttemptQueryHandler.cs (~L66)varalwaysBrowserGraded=strategy.Metadata.GradingRuntimeisLessonGradingRuntime.CPythonorLessonGradingRuntime.NodeJavaScriptorLessonGradingRuntime.RegexBrowserorLessonGradingRuntime.BlinkX86orLessonGradingRuntime.DuckDbSqlorLessonGradingRuntime.ClangCorLessonGradingRuntime.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&&_offlineStoreisnotnull){/* 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`.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 ownsupportsBrowserGradingcheck:That list is stale relative to
GetLessonAttemptQueryHandler, which already treats a broader set as browser-graded:So
complete-tutorialomitsDuckDbSql,ClangC,ClangCppoutright, andRoslynCSharp(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 behindsupportsBrowserGradingand is skipped: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
RemoveAsyncis also gated on the same flag). This is the tutorial analogue of the bug #253 fixed for graded lessons.Proposed fix
LessonStrategyBase/strategy metadata or a small static helper), covering CPython, NodeJavaScript, RegexBrowser, BlinkX86, DuckDbSql, ClangC, ClangCpp.RoslynCSharpforcomplete-tutorialon the same basis the attempt handler uses. Tutorial completion doesn't currently send abrowserGradingflag; either (a) consult the offline store forRoslynCSharpunconditionally (safe — a C# entry only exists there whenbrowserGrading=truewas requested at attempt time), or (b) thread the flag through if we want symmetry. Option (a) is simplest.Notes
completeTutorialincsharp-runner.service.ts; no SPA change expected.spikersoft-backend.Fix opened: spikersoft-backend PR #19. Extracts a single
LessonGradingRuntimeExtensionspredicate used by bothGET /attemptandPOST /complete-tutorial(so they can't drift), andcomplete-tutorialnow includesRoslynCSharp+DuckDbSql/ClangC/ClangCpp. Adds predicate + C#/Clang tutorial offline-fallback tests (29 passed). Will close once merged.Resolved in spikersoft-backend PR #19 (merged to
master). Aligned thecomplete-tutorialbrowser-grading allowlist with the sharedLessonGradingRuntimeExtensionspredicate so offline tutorial completions for all browser-graded runtimes (incl. RoslynCSharp, DuckDbSql, ClangC/ClangCpp) are handled consistently. Closing.