feat: interactive review CLI
Per sub-project-2 plan Task 12 / spec §8.2. Walks proposed records (fields + coords + rationale + best-effort ffmpeg preview), prompts accept/edit/skip/quit, and persists each approval via save_catalog rewrite. I/O seams (input_fn/now_fn/ out) and --no-preview make the walk testable hermetically; decision logic lives in the tested review core. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import io
|
||||
|
||||
import pytest
|
||||
|
||||
from hef.catalog import Record, load_catalog, save_catalog
|
||||
from tools.review_cli import main
|
||||
|
||||
|
||||
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="nasa/a.mp4",
|
||||
)
|
||||
base.update(o)
|
||||
return Record(**base)
|
||||
|
||||
|
||||
def test_help_parses():
|
||||
with pytest.raises(SystemExit) as e:
|
||||
main(["--help"])
|
||||
assert e.value.code == 0
|
||||
|
||||
|
||||
def test_no_proposed_records(tmp_path):
|
||||
catalog = tmp_path / "library.jsonl"
|
||||
save_catalog([make_record(id="x", review_status="approved", reviewed_at="t")], catalog)
|
||||
out = io.StringIO()
|
||||
rc = main(["--catalog", str(catalog), "--no-preview"], out=out)
|
||||
assert rc == 0
|
||||
assert "no proposed records" in out.getvalue()
|
||||
|
||||
|
||||
def test_accept_flips_to_approved(tmp_path):
|
||||
catalog = tmp_path / "library.jsonl"
|
||||
save_catalog([make_record(id="a", review_status="proposed")], catalog)
|
||||
out = io.StringIO()
|
||||
rc = main(
|
||||
["--catalog", str(catalog), "--no-preview"],
|
||||
input_fn=lambda prompt: "a",
|
||||
now_fn=lambda: "2026-06-04T13:00:00+00:00",
|
||||
out=out,
|
||||
)
|
||||
assert rc == 0
|
||||
r = load_catalog(catalog)[0]
|
||||
assert r.review_status == "approved"
|
||||
assert r.reviewed_at == "2026-06-04T13:00:00+00:00"
|
||||
|
||||
|
||||
def test_skip_leaves_proposed(tmp_path):
|
||||
catalog = tmp_path / "library.jsonl"
|
||||
save_catalog([make_record(id="a", review_status="proposed")], catalog)
|
||||
rc = main(
|
||||
["--catalog", str(catalog), "--no-preview"],
|
||||
input_fn=lambda prompt: "s",
|
||||
out=io.StringIO(),
|
||||
)
|
||||
assert rc == 0
|
||||
assert load_catalog(catalog)[0].review_status == "proposed"
|
||||
|
||||
|
||||
def test_edit_overrides_coordinates(tmp_path):
|
||||
catalog = tmp_path / "library.jsonl"
|
||||
save_catalog([make_record(id="a", review_status="proposed")], catalog)
|
||||
answers = iter(["e", "4", "1", "2", "3"])
|
||||
rc = main(
|
||||
["--catalog", str(catalog), "--no-preview"],
|
||||
input_fn=lambda prompt: next(answers),
|
||||
now_fn=lambda: "t",
|
||||
out=io.StringIO(),
|
||||
)
|
||||
assert rc == 0
|
||||
r = load_catalog(catalog)[0]
|
||||
assert (r.left, r.right, r.dark, r.light) == (4, 1, 2, 3)
|
||||
assert r.review_status == "approved"
|
||||
|
||||
|
||||
def test_quit_stops_walk(tmp_path):
|
||||
catalog = tmp_path / "library.jsonl"
|
||||
save_catalog(
|
||||
[
|
||||
make_record(id="a", review_status="proposed"),
|
||||
make_record(id="b", review_status="proposed"),
|
||||
],
|
||||
catalog,
|
||||
)
|
||||
rc = main(
|
||||
["--catalog", str(catalog), "--no-preview"],
|
||||
input_fn=lambda prompt: "q",
|
||||
out=io.StringIO(),
|
||||
)
|
||||
assert rc == 0
|
||||
statuses = {r.id: r.review_status for r in load_catalog(catalog)}
|
||||
assert statuses == {"a": "proposed", "b": "proposed"}
|
||||
Reference in New Issue
Block a user