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:
Eric Wagoner
2026-08-02 14:34:44 -04:00
parent 38e20f2c30
commit 65d4cdd5ec
23 changed files with 624 additions and 170 deletions
+12
View File
@@ -1,3 +1,5 @@
"""BGG client tests: canned transports, fake clocks — never online."""
from __future__ import annotations
import random
@@ -182,3 +184,13 @@ def test_collection_item_missing_collid_refuses_to_parse(tmp_path):
)
with pytest.raises(BGGResponseError):
parse_collection(bad_xml)
def test_malformed_xml_raises_and_is_never_cached(tmp_path):
from bggpipe.models import BGGResponseError
torn = '<items total="1"><item type="boardgame" id="13"><na'
client, _, _ = make_client(tmp_path, [(200, torn)])
with pytest.raises(BGGResponseError):
client.get_xml("search", {"query": "catan", "type": "boardgame"})
assert list((tmp_path / "cache").glob("*.xml")) == []