Audit round 7, web + stages cluster: 13 more verified findings fixed
The web layer's serialization story had three gaps: /api/run started a stage without the lock, so a decision mid-save could pass the rewrite guard and still be clobbered by the stage's full rewrite (now the start itself serializes); /api/photos accepted a replacement photo while extract was running, permanently pairing the new bytes with the old photo's reads (now refuses like every other mutation); and /api/queue read session rows lock-free and stale (now freshens under the lock). The localhost Host allowlist applied only to writes — a DNS-rebound page could read pipeline state and shelf photos with plain GETs; it now covers all methods (foreign-Origin reads still pass: without CORS headers a cross-origin page can't read the response anyway). Data-loss finds: the off-BGG edit form re-rendered from games.json, which only sees hand data after enrich — so a second save resubmitted pre-save blanks and cleared the first (the detail endpoint now overlays local_games.json live). The local key embeds the photo list, so a new sighting orphaned hand-written facts silently; enrich now migrates them when the title still matches exactly one line, and warns instead of ever dropping. research() left the previous game's version verdicts on the row, riding a stale version_id onto the next pick; it clears all four fields as reopen does. find_row now prefers the version-open sibling on duplicate keys, mirroring _adopt. Re-adding a removed hand-added title silently no-opped behind a 200 — it now rescinds the removal (an explicit undo), and a true duplicate add answers 409. Smaller: parse_search's dedupe collapsed same-id rows under DIFFERENT names, discarding the alternate-name row whose exact match downstream scoring needed (now collapses same-name only; research merges its ballot per game preferring exact evidence); rpgitems rank in their own family so their rank parsed null; the pipeline badge counted review-retired queue rows as pending; the catalog pairing cascade ran per-entry so a tier-3 claim could steal a sibling's exact row (now tier-by-tier across all entries, as resolve does); library cards render a lone player bound without "undefined" and the seats filter tolerates it; added_no_version reads "done · no version" instead of a bare green done. Every finding verified against the code before fixing; each fix carries a regression test. 337 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
32b6aae841
commit
e6d45011cc
@@ -698,3 +698,66 @@ def test_research_can_target_rpggeek_explicitly(tmp_path):
|
||||
assert row["bgg_id"] == ""
|
||||
with pytest.raises(ValueError, match="needs some text"):
|
||||
session.research(row, " ")
|
||||
|
||||
|
||||
def test_find_row_prefers_the_version_open_sibling(tmp_path):
|
||||
"""Two decided duplicate rows: a version pick with a stale row_ix must
|
||||
land on the sibling whose VERSION is still open, not overwrite the
|
||||
other's earlier human decision (mirrors _adopt)."""
|
||||
cfg = _setup(
|
||||
tmp_path,
|
||||
[
|
||||
_row(
|
||||
title_raw="Wiz-War",
|
||||
match_status="approved",
|
||||
bgg_id="589",
|
||||
version_status="version_approved",
|
||||
version_id="1",
|
||||
),
|
||||
_row(
|
||||
title_raw="Wiz-War",
|
||||
match_status="approved",
|
||||
bgg_id="589",
|
||||
version_status="version_ambiguous",
|
||||
),
|
||||
],
|
||||
)
|
||||
session = ReviewSession(
|
||||
cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
|
||||
)
|
||||
row = session.find_row("Wiz-War", "hand-typed-test-list", row_ix=None)
|
||||
assert row["version_status"] == "version_ambiguous"
|
||||
|
||||
|
||||
def test_research_clears_the_previous_games_version(tmp_path):
|
||||
"""A fresh ballot supersedes EVERYTHING about the old match: a
|
||||
surviving version_id would put the old game's edition on whatever the
|
||||
human picks next."""
|
||||
cfg = _setup(
|
||||
tmp_path,
|
||||
[
|
||||
_row(
|
||||
title_raw="Citadels",
|
||||
match_status="approved",
|
||||
bgg_id="478",
|
||||
version_status="version_approved",
|
||||
version_id="99999",
|
||||
version_name="Old Game's Edition",
|
||||
)
|
||||
],
|
||||
)
|
||||
session = ReviewSession(
|
||||
cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
|
||||
)
|
||||
# stats batch for these results isn't recorded; decoration may fail
|
||||
session.client = BGGClient(
|
||||
cache_dir=FIXTURES,
|
||||
transport=httpx.MockTransport(
|
||||
lambda req: httpx.Response(401, text="Unauthorized")
|
||||
),
|
||||
)
|
||||
row = session.rows[0]
|
||||
session.research(row, "Citadels")
|
||||
assert row["version_status"] == ""
|
||||
assert row["version_id"] == "" and row["version_name"] == ""
|
||||
assert row["version_candidates_json"] == "[]"
|
||||
|
||||
Reference in New Issue
Block a user