Context
src/lib/pendingAttempt.ts is the guest mock-exam save path: readPendingAttempt validates the payload shape and expires entries past a 24h TTL, and flushPendingAttempt (line 122) mirrors the live save with a compensating delete on a partial write (deletes the exam_attempts row if the attempt_questions insert fails) plus a concurrency guard. All of it is untested, and a regression could silently drop a guest's completed attempt or leave a scored attempt with zero reviewable questions.
Scope
- Add
src/lib/pendingAttempt.test.ts (co-located, Vitest). Reuse the mock-Supabase harness pattern from src/lib/supabaseUtils.test.ts. Tests only; do not change production behavior.
Cases to cover
storePendingAttempt then readPendingAttempt round-trips a valid payload.
readPendingAttempt returns null for an expired entry (past the TTL) and for a malformed / shape-invalid entry.
flushPendingAttempt: success path inserts both rows; on an attempt_questions failure it deletes the exam_attempts row (compensating rollback); the concurrency guard prevents a double flush.
Acceptance criteria
Context
src/lib/pendingAttempt.tsis the guest mock-exam save path:readPendingAttemptvalidates the payload shape and expires entries past a 24h TTL, andflushPendingAttempt(line 122) mirrors the live save with a compensating delete on a partial write (deletes theexam_attemptsrow if theattempt_questionsinsert fails) plus a concurrency guard. All of it is untested, and a regression could silently drop a guest's completed attempt or leave a scored attempt with zero reviewable questions.Scope
src/lib/pendingAttempt.test.ts(co-located, Vitest). Reuse the mock-Supabase harness pattern fromsrc/lib/supabaseUtils.test.ts. Tests only; do not change production behavior.Cases to cover
storePendingAttemptthenreadPendingAttemptround-trips a valid payload.readPendingAttemptreturns null for an expired entry (past the TTL) and for a malformed / shape-invalid entry.flushPendingAttempt: success path inserts both rows; on anattempt_questionsfailure it deletes theexam_attemptsrow (compensating rollback); the concurrency guard prevents a double flush.Acceptance criteria
src/lib/pendingAttempt.test.tscovers round-trip, TTL expiry, shape rejection, partial-write rollback, and the concurrency guard.npm run testpasses; no new dependencies; no production code changed.