38e20f2c30
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>
123 lines
4.6 KiB
Python
123 lines
4.6 KiB
Python
"""Write hand-crafted stub XML fixtures into tests/fixtures/bgg_cache/.
|
|
|
|
BGG's XML API now requires a registered application token, which we don't
|
|
have yet, so these fixtures are realistic approximations of real responses
|
|
(shapes verified against the API docs) rather than recordings. Once a
|
|
BGG_API_TOKEN exists: delete tests/fixtures/bgg_cache/ and run
|
|
scripts/record_fixtures.py — it writes real recordings under the SAME
|
|
filenames, and the test suite must still pass.
|
|
|
|
Usage: uv run python scripts/write_stub_fixtures.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from fixture_common import esc, write_cache_marker
|
|
|
|
from bggpipe.bgg_client import cache_key
|
|
|
|
FIXTURE_CACHE = Path("tests/fixtures/bgg_cache")
|
|
SEARCH_TYPES = "boardgame,boardgameexpansion"
|
|
|
|
|
|
def search_item(bgg_id: int, name: str, year: int | None, type_: str) -> str:
|
|
year_xml = f'<yearpublished value="{year}"/>' if year else ""
|
|
return (
|
|
f'<item type="{type_}" id="{bgg_id}">'
|
|
f'<name type="primary" value="{esc(name)}"/>{year_xml}</item>'
|
|
)
|
|
|
|
|
|
WINGSPAN_FAMILY = (
|
|
search_item(266192, "Wingspan", 2019, "boardgame")
|
|
+ search_item(290448, "Wingspan: European Expansion", 2019, "boardgameexpansion")
|
|
+ search_item(300580, "Wingspan: Oceania Expansion", 2020, "boardgameexpansion")
|
|
+ search_item(366161, "Wingspan Asia", 2022, "boardgame")
|
|
)
|
|
|
|
SEARCHES = {
|
|
"Catan": (
|
|
search_item(13, "CATAN", 1995, "boardgame")
|
|
+ search_item(926, "CATAN: Seafarers", 1997, "boardgameexpansion")
|
|
+ search_item(27760, "Catan Dice Game", 2007, "boardgame")
|
|
+ search_item(133038, "CATAN 3D Collector's Edition", 2005, "boardgame")
|
|
),
|
|
"Wingspan": WINGSPAN_FAMILY,
|
|
"Wingspan: European Expansion": WINGSPAN_FAMILY,
|
|
"Wingspan Europe": WINGSPAN_FAMILY,
|
|
"Café International": (
|
|
search_item(373, "Café International", 1989, "boardgame")
|
|
+ search_item(8103, "Café International: Das Kartenspiel", 2001, "boardgame")
|
|
),
|
|
"Citadels": (
|
|
search_item(478, "Citadels", 2000, "boardgame")
|
|
+ search_item(205398, "Citadels", 2016, "boardgame")
|
|
),
|
|
"Blorvath: Quest of the Zzyzx": "",
|
|
"Blorvath": "", # truncation-retry head of the nonsense title
|
|
}
|
|
|
|
THINGS = {
|
|
# Citadels tie-break: neither edition dominates 10x -> stays ambiguous.
|
|
("478,205398", "stats"): """<items>
|
|
<item type="boardgame" id="478">
|
|
<name type="primary" value="Citadels"/><yearpublished value="2000"/>
|
|
<statistics><ratings><owned value="85000"/>
|
|
<ranks><rank type="subtype" id="1" name="boardgame" value="250"/></ranks>
|
|
</ratings></statistics>
|
|
</item>
|
|
<item type="boardgame" id="205398">
|
|
<name type="primary" value="Citadels"/><yearpublished value="2016"/>
|
|
<statistics><ratings><owned value="24000"/>
|
|
<ranks><rank type="subtype" id="1" name="boardgame" value="400"/></ranks>
|
|
</ratings></statistics>
|
|
</item>
|
|
</items>""",
|
|
# Wingspan versions: 2019 Stonemaier English must beat the 2020 printing.
|
|
("266192", "versions"): """<items>
|
|
<item type="boardgame" id="266192">
|
|
<name type="primary" value="Wingspan"/><yearpublished value="2019"/>
|
|
<versions>
|
|
<item type="boardgameversion" id="465063">
|
|
<name type="primary" value="English first edition"/>
|
|
<yearpublished value="2019"/>
|
|
<link type="boardgamepublisher" id="23202" value="Stonemaier Games"/>
|
|
<link type="language" id="2184" value="English"/>
|
|
</item>
|
|
<item type="boardgameversion" id="521212">
|
|
<name type="primary" value="English fourth printing"/>
|
|
<yearpublished value="2020"/>
|
|
<link type="boardgamepublisher" id="23202" value="Stonemaier Games"/>
|
|
<link type="language" id="2184" value="English"/>
|
|
</item>
|
|
<item type="boardgameversion" id="472430">
|
|
<name type="primary" value="German edition"/>
|
|
<yearpublished value="2019"/>
|
|
<link type="boardgamepublisher" id="22160" value="Feuerland Spiele"/>
|
|
<link type="language" id="2188" value="German"/>
|
|
</item>
|
|
</versions>
|
|
</item>
|
|
</items>""",
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
FIXTURE_CACHE.mkdir(parents=True, exist_ok=True)
|
|
write_cache_marker(FIXTURE_CACHE)
|
|
for query, items in SEARCHES.items():
|
|
key = cache_key("search", {"query": query, "type": SEARCH_TYPES})
|
|
total = items.count("<item ")
|
|
(FIXTURE_CACHE / key).write_text(f'<items total="{total}">{items}</items>')
|
|
for (ids, flavor), xml in THINGS.items():
|
|
key = cache_key("thing", {"id": ids, flavor: "1"})
|
|
(FIXTURE_CACHE / key).write_text(xml)
|
|
for f in sorted(FIXTURE_CACHE.iterdir()):
|
|
print(f" {f.name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|