In In-Browser Compile (local grading) mode for C# lessons, completing a lesson successfully and then force-reloading the page reverts the lesson to incomplete — the student has to redo it.
Steps to reproduce
Open a C# lesson and switch grading to In-Browser Compile (local mode).
Submit a correct solution; the lesson is marked complete.
Wait a moment, then force-reload the page.
The lesson is no longer complete.
Root cause
C# local attempts are generated entirely client-side by the WASM worker (wasm-runtime.service.ts → getAttempt). The backend never issues the attempt token and never stores its grading state. (RoslynCSharp is also not in the backend supportsBrowserGrading allowlist in GetLessonAttemptQueryHandler, so even a server-issued C# attempt would not be persisted.)
When a local C# pass is flushed to POST /Lessons/progress/batch, the backend's ProcessBatchItem looks up the offline state by (userId, lessonNumber, attemptToken), finds nothing (the client-generated token was never stored), and returns state_expired.
In the frontend ProgressSyncService.flush(), state_expired was treated as a terminal rejection, which deleted both the outbox row and the durable completion ledger entry (the floor that keeps a local pass visible across a refresh — see spikersoft-issues#72 / #73 / #223). Once that post-pass flush round-trip completed (a few hundred ms after passing — well within "complete then reload" timing), the floor was gone, so on reload the lesson reverted.
The conceptual error: state_expired means "the server couldn't re-verify this pass", not "this pass was forged". For C# it fires on every local pass.
Fix
In progress-sync.service.ts, split the rejection reasons into two sets:
terminalReasons (stop retrying / drop the outbox row) — unchanged, still includes state_expired.
ledgerClearingReasons (forget the local completion) — a subset that excludes state_expired. Only genuinely-invalid passes (invalid_token, unknown_lesson, challenge_lesson_wrong_endpoint, server_grading_failed) clear the durable floor.
Net effect: a C# In-Browser Compile pass now survives a force reload. Added a regression test.
Known follow-up (not in this fix)
C# local passes remain unverified server-side (no skills awarded, no cross-device sync) because the backend can't re-grade a client-only C# attempt. Fully resolving that is the larger item the code already flags: route C# attempts through the server when online so the offline state is stored, and add RoslynCSharp to supportsBrowserGrading.
## Summary
In **In-Browser Compile** (local grading) mode for **C#** lessons, completing a lesson successfully and then force-reloading the page reverts the lesson to incomplete — the student has to redo it.
## Steps to reproduce
1. Open a C# lesson and switch grading to **In-Browser Compile** (local mode).
2. Submit a correct solution; the lesson is marked complete.
3. Wait a moment, then force-reload the page.
4. The lesson is no longer complete.
## Root cause
C# local attempts are generated **entirely client-side** by the WASM worker (`wasm-runtime.service.ts` → `getAttempt`). The backend never issues the attempt token and never stores its grading state. (`RoslynCSharp` is also not in the backend `supportsBrowserGrading` allowlist in `GetLessonAttemptQueryHandler`, so even a server-issued C# attempt would not be persisted.)
When a local C# pass is flushed to `POST /Lessons/progress/batch`, the backend's `ProcessBatchItem` looks up the offline state by `(userId, lessonNumber, attemptToken)`, finds nothing (the client-generated token was never stored), and returns **`state_expired`**.
In the frontend `ProgressSyncService.flush()`, `state_expired` was treated as a *terminal* rejection, which deleted **both** the outbox row **and the durable completion ledger entry** (the floor that keeps a local pass visible across a refresh — see spikersoft-issues#72 / #73 / #223). Once that post-pass flush round-trip completed (a few hundred ms after passing — well within "complete then reload" timing), the floor was gone, so on reload the lesson reverted.
The conceptual error: `state_expired` means *"the server couldn't re-verify this pass"*, not *"this pass was forged"*. For C# it fires on **every** local pass.
## Fix
In `progress-sync.service.ts`, split the rejection reasons into two sets:
- `terminalReasons` (stop retrying / drop the outbox row) — unchanged, still includes `state_expired`.
- `ledgerClearingReasons` (forget the local completion) — a **subset** that excludes `state_expired`. Only genuinely-invalid passes (`invalid_token`, `unknown_lesson`, `challenge_lesson_wrong_endpoint`, `server_grading_failed`) clear the durable floor.
Net effect: a C# In-Browser Compile pass now survives a force reload. Added a regression test.
## Known follow-up (not in this fix)
C# local passes remain `unverified` server-side (no skills awarded, no cross-device sync) because the backend can't re-grade a client-only C# attempt. Fully resolving that is the larger item the code already flags: route C# attempts through the server when online so the offline state is stored, and add `RoslynCSharp` to `supportsBrowserGrading`.
Resolved in spikersoft-angular PR #75 (merged to master). C# local-grading (In-Browser Compile) lesson completion is now persisted/synced so it survives a force reload instead of reverting. Closing.
Resolved in spikersoft-angular PR #75 (merged to `master`). C# local-grading (In-Browser Compile) lesson completion is now persisted/synced so it survives a force reload instead of reverting. 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.
Summary
In In-Browser Compile (local grading) mode for C# lessons, completing a lesson successfully and then force-reloading the page reverts the lesson to incomplete — the student has to redo it.
Steps to reproduce
Root cause
C# local attempts are generated entirely client-side by the WASM worker (
wasm-runtime.service.ts→getAttempt). The backend never issues the attempt token and never stores its grading state. (RoslynCSharpis also not in the backendsupportsBrowserGradingallowlist inGetLessonAttemptQueryHandler, so even a server-issued C# attempt would not be persisted.)When a local C# pass is flushed to
POST /Lessons/progress/batch, the backend'sProcessBatchItemlooks up the offline state by(userId, lessonNumber, attemptToken), finds nothing (the client-generated token was never stored), and returnsstate_expired.In the frontend
ProgressSyncService.flush(),state_expiredwas treated as a terminal rejection, which deleted both the outbox row and the durable completion ledger entry (the floor that keeps a local pass visible across a refresh — see spikersoft-issues#72 / #73 / #223). Once that post-pass flush round-trip completed (a few hundred ms after passing — well within "complete then reload" timing), the floor was gone, so on reload the lesson reverted.The conceptual error:
state_expiredmeans "the server couldn't re-verify this pass", not "this pass was forged". For C# it fires on every local pass.Fix
In
progress-sync.service.ts, split the rejection reasons into two sets:terminalReasons(stop retrying / drop the outbox row) — unchanged, still includesstate_expired.ledgerClearingReasons(forget the local completion) — a subset that excludesstate_expired. Only genuinely-invalid passes (invalid_token,unknown_lesson,challenge_lesson_wrong_endpoint,server_grading_failed) clear the durable floor.Net effect: a C# In-Browser Compile pass now survives a force reload. Added a regression test.
Known follow-up (not in this fix)
C# local passes remain
unverifiedserver-side (no skills awarded, no cross-device sync) because the backend can't re-grade a client-only C# attempt. Fully resolving that is the larger item the code already flags: route C# attempts through the server when online so the offline state is stored, and addRoslynCSharptosupportsBrowserGrading.Resolved in spikersoft-angular PR #75 (merged to
master). C# local-grading (In-Browser Compile) lesson completion is now persisted/synced so it survives a force reload instead of reverting. Closing.