Found by the #393 browser journey on its first live run (epic #402 — exactly the cross-layer class of bug the E2E lane exists to catch).
Symptom: completing any quiz through the UI fails silently at the results step; POST /api/quiz/submit returns 422 Unprocessable Content and the panel stays on the review phase with an error toast.
Cause:frontend/src/components/QuizPanel.tsx builds answers as {question_id, selected} while backend/models/__init__.py::AnswerItem requires selected_label (no alias). Introduced by 399eae3 ("frontend: ship revamp shell, screens, and API client"), which dropped the old selected_label payload. Verified live on origin/main. Neither side's mocked tests could catch it: backend tests construct AnswerItem directly; frontend tests mock fetch.
Consequence: no UI-driven quiz has ever written a score, quiz_attempts completion, or a mastery update since the revamp.
Fix: PR #437 renames the request field to the backend contract (selected_label); the response items keep their separate selected key. The #393 journey now pins the full path (10/10 green), so a regression fails the browser lane.
Distinct from #129 (double-submit double-count / malformed-item free point), which remains open.
🤖 Generated with Claude Code
Found by the #393 browser journey on its first live run (epic #402 — exactly the cross-layer class of bug the E2E lane exists to catch).
Symptom: completing any quiz through the UI fails silently at the results step;
POST /api/quiz/submitreturns 422 Unprocessable Content and the panel stays on the review phase with an error toast.Cause:
frontend/src/components/QuizPanel.tsxbuilds answers as{question_id, selected}whilebackend/models/__init__.py::AnswerItemrequiresselected_label(no alias). Introduced by399eae3("frontend: ship revamp shell, screens, and API client"), which dropped the oldselected_labelpayload. Verified live onorigin/main. Neither side's mocked tests could catch it: backend tests constructAnswerItemdirectly; frontend tests mockfetch.Consequence: no UI-driven quiz has ever written a score,
quiz_attemptscompletion, or a mastery update since the revamp.Fix: PR #437 renames the request field to the backend contract (
selected_label); the response items keep their separateselectedkey. The #393 journey now pins the full path (10/10 green), so a regression fails the browser lane.Distinct from #129 (double-submit double-count / malformed-item free point), which remains open.
🤖 Generated with Claude Code