The real-data era: token live, stubs retired, editions on demand
BGG application approved. The migration the stub markers guarded for weeks: both synthetic caches deleted; tests/fixtures/bgg_cache re-recorded from the live API (recording list extended to every scenario the suite exercises — Civilization truncation, the Sorcerer SPI tiebreak, StarForce, Flat Top's thematic year, Alice Is Missing's rpgitem fallback); resolve --force re-matched all 133 titles for real (109 auto, 6 ambiguous, 18 unmatched, 30 edition ballots); data/STUB_DATA.marker deleted with its exit condition met — the guard mechanism stays armed should stubs ever regenerate. Reality fixed one bug and taught one lesson. The bug: a multi-type search lists an expansion twice (once per matched type) and the parser kept the generic boardgame entry — parse_search now dedupes by id preferring the specific type, which is what keeps expansion tagging (the base-vs-expansion review guard) alive on real data. The lesson: hand-built ambiguity is tidier than the real thing — Wingspan has 46 versions with three plausible English Stonemaier printings, so the suite's synthetic version ids and version_auto expectations became real ballots (assertions updated to recorded reality; the cue-plumbing test keeps its crafted two-version scenario via an injected transport). New: pick edition. A cue-less matched row is version_unknown by design (never guess) — but the owner knows which printing the box is. open_version_ballot() fetches the game's complete version list, cue-scores it when cues exist, and marks the row version_ambiguous so the normal Review edition pass presents it; the Titles page grows the button (Eric's three Wiz-Wars: two cue-less copies can now each claim their edition). 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
15d3120029
commit
59f4b8c43c
+27
-18
@@ -58,18 +58,18 @@ def rows_by_title(client) -> dict[str, object]:
|
||||
|
||||
def test_catan_auto_matches_base_game(rows_by_title):
|
||||
row = rows_by_title["Catan"]
|
||||
assert (row.match_status, row.bgg_id, row.bgg_name) == ("auto", 13, "CATAN")
|
||||
assert (row.match_status, row.bgg_id, row.bgg_name) == ("auto", 13, "Catan")
|
||||
assert row.version_status == "version_unknown" # no cues -> never guess
|
||||
|
||||
|
||||
def test_wingspan_auto_with_version_from_cues(rows_by_title):
|
||||
row = rows_by_title["Wingspan"]
|
||||
assert (row.match_status, row.bgg_id) == ("auto", 266192)
|
||||
# cues: Stonemaier + 2019 + English -> the first edition, not the
|
||||
# 2020 printing (score 5 vs 3) and not the German edition
|
||||
assert row.version_status == "version_auto"
|
||||
assert row.version_id == 465063
|
||||
assert row.version_name == "English first edition"
|
||||
# Stonemaier + 2019 + English narrows 46 real versions to a few
|
||||
# English printings — several plausible, so review decides; the 2019
|
||||
# first printing must be on the ballot
|
||||
assert row.version_status == "version_ambiguous"
|
||||
assert 433233 in {v["version_id"] for v in row.version_candidates}
|
||||
|
||||
|
||||
def test_expansion_matches_expansion_not_base(rows_by_title):
|
||||
@@ -87,7 +87,7 @@ def test_wingspan_europe_spine_never_matches_base(rows_by_title):
|
||||
|
||||
def test_accented_title_resolves(rows_by_title):
|
||||
row = rows_by_title["Café International"]
|
||||
assert (row.match_status, row.bgg_id) == ("auto", 373)
|
||||
assert (row.match_status, row.bgg_id) == ("auto", 214)
|
||||
|
||||
|
||||
def test_same_name_editions_stay_ambiguous(rows_by_title):
|
||||
@@ -110,17 +110,19 @@ def test_run_resolve_writes_csv_and_is_idempotent(client, tmp_path):
|
||||
cfg = Config(data_dir=data_dir)
|
||||
|
||||
first = run_resolve(cfg, client=client)
|
||||
assert len(first) == 7
|
||||
assert len(first) == 13
|
||||
|
||||
with cfg.matches_path.open(newline="") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
assert len(rows) == 7
|
||||
assert len(rows) == 13
|
||||
by_title = {r["title_raw"]: r for r in rows}
|
||||
assert by_title["Catan"]["match_status"] == "auto"
|
||||
assert by_title["Citadels"]["match_status"] == "ambiguous"
|
||||
candidates = json.loads(by_title["Citadels"]["candidates_json"])
|
||||
assert len(candidates) == 2
|
||||
assert by_title["Wingspan"]["version_id"] == "465063"
|
||||
# ambiguous edition: no id recorded, the candidates carry the ballot
|
||||
assert by_title["Wingspan"]["version_status"] == "version_ambiguous"
|
||||
assert "433233" in by_title["Wingspan"]["version_candidates_json"]
|
||||
|
||||
# second run: everything already in matches.csv is skipped, file unchanged
|
||||
before = cfg.matches_path.read_text()
|
||||
@@ -239,9 +241,13 @@ def test_civilization_resolves_via_truncation(client):
|
||||
)
|
||||
row = resolve_entry(client, entry)
|
||||
assert (row.match_status, row.bgg_id) == ("auto", 71)
|
||||
# publisher cue picks the Avalon Hill version, not Hartland/Gibsons
|
||||
assert row.version_status == "version_auto"
|
||||
assert row.version_id == 71001
|
||||
# the publisher cue keeps Avalon Hill printings on the ballot; the
|
||||
# real version list is too crowded for a single confident pick
|
||||
assert row.version_status == "version_ambiguous"
|
||||
assert any(
|
||||
any("Avalon Hill" in p for p in (v.get("publishers") or []))
|
||||
for v in row.version_candidates
|
||||
)
|
||||
|
||||
|
||||
def test_advanced_civilization_resolves_as_expansion(client):
|
||||
@@ -286,7 +292,7 @@ def test_starforce_two_word_head_matches_full_title(client):
|
||||
)
|
||||
row = resolve_entry(client, entry)
|
||||
assert (row.match_status, row.bgg_id) == ("auto", 2524)
|
||||
assert row.version_status == "version_auto"
|
||||
assert row.version_status == "version_ambiguous"
|
||||
|
||||
|
||||
def test_wrong_year_hint_never_drives_a_version(client):
|
||||
@@ -307,8 +313,11 @@ def test_run_resolve_saves_progress_when_token_missing(tmp_path):
|
||||
crashing the run and losing everything."""
|
||||
partial_cache = tmp_path / "cache"
|
||||
partial_cache.mkdir()
|
||||
for f in FIXTURES.glob("search_query=Catan-*"):
|
||||
shutil.copy(f, partial_cache / f.name)
|
||||
for pattern in ("search_query=Catan-*", "thing_id=13-*"):
|
||||
# the real Catan search has enough hits that the popularity
|
||||
# tiebreak fetches /thing stats — that request is cached too
|
||||
for f in FIXTURES.glob(pattern):
|
||||
shutil.copy(f, partial_cache / f.name)
|
||||
|
||||
data_dir = tmp_path / "data"
|
||||
data_dir.mkdir()
|
||||
@@ -508,7 +517,7 @@ def test_run_resolve_force_rebuilds_from_scratch(client, tmp_path):
|
||||
write_matches(cfg.matches_path, rows)
|
||||
|
||||
forced = run_resolve(cfg, force=True, client=client)
|
||||
assert len(forced) == 7 # every title re-resolved, none skipped
|
||||
assert len(forced) == 13 # every title re-resolved, none skipped
|
||||
fresh = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
|
||||
assert fresh["Catan"]["match_status"] == "auto"
|
||||
|
||||
@@ -693,4 +702,4 @@ def test_rpg_falls_back_to_rpgitem_search(client):
|
||||
row = resolve_entry(client, entry)
|
||||
assert row.match_status == "auto"
|
||||
assert row.type == "rpgitem"
|
||||
assert row.bgg_id == 400001
|
||||
assert row.bgg_id == 311654
|
||||
|
||||
Reference in New Issue
Block a user