Uh oh!
There was an error while loading. Please reload this page.
Commit b14fc3b
* test(quiz): the loop through HTTP against real Postgres (#545)
The gap in the batch's test gate. Every other quiz integration test either
writes its rows with `table()` and asserts the schema, or GETs one attempt —
so the WRITE paths (`generate`, `answer`, `submit`) were exercised only
against a MagicMock, which is precisely the blind spot #545 exists to close:
* #529 lived 51 days because the failing write was only ever mocked;
* #265's column drift is the same shape — a route writing or selecting a
column the migrations don't have, invisible to a mocked `table()`;
* #555 added a brand-new column to the `generate` INSERT last week, and
nothing in this lane would have noticed a missing migration.
13 tests, in through the app's own HTTP surface with a real session cookie,
read back through direct psycopg — never through the PostgREST layer that
made the write, which would only prove the echo.
Covering the issue's checklist: generate (every difficulty `/api/quiz/config`
advertises, INCLUDING adaptive, plus the boundary counts), per-question
answer, submit, replayed submit -> 409 with mastery unchanged, resume,
history, and the #529 write against the real UNIQUE.
Two of them are things the hermetic lane structurally cannot test: the IDOR
negative (the hermetic conftest stubs `require_self` to a no-op, so ownership
is untestable there) and ciphertext-at-rest for `questions_json`/
`answers_json`.
Bounds and difficulties are asserted against what `/api/quiz/config` returns
rather than against literals, so widening a cap cannot leave this file
pinning the old one — the "15 questions" option 422'd for months precisely
because the UI and the route disagreed about that list.
Skips loudly without `SAPLING_MODEL_MODE=function` rather than calling live
Gemini from a test.
Writing these found two contract details the mocked tests had never had to
be right about: `/api/quiz/config` nests the counts under `num_questions`,
and the history listing keys each entry `quiz_id`, not `id`.
Integration lane 56 -> 69 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(quiz): #545 review — the gate passed the bugs it claimed to catch
Nine findings, and for a TEST PR they were the worst possible kind: each
came with a concrete mutation that my tests passed. A gate that survives the
regression it guards is worse than no gate, because it reports safety.
Mutation-verified after fixing (both were green BEFORE):
* drop `exam_days_away` from quiz_attempts -> 2 tests fail
* invert `is_correct` in answer_question -> the loop test fails
**It skipped instead of raising.** `pytest.skip` on a missing
SAPLING_MODEL_MODE is the exact pattern this lane's conftest forbids, and the
documented invocation (`RUN_INTEGRATION=1 pytest -m integration`) does not
set it — so all 13 tests skipped, pytest exited 0, and "the gate this file
closes" closed on nothing. That is how #265 survived the one lane built to
catch it. Raises now, and checks SAPLING_FUNCTION_HANDLERS too.
**The #555 column claim was false.** The docstring said nothing in this lane
would notice a missing migration — but nothing here noticed either, for two
independent reasons: every exam in the rich seed is in the PAST, so
`days_until_next_exam` returns None and the column never enters the INSERT;
and `_insert_attempt` catches the unknown-column failure and retries without
it, returning 200. Two tests now provoke the write with a future exam and
read the column back, plus pin the seed's own no-upcoming-exam state so a
constant-returning lookup can't satisfy the first.
**`is_correct` was never asserted anywhere.** It exists BECAUSE grading moved
server-side (#541), and submit re-derives the score without reading it — so
inverting it poisons every response row and every future item statistic while
the whole suite stays green.
**`answers_json` was only checked for `isinstance(str)`** — and `encrypt_json([])`
is a str, so storing the empty request payload instead of what was graded
passed the assertion the comment said it was guarding. Decrypted and counted.
**The replay test compared a value to itself**, snapshotting mastery after the
first submit — so it could not tell "the replay applied nothing" from
"nothing was ever applied". A no-op apply_graph_update, or a #553-shaped
keyspace miss, passed it. Anchored on the seeded 0.25 -> 0.24, with the
mastery-event count as a second witness.
**Bounds ignored `options`** — the list the UI renders, and exactly where #540
lived: putting 15 back in it while the cap stays 10 reproduced the bug and
passed. Every advertised option now has to round-trip.
**Difficulties were a hard-coded parametrize list**, so it only proved the
values I thought of are advertised, never the reverse; a new difficulty
shipped untested. Parametrized off `quiz_config_payload()` now.
Also: the seam's `E2E_QUIZ_CORRECT_LABELS` is imported rather than
re-declared (a drift surfaced as a confusing reconciliation failure instead
of naming the constant that moved), and the #529 test is dropped — it
duplicated `test_quiz_context_repair_db` and never went through submit, so it
did not cover the failure mode its own docstring described.
Integration lane 69 -> 70 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 8626903 commit b14fc3b
1 file changed
Lines changed: 402 additions & 0 deletions
0 commit comments