diff --git a/tests/test_review.py b/tests/test_review.py new file mode 100644 index 0000000..5fb9d2a --- /dev/null +++ b/tests/test_review.py @@ -0,0 +1,59 @@ +from hef.catalog import Record, validate +from hef.selection import Coordinate +from tools.review import approve, proposed_records + + +def make_record(**o): + base = dict( + id="a", + title="t", + source_url="u", + source_archive="nasa", + license="public_domain", + mode="video", + left=0, + right=0, + dark=0, + light=0, + duration_s=1, + file_path="p", + ) + base.update(o) + return Record(**base) + + +def test_proposed_records_filters(): + a = make_record(id="a", review_status="proposed") + b = make_record(id="b", review_status="approved") + assert [r.id for r in proposed_records([a, b])] == ["a"] + + +def test_approve_sets_status_and_timestamp(): + r = make_record(review_status="proposed") + out = approve(r, reviewed_at="2026-06-04T13:00:00+00:00") + assert out.review_status == "approved" + assert out.reviewed_at == "2026-06-04T13:00:00+00:00" + + +def test_approve_can_override_coordinates(): + r = make_record(left=0, right=0, dark=0, light=0, review_status="proposed") + out = approve(r, reviewed_at="t", coordinate=Coordinate(4, 1, 2, 3)) + assert (out.left, out.right, out.dark, out.light) == (4, 1, 2, 3) + + +def test_approve_does_not_mutate_input(): + r = make_record(review_status="proposed") + approve(r, reviewed_at="t") + assert r.review_status == "proposed" and r.reviewed_at is None + + +def test_approve_can_set_rationale(): + r = make_record(review_status="proposed", rationale="auto") + out = approve(r, reviewed_at="t", rationale="human note") + assert out.rationale == "human note" + + +def test_approved_record_revalidates(): + r = make_record(review_status="proposed") + out = approve(r, reviewed_at="t", coordinate=Coordinate(2, 2, 1, 1)) + validate(out) # must not raise diff --git a/tools/review.py b/tools/review.py new file mode 100644 index 0000000..90467b9 --- /dev/null +++ b/tools/review.py @@ -0,0 +1,34 @@ +"""Review state-transition core (pure, tested): proposed -> approved. + +No I/O, no clock — the timestamp is injected so the logic stays deterministic. +The CLI persists the result via hef.catalog.save_catalog after re-validating. +""" + +from __future__ import annotations + +from dataclasses import replace + + +def proposed_records(records): + """Records still awaiting review.""" + return [r for r in records if r.review_status == "proposed"] + + +def approve(record, *, reviewed_at, coordinate=None, rationale=None): + """Return an approved copy of `record` (the input is not mutated). + + review_status -> 'approved' and reviewed_at is stamped. Optionally override + the four coordinates with a human correction and/or replace the rationale. + The caller re-validates before saving. + """ + changes = {"review_status": "approved", "reviewed_at": reviewed_at} + if coordinate is not None: + changes.update( + left=coordinate.left, + right=coordinate.right, + dark=coordinate.dark, + light=coordinate.light, + ) + if rationale is not None: + changes["rationale"] = rationale + return replace(record, **changes)