Add repository and projection-service tests #6
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
The projection service is the most safety-critical code in the backend:
Photoprojection fields must be updated in the same transaction as the history write.
The plan calls this out as the rule that prevents silent data loss, and it is
currently unverified.
Scope
Tests for the repository layer and
app/services/projections.py.Implementation notes
Photoprojectionand the
DateDecisionhistory row are consistent — and if the transaction is rolledback mid-write, neither is persisted. Simulate a failure between the two writes
and confirm atomicity rather than assuming it.
review_versionincrements exactly once per mutating write.is_active = Falseandsuperseded_by, and never deletes a row.is_activeand returns UI-ready ordering.AuditEventtype — audit logging is append-only,so a missing event is unrecoverable after the fact.
Done when
review_versionincrement behaviour is covered for every write pathReferences
backend/app/services/projections.pybackend/app/repositories/docs/circa-phase1-plan.mdsection 5.2 ("projection updates must happen in the same transaction as history writes")Depends on: the backend test harness.
Amended by the audit of 2026-07-28.
These tests need an oracle. #79 adds
rebuild_photo_projection(photo_id), which recomputes theprojection purely from decision and evidence history — turning "projections cannot drift" from a
docstring into a property test: after any sequence of operations, stored projection must equal
rebuild output.
That matters because drift is already possible:
supersede_evidencesetsis_active=Falsebutnever
superseded_by(#80), ingest setsphoto.statusinline rather than through the projectionservice, and status-transition rules currently live in three separate places.
Also define the fold explicitly for
skip— #81 changes it to write history and touch nothing else.Done in
ed0dbed. CI green. 34 tests; full suite 723 passed.Done when
review_versionincrement behaviour is covered for every write pathThe rollback case is the point. The failure plan §5.2 prevents is the worst kind an archive can have — a decision recorded in history while the photo still shows the old date, or the reverse — and neither half looks wrong on its own, so nothing surfaces it. The failure is now injected after both writes are staged and before the commit, which is the window a half-written state would escape through. Neither the history row, the projection, nor the audit event survives it. The event matters particularly: the ledger's triggers refuse deletion (#67), so one written by an abandoned request could never be removed afterwards — it has to not be written in the first place.
On the amendment's ask for an oracle. Implemented, and then made to earn it. The property test passed on the first run, which told me nothing, so I injected #79's drift into
recompute()to see whether it would notice.It did not. Checking only the final state, the mutation left the property test green — because a sequence ending in a decision hides evidence drift entirely: a decision outranks evidence in
fold(), so the stored value and the oracle agree for the wrong reason. Checked after every operation instead, the same mutation fails five of twelve seeds. That distinction is now recorded in the test's docstring, since the weaker version looks equally correct on the page.Worth noting the mutation was caught by one hand-written test either way (
test_superseding_the_only_actionable_evidence_demotes_the_photo). The hand-written cases and the property test are not redundant: the first catches what someone thought of, the second catches what nobody did.review_versionper path, with the two asymmetries stated as tests rather than left in comments: a skip does not advance it (#81 — no reviewable state changed), and an evidence append does even when the status is unchanged (#77 — otherwise a reviewer mid-decision never learns the evidentiary basis moved).Repository orderings are asserted with explicit timestamps, because rows made in one test share a clock to the microsecond. Evidence reads oldest-first because the panel shows how the archive's understanding accumulated; decisions read newest-first because the history tab leads with what holds now. Two opposite orderings, both deliberate.