Re-audit round 2: 5 blind reviewers, 17 fixes, +12 tests
The re-run confirmed round 1 held and then caught second-order bugs in its own fixes plus two long-standing ones everyone missed. TUI decisions after a mid-session reload were counted but never written (rows are now re-adopted into the fresh list on every save, preferring undecided slots on duplicate keys); row_ix was computed by equality so duplicate rows shared an ordinal (identity now, merges included, veto sends it); upload job keys collided for two same-version copies (completions are counted per key, so --limit or an interrupt can no longer strand the second copy); diff consumes collids on exact-version matches (a vetoed same-version second copy was silently swallowed) and splits mismatches: report-only disagreement while an unclaimed copy exists, second-copy add only when every copy is claimed. Also: XML responses are validated and written atomically before caching (a torn or truncated 200 body can never poison a re-run), JSON artifacts write atomically, thing/search parsers refuse missing ids like the collection parser, empty game names are refused by the upload queue, a never-rendering version picker fails retryably instead of terminally, the systemic-failure abort compares exception types, blocked same-title entries defer as a group so positional pairing can't misalign, truncation heads pick the earliest separator, diff messages tell the truth when a token exists without a username, and the shared-constant sweep now actually covers every module (statuses, search types, marker names, client_for, ports). pydantic declared as a direct dependency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -403,3 +403,32 @@ def test_session_warnings_surface_in_state(tmp_path):
|
||||
warnings = res.json()["warnings"]
|
||||
assert any("couldn't look up id 42" in w for w in warnings)
|
||||
assert warnings == web.get("/api/state").json()["warnings"]
|
||||
|
||||
|
||||
def test_duplicate_rows_are_individually_decidable_via_row_ix(tmp_path):
|
||||
# two editions of one game in one photo: byte-identical rows. The
|
||||
# ordinal must land each decision on its own row.
|
||||
from bggpipe.resolve import read_matches as read_m
|
||||
from bggpipe.resolve import write_matches as write_m
|
||||
|
||||
cfg = make_cfg(tmp_path)
|
||||
dup = _row(title_raw="Twins", match_status="unmatched")
|
||||
write_m(cfg.matches_path, [dict(dup), dict(dup)])
|
||||
web = TestClient(create_app(cfg, client=unauthorized_client(tmp_path)))
|
||||
|
||||
pending = web.get("/api/state").json()["pending"]
|
||||
assert [p["title_raw"] for p in pending] == ["Twins", "Twins"]
|
||||
assert pending[0]["row_ix"] != pending[1]["row_ix"] # identity, not ==
|
||||
|
||||
second = pending[1]
|
||||
web.post(
|
||||
"/api/decision",
|
||||
json={
|
||||
"title_raw": second["title_raw"],
|
||||
"source_photos": second["source_photos"],
|
||||
"row_ix": second["row_ix"],
|
||||
"action": "reject",
|
||||
},
|
||||
)
|
||||
rows = read_m(cfg.matches_path)
|
||||
assert [r["match_status"] for r in rows] == ["unmatched", "rejected"]
|
||||
|
||||
Reference in New Issue
Block a user