Audit: 5-reviewer sweep — 19 fixes across every stage, +24 tests
Correctness: review vetoes persist via a dedupe_veto column (resolve re-runs no longer overturn humans); diff emits second copies whose confident version matches no owned copy (spec: pairs own only on both ids) and fetches the live collection with refresh; resolve pairs titles.json entries to rows by title so a reshoot photo updates provenance instead of duplicating rows; version lookups survive empty /thing results; publisher tie-break now honors the mixed base/expansion veto and refuses multi-candidate picks; empty-normalized (non-Latin) titles never count as exact. Upload: LoginError aborts a run instead of logging N bogus failures (and 3 identical consecutive failures abort as systemic); Cloudflare interstitials are detected; added-without-version gets its own logged status that verify understands; same-game updates run one per pass so the name-targeted row edit can't overwrite a fresh version; absent diff outputs fail loudly; pagination clicks are paced. Web review: a lock serializes freshen/decide (threadpool race dropped decisions); failed saves roll memory back and always alert the browser (non-JSON 500s included); session warnings reach the page instead of a StringIO; state-load failures and dead servers show banners instead of a blank page; duplicate (title, photos) rows are addressable by ordinal. Consistency: shared CONFIDENT_VERSION_STATUSES, client_for(), Config paths for every artifact, one review-port constant, named matching thresholds, strict collection-id parsing, error-doc responses never cached, unknown config keys warn, extract reports dropped vision entries, fixture generators share escaping + marker text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+64
-5
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from bggpipe.config import Config
|
||||
from bggpipe.diff import compute_diff, load_snapshot_collection
|
||||
from bggpipe.models import CollectionItem
|
||||
|
||||
@@ -114,7 +115,10 @@ def test_owned_with_matching_version_is_just_owned():
|
||||
assert not result.to_update and not result.to_add
|
||||
|
||||
|
||||
def test_owned_with_different_version_reports_disagreement_untouched():
|
||||
def test_confident_version_matching_no_copy_is_a_second_copy_to_add():
|
||||
# Spec: a (bgg_id, version_id) pair is owned only if a collection item
|
||||
# matches BOTH. All copies carry different versions -> this is an
|
||||
# additional physical copy; existing entries are never edited.
|
||||
result = compute_diff(
|
||||
[
|
||||
_match(
|
||||
@@ -127,9 +131,10 @@ def test_owned_with_different_version_reports_disagreement_untouched():
|
||||
],
|
||||
[_item(266192, 5, version_id=465063)],
|
||||
)
|
||||
assert result.already_owned == ["Wingspan"]
|
||||
assert not result.to_update # additive only: never edit a set version
|
||||
assert "fourth printing" in result.disagreements[0]
|
||||
assert [r["version_id"] for r in result.to_add] == ["521212"]
|
||||
assert "fourth printing" in result.second_copies[0]
|
||||
assert result.already_owned == []
|
||||
|
||||
|
||||
def test_version_unknown_owned_by_bare_id():
|
||||
@@ -169,8 +174,11 @@ def test_pending_rejected_and_unseen_are_reported():
|
||||
def test_merged_rows_are_skipped_but_photos_carry_to_survivor():
|
||||
matches = [
|
||||
_match("Joking Hazard", "193621"),
|
||||
{**_match("Jokin Ha...", "193621", status="merged"),
|
||||
"merged_into": "Joking Hazard", "source_photos": "other.jpg"},
|
||||
{
|
||||
**_match("Jokin Ha...", "193621", status="merged"),
|
||||
"merged_into": "Joking Hazard",
|
||||
"source_photos": "other.jpg",
|
||||
},
|
||||
]
|
||||
result = compute_diff(matches, []) # empty collection -> to_add
|
||||
assert result.merged == 1
|
||||
@@ -178,3 +186,54 @@ def test_merged_rows_are_skipped_but_photos_carry_to_survivor():
|
||||
(row,) = result.to_add
|
||||
assert row["title_raw"] == "Joking Hazard"
|
||||
assert row["source_photos"] == "other.jpg;x.jpg" # combined
|
||||
|
||||
|
||||
def test_versionless_copies_exhaust_then_second_copy_becomes_add():
|
||||
# two confident-version matches, ONE versionless copy: the first consumes
|
||||
# it (to_update), the second is an additional physical copy (to_add)
|
||||
result = compute_diff(
|
||||
[
|
||||
_match("Sorcerer", "39", vstatus="version_auto", vid="111", vname="1st"),
|
||||
_match("Sorcerer", "39", vstatus="version_auto", vid="222", vname="2nd"),
|
||||
],
|
||||
[_item(39, 701)],
|
||||
)
|
||||
assert [u["version_id"] for u in result.to_update] == ["111"]
|
||||
assert [a["version_id"] for a in result.to_add] == ["222"]
|
||||
assert len(result.second_copies) == 1
|
||||
|
||||
|
||||
def test_run_diff_outputs_feed_upload_unchanged(tmp_path, monkeypatch):
|
||||
# the cross-stage contract: whatever run_diff writes, run_upload must
|
||||
# read — a column rename on either side has to fail HERE
|
||||
import shutil
|
||||
|
||||
from bggpipe.diff import run_diff
|
||||
from bggpipe.resolve import write_matches
|
||||
from bggpipe.upload import run_upload
|
||||
|
||||
monkeypatch.delenv("BGG_API_TOKEN", raising=False)
|
||||
cfg = Config(data_dir=tmp_path)
|
||||
fixtures = Path(__file__).parent / "fixtures"
|
||||
for name in (
|
||||
"collection_snapshot_base.xml",
|
||||
"collection_snapshot_expansions.xml",
|
||||
):
|
||||
shutil.copy(fixtures / name, tmp_path / name)
|
||||
write_matches(
|
||||
cfg.matches_path,
|
||||
[
|
||||
_match("Wingspan", "266192", status="auto"), # not in the snapshots
|
||||
_match("5 MINUTE DUNGEON", "207830", status="auto"), # owned
|
||||
],
|
||||
)
|
||||
result = run_diff(cfg)
|
||||
assert [r["bgg_id"] for r in result.to_add] == ["266192"]
|
||||
|
||||
from test_upload import FakeUploader
|
||||
|
||||
fake = FakeUploader()
|
||||
run_upload(cfg, uploader=fake, sleep=lambda s: None, now=lambda: "t")
|
||||
assert [(j.action, j.bgg_id, j.name) for j in fake.calls] == [
|
||||
("add", "266192", "Wingspan")
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user