C# local-grading (In-Browser Compile) lesson completion reverts after a force reload #252

Closed
opened 2026-06-25 15:12:34 +00:00 by spikerj · 1 comment
Owner

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.tsgetAttempt). 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`.
Author
Owner

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.
Sign in to join this conversation.