"""Resolve-stage tests, replayed from tests/fixtures/bgg_cache — never online. The transport below raises on any network attempt, proving every response comes from the committed fixture cache. """ from __future__ import annotations import csv import json import shutil from pathlib import Path import httpx import pytest from bggpipe.bgg_client import BGGClient from bggpipe.config import Config from bggpipe.models import GameVersion from bggpipe.resolve import ( Candidate, TitleEntry, _dominant, _score_version, load_titles, resolve_entry, run_resolve, ) FIXTURES = Path(__file__).parent / "fixtures" / "bgg_cache" TITLES_JSON = Path(__file__).parent / "data" / "titles.json" def _no_network(request: httpx.Request) -> httpx.Response: raise AssertionError(f"test hit the network: {request.url}") @pytest.fixture def client() -> BGGClient: return BGGClient( cache_dir=FIXTURES, transport=httpx.MockTransport(_no_network), sleep=lambda s: pytest.fail("slept during fixture replay"), ) @pytest.fixture def rows_by_title(client) -> dict[str, object]: entries = load_titles(TITLES_JSON) return {e.title_raw: resolve_entry(client, e) for e in entries} 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.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" def test_expansion_matches_expansion_not_base(rows_by_title): row = rows_by_title["Wingspan: European Expansion"] assert (row.match_status, row.bgg_id) == ("auto", 290448) assert row.type == "boardgameexpansion" def test_wingspan_europe_spine_never_matches_base(rows_by_title): """The spec's canonical trap: a truncated spine must not auto-match.""" row = rows_by_title["Wingspan Europe"] assert row.match_status == "unmatched" assert row.bgg_id is None def test_accented_title_resolves(rows_by_title): row = rows_by_title["Café International"] assert (row.match_status, row.bgg_id) == ("auto", 373) def test_same_name_editions_stay_ambiguous(rows_by_title): row = rows_by_title["Citadels"] assert row.match_status == "ambiguous" assert {c.bgg_id for c in row.candidates} == {478, 205398} # stats were fetched for the review UI assert all(c.owned is not None for c in row.candidates) def test_nonsense_lands_unmatched(rows_by_title): row = rows_by_title["Blorvath: Quest of the Zzyzx"] assert row.match_status == "unmatched" def test_run_resolve_writes_csv_and_is_idempotent(client, tmp_path): data_dir = tmp_path / "data" data_dir.mkdir() shutil.copy(TITLES_JSON, data_dir / "titles.json") cfg = Config(data_dir=data_dir) first = run_resolve(cfg, client=client) assert len(first) == 7 with cfg.matches_path.open(newline="") as f: rows = list(csv.DictReader(f)) assert len(rows) == 7 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" # second run: everything already in matches.csv is skipped, file unchanged before = cfg.matches_path.read_text() second = run_resolve(cfg, client=client) assert second == [] assert cfg.matches_path.read_text() == before # -- pure-logic unit tests (no fixtures) -------------------------------- def _cand(bgg_id, exact=True, owned=None, type_="boardgame"): return Candidate( bgg_id=bgg_id, name="X", year=2000, type=type_, exact=exact, fuzzy=100.0, owned=owned, ) def test_dominant_famous_game_beats_obscure_duplicate(): top = [_cand(1, owned=50000), _cand(2, owned=300)] assert _dominant(top).bgg_id == 1 def test_dominant_refuses_mixed_types(): top = [_cand(1, owned=50000), _cand(2, owned=300, type_="boardgameexpansion")] assert _dominant(top) is None def test_dominant_refuses_close_call(): top = [_cand(1, owned=50000), _cand(2, owned=20000)] assert _dominant(top) is None def test_score_version_all_cues(): entry = TitleEntry( title_raw="Wingspan", title_normalized="wingspan", publisher_hint="Stonemaier", year_hint=2019, language_hint="english", ) version = GameVersion( version_id=1, name="English first edition", year=2019, publishers=("Stonemaier Games",), languages=("English",), ) assert _score_version(entry, version) == 5 def test_score_version_no_overlap(): entry = TitleEntry( title_raw="Wingspan", title_normalized="wingspan", publisher_hint="Feuerland", year_hint=2021, language_hint="german", ) version = GameVersion( version_id=1, name="English first edition", year=2019, publishers=("Stonemaier Games",), languages=("English",), ) assert _score_version(entry, version) == 0