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:
@@ -383,3 +383,45 @@ def test_real_run_without_credentials_exits_before_any_browser(tmp_path, monkeyp
|
||||
with pytest.raises(typer.Exit):
|
||||
run_upload(cfg, sleep=lambda s: None, now=NOW) # uploader=None: real path
|
||||
assert not (tmp_path / "upload_log.csv").exists()
|
||||
|
||||
|
||||
def test_same_key_second_copy_survives_limit_and_interrupts(tmp_path):
|
||||
# two vetoed duplicate copies share ("add", bgg_id, version): one logged
|
||||
# success must complete exactly ONE of them, not both
|
||||
cfg = _cfg(tmp_path)
|
||||
twin = _add_row(bgg_id="13", name="Catan", version_id="123", version_name="3rd")
|
||||
_seed_data(tmp_path, to_add=[dict(twin), dict(twin)])
|
||||
|
||||
first = FakeUploader()
|
||||
run_upload(cfg, uploader=first, limit=1, sleep=lambda s: None, now=NOW)
|
||||
assert len(first.calls) == 1
|
||||
|
||||
second = FakeUploader()
|
||||
run_upload(cfg, uploader=second, sleep=lambda s: None, now=NOW)
|
||||
assert len(second.calls) == 1 # the second copy, not zero, not two
|
||||
|
||||
third = FakeUploader()
|
||||
assert run_upload(cfg, uploader=third, sleep=lambda s: None, now=NOW) == []
|
||||
|
||||
|
||||
def test_consecutive_failure_counter_resets_on_success(tmp_path):
|
||||
class FlakyPairs(FakeUploader):
|
||||
def add_game(self, job):
|
||||
self.calls.append(job)
|
||||
if job.bgg_id in ("1", "2", "4", "5"):
|
||||
raise RuntimeError("dialog never appeared")
|
||||
return "added", ""
|
||||
|
||||
cfg = _cfg(tmp_path)
|
||||
_seed_data(tmp_path, to_add=[_add_row(bgg_id=str(i)) for i in range(1, 7)])
|
||||
fake = FlakyPairs()
|
||||
run_upload(cfg, uploader=fake, sleep=lambda s: None, now=NOW)
|
||||
assert len(fake.calls) == 6 # fail,fail,ok,fail,fail,ok — never aborts
|
||||
|
||||
|
||||
def test_empty_game_name_is_refused_not_uploaded(tmp_path):
|
||||
cfg = _cfg(tmp_path)
|
||||
_seed_data(tmp_path, to_add=[_add_row(bgg_id="42", name="")])
|
||||
fake = FakeUploader()
|
||||
results = run_upload(cfg, uploader=fake, sleep=lambda s: None, now=NOW)
|
||||
assert results == [] and fake.calls == []
|
||||
|
||||
Reference in New Issue
Block a user