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:
@@ -590,3 +590,44 @@ def test_empty_normalized_title_never_matches(client):
|
||||
entry = TitleEntry(title_raw="风声", title_normalized="")
|
||||
# any cached query works; candidates must be rejected regardless of name
|
||||
assert _plausible_candidates(client, entry, "Catan") == []
|
||||
|
||||
|
||||
def test_blocked_same_title_entry_defers_the_whole_group(tmp_path):
|
||||
# entry1 of a two-edition title is blocked (no token); entry2 must NOT
|
||||
# resolve, or its row would occupy entry1's pairing slot next run
|
||||
import httpx as _httpx
|
||||
|
||||
data_dir = tmp_path / "data"
|
||||
data_dir.mkdir()
|
||||
(data_dir / "titles.json").write_text(
|
||||
json.dumps(
|
||||
[
|
||||
{
|
||||
"title_raw": "Catan",
|
||||
"edition_hint": "3rd edition",
|
||||
"source_photos": ["a.jpg"],
|
||||
},
|
||||
{
|
||||
"title_raw": "Catan",
|
||||
"edition_hint": "5th edition",
|
||||
"source_photos": ["b.jpg"],
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
cfg = Config(data_dir=data_dir)
|
||||
blocked_client = BGGClient(
|
||||
cache_dir=tmp_path / "empty_cache",
|
||||
transport=_httpx.MockTransport(
|
||||
lambda req: _httpx.Response(401, text="Unauthorized")
|
||||
),
|
||||
sleep=lambda s: None,
|
||||
)
|
||||
run_resolve(cfg, client=blocked_client)
|
||||
assert read_matches(cfg.matches_path) == [] # both deferred, none misplaced
|
||||
|
||||
|
||||
def test_truncation_separator_chosen_by_position():
|
||||
heads = _truncation_heads("Blorvath: Quest of the Zzyzx - 2nd Edition")
|
||||
assert heads[0] == "Blorvath"
|
||||
assert "Blorvath: Quest" not in heads # the comment's guarantee, now true
|
||||
|
||||
Reference in New Issue
Block a user