A box of expansions in one pass: search, tick, done — with containment
Eric's Character Trove problem, both halves. The Titles add panel gains "search BGG and tick them off": one API search (rate-limited, cached) returns the whole family as a checklist, already-cataloged ids greyed out, and every ticked result lands as an APPROVED match row plus a title addition — the human picked it off BGG's own list, so resolve has nothing left to derive. A pick whose name matches an undecided photo line decides THAT line (photos kept) instead of duplicating it; BGG's true name twins (two games both called "Citadels") skip with an honest message rather than fusing. And the half Eric spotted mid-build: containment is real data, not a convention. A stored_in column on the match row (the container's bgg_id — human curation, riding the same durable CSV as dedupe_veto) is set by the pick panel's "they all live inside" selector, flows through enrich onto games.json, and surfaces both directions in the Library — "where it lives" on the content, "in this box" on the container. The dims report excludes contained games from the Kallax unknowns and counts them separately: a game with no box of its own has no shelf space to plan. 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
102507b040
commit
dbc1899759
@@ -103,6 +103,7 @@ def make_cfg(tmp_path) -> Config:
|
||||
[
|
||||
{
|
||||
"title_raw": "Citadels",
|
||||
"confidence": "high",
|
||||
"publisher_hint": "Fantasy Flight",
|
||||
"edition_hint": "",
|
||||
"source_photos": ["shelf.jpg"],
|
||||
@@ -1443,3 +1444,99 @@ def test_review_summary_reports_token_presence_not_value(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("BGG_API_TOKEN")
|
||||
web2, _ = make_client(tmp_path / "tokenless")
|
||||
assert web2.get("/api/state").json()["summary"]["token_present"] is False
|
||||
|
||||
|
||||
def test_bgg_search_and_add_picked_flow(tmp_path):
|
||||
"""A whole box of expansions in one pass: search once, tick, and each
|
||||
pick lands already-matched with its container recorded."""
|
||||
cfg = make_cfg(tmp_path)
|
||||
web = TestClient(create_app(cfg, client=fixture_search_client()))
|
||||
|
||||
found = web.get("/api/bgg-search", params={"q": "Citadels"}).json()["results"]
|
||||
assert len(found) >= 2
|
||||
ids = {r["bgg_id"] for r in found}
|
||||
assert 478 in ids
|
||||
assert all(r["already"] is False for r in found)
|
||||
|
||||
picks = [r for r in found if r["bgg_id"] in (478, 13291, 205398)]
|
||||
res = web.post(
|
||||
"/api/add-picked", json={"picks": picks, "stored_in": "173634"}
|
||||
).json()
|
||||
# 478 and The Dark City land; 205398 is BGG's OTHER game named
|
||||
# "Citadels" — a true name twin, skipped honestly rather than fused
|
||||
assert res["added"] == 2
|
||||
assert res["skipped"] == ["Citadels — a same-named line already exists"]
|
||||
|
||||
rows = {r["bgg_id"]: r for r in read_matches(cfg.matches_path)}
|
||||
# "Citadels" already existed as an undecided photo line: the pick
|
||||
# DECIDED that line (photos kept) instead of duplicating it
|
||||
assert rows["478"]["match_status"] == "approved"
|
||||
assert rows["478"]["source_photos"] == "shelf.jpg"
|
||||
assert rows["478"]["stored_in"] == "173634"
|
||||
# the expansion had no line: appended photo-less, already matched
|
||||
assert rows["13291"]["match_status"] == "approved"
|
||||
assert rows["13291"]["source_photos"] == ""
|
||||
|
||||
# re-searching now flags them; re-adding skips instead of duplicating
|
||||
found2 = web.get("/api/bgg-search", params={"q": "Citadels"}).json()["results"]
|
||||
assert next(r for r in found2 if r["bgg_id"] == 478)["already"] is True
|
||||
res2 = web.post("/api/add-picked", json={"picks": picks}).json()
|
||||
assert res2["added"] == 0 and len(res2["skipped"]) == 3
|
||||
|
||||
# blank query is a 400, and BGG-down is a 502, not a crash
|
||||
assert web.get("/api/bgg-search", params={"q": " "}).status_code == 400
|
||||
down = TestClient(create_app(cfg, client=unauthorized_client(tmp_path)))
|
||||
assert down.get("/api/bgg-search", params={"q": "x"}).status_code == 502
|
||||
|
||||
|
||||
def fixture_search_client() -> BGGClient:
|
||||
from pathlib import Path as _P
|
||||
|
||||
fixtures = _P(__file__).parent / "fixtures" / "bgg_cache"
|
||||
return BGGClient(
|
||||
cache_dir=fixtures,
|
||||
transport=httpx.MockTransport(
|
||||
lambda req: httpx.Response(401, text="Unauthorized")
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_library_detail_shows_containment_both_ways(tmp_path):
|
||||
cfg = make_cfg(tmp_path)
|
||||
rows = read_matches(cfg.matches_path)
|
||||
rows.append(
|
||||
_row(
|
||||
title_raw="The Trove",
|
||||
match_status="approved",
|
||||
bgg_id="173634",
|
||||
source_photos="shelf.jpg",
|
||||
)
|
||||
)
|
||||
rows.append(
|
||||
_row(
|
||||
title_raw="Witchdoctor",
|
||||
match_status="approved",
|
||||
bgg_id="999",
|
||||
source_photos="",
|
||||
stored_in="173634",
|
||||
)
|
||||
)
|
||||
write_matches(cfg.matches_path, rows)
|
||||
cfg.games_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"173634": {"bgg_id": 173634, "name": "The Trove", "type": "boardgame"},
|
||||
"999": {
|
||||
"bgg_id": 999,
|
||||
"name": "Witchdoctor",
|
||||
"type": "boardgameexpansion",
|
||||
"stored_in": "173634",
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
web = TestClient(create_app(cfg, client=unauthorized_client(tmp_path)))
|
||||
inside = web.get("/api/library/999").json()
|
||||
assert inside["stored_in_game"] == {"key": "173634", "name": "The Trove"}
|
||||
box = web.get("/api/library/173634").json()
|
||||
assert box["contains"] == [{"key": "999", "name": "Witchdoctor"}]
|
||||
|
||||
Reference in New Issue
Block a user