Judge / backend /tests /test_parse_cards.py
GonzaViss's picture
fix(cards): collapse cosmetic card variants by shared artwork
26a4699
Raw
History Blame Contribute Delete
11 kB
import copy
import json
from pathlib import Path
import pytest
from scripts.parse_cards import (
_filter_cards,
_render_card,
_render_markdown,
build_card_index,
parse_cards,
serialize_card_index_ts,
)
FIXTURES = Path(__file__).parent / "fixtures"
SAMPLE_JSON = FIXTURES / "cards_sample.json"
@pytest.fixture
def sample_cards() -> list[dict]:
return json.loads(SAMPLE_JSON.read_text(encoding="utf-8"))
@pytest.fixture
def yasuo(sample_cards) -> dict:
return copy.deepcopy(sample_cards[0])
# ---------------------------------------------------------------------------
# _render_card — single-card rendering
# ---------------------------------------------------------------------------
def test_render_uses_clean_name_as_h2_section(yasuo):
md = _render_card(yasuo)
assert md.startswith("## yasuo\n")
def test_render_includes_display_name(yasuo):
md = _render_card(yasuo)
assert "**Name**: Yasuo" in md
def test_render_set_line_uses_label_and_riftbound_id(yasuo):
md = _render_card(yasuo)
assert "**Set**: Origins (ORI-042)" in md
def test_render_includes_rarity(yasuo):
md = _render_card(yasuo)
assert "**Rarity**: Legendary" in md
def test_render_joins_multiple_domains(yasuo):
yasuo["classification"]["domain"] = ["Body", "Mind"]
md = _render_card(yasuo)
assert "**Domain**: Body, Mind" in md
def test_render_includes_full_stat_line_for_unit(yasuo):
md = _render_card(yasuo)
assert "**Energy**: 3" in md
assert "**Might**: 4" in md
assert "**Power**: 3" in md
assert "**Type**: Unit" in md
def test_render_omits_missing_stats(sample_cards):
counterspell = sample_cards[1]
md = _render_card(counterspell)
assert "**Energy**: 2" in md
assert "**Type**: Spell" in md
assert "**Might**" not in md
assert "**Power**" not in md
def test_render_joins_tags_with_commas(yasuo):
md = _render_card(yasuo)
assert "**Tags**: Accelerate, Quick-Draw" in md
def test_render_omits_tags_line_when_empty(sample_cards):
shen = sample_cards[3]
md = _render_card(shen)
assert "**Tags**" not in md
def test_render_uses_plain_text_not_rich(yasuo):
md = _render_card(yasuo)
assert "When Yasuo enters the board, draw a card." in md
assert "<p>" not in md
def test_render_includes_flavor_when_present(yasuo):
md = _render_card(yasuo)
assert '*Flavor*: "A wanderer\'s path is his own."' in md
def test_render_omits_flavor_when_missing(sample_cards):
counterspell = sample_cards[1]
md = _render_card(counterspell)
assert "*Flavor*" not in md
def test_render_preserves_multiline_card_text(yasuo):
md = _render_card(yasuo)
assert "When Yasuo enters the board" in md
assert "Activated: pay 1, ready Yasuo." in md
# ---------------------------------------------------------------------------
# _filter_cards — dedupe + alternate_art exclusion
# ---------------------------------------------------------------------------
def test_filter_excludes_alternate_art(sample_cards):
result = _filter_cards(sample_cards)
assert not any(c["metadata"]["alternate_art"] for c in result)
def test_filter_dedupes_by_riftbound_id(sample_cards):
result = _filter_cards(sample_cards)
riftbound_ids = [c["riftbound_id"] for c in result]
assert len(riftbound_ids) == len(set(riftbound_ids))
def test_filter_keeps_distinct_cards(sample_cards):
result = _filter_cards(sample_cards)
names = sorted(c["name"] for c in result)
assert names == ["Counterspell", "Shen", "Yasuo"]
# ---------------------------------------------------------------------------
# _render_markdown — full corpus
# ---------------------------------------------------------------------------
def test_render_markdown_produces_one_section_per_unique_card(sample_cards):
md = _render_markdown(sample_cards)
h2_count = sum(1 for line in md.splitlines() if line.startswith("## "))
assert h2_count == 3 # Yasuo, Counterspell, Shen — alternate art + duplicate filtered
def test_render_markdown_separates_cards_with_blank_line(sample_cards):
md = _render_markdown(sample_cards)
assert "\n\n## " in md
def test_render_markdown_skips_card_missing_clean_name():
broken = {"name": "Broken", "riftbound_id": "X-1", "metadata": {"alternate_art": False}}
md = _render_markdown([broken])
assert md.strip() == ""
# ---------------------------------------------------------------------------
# parse_cards — entry point with local JSON
# ---------------------------------------------------------------------------
def test_parse_cards_from_local_json_path(tmp_path, sample_cards):
fixture = tmp_path / "cards.json"
fixture.write_text(json.dumps(sample_cards), encoding="utf-8")
md = parse_cards(fixture)
assert "## yasuo" in md
assert "## counterspell" in md
assert "## shen" in md
# ---------------------------------------------------------------------------
# build_card_index — frontend lookup index generation
# ---------------------------------------------------------------------------
def test_build_card_index_excludes_alternate_art(sample_cards):
"""rc-003 in the fixture has alternate_art=true — must not appear in the index."""
index = build_card_index(sample_cards)
rb_ids = [e["riftbound_id"] for e in index]
assert rb_ids.count("ORI-042") <= 1 # alt version must be filtered
def test_build_card_index_excludes_signature(yasuo):
sig = copy.deepcopy(yasuo)
sig["riftbound_id"] = "ORI-042s"
sig["metadata"]["signature"] = True
index = build_card_index([sig])
assert index == []
def test_build_card_index_excludes_overnumbered(yasuo):
over = copy.deepcopy(yasuo)
over["riftbound_id"] = "ORI-042o"
over["metadata"]["overnumbered"] = True
index = build_card_index([over])
assert index == []
def test_build_card_index_dedupes_by_clean_name(sample_cards):
"""Fixture has 3 Yasuo records (rc-001, rc-003 alt, rc-005 reprint) — only one survives."""
index = build_card_index(sample_cards)
clean_names = [e["clean_name"] for e in index]
assert clean_names.count("yasuo") == 1
def test_build_card_index_first_match_wins_on_dedupe(sample_cards):
"""When multiple non-special cards share a clean_name, the first one in the input list wins."""
index = build_card_index(sample_cards)
yasuo_entry = next(e for e in index if e["clean_name"] == "yasuo")
assert yasuo_entry["riftbound_id"] == "ORI-042"
assert yasuo_entry["image_url"] == "https://example.com/yasuo.png" # rc-001's image, not rc-005's
def test_build_card_index_collapses_variant_sharing_image(yasuo):
"""A foil/promo variant (e.g. "... Metal") shares the base card's artwork.
It must collapse into a single entry keyed by image_url."""
base = copy.deepcopy(yasuo)
base["metadata"]["clean_name"] = "yasuo unforgiven"
base["riftbound_id"] = "ORI-042"
base["media"]["image_url"] = "https://example.com/yu.png"
metal = copy.deepcopy(base)
metal["metadata"]["clean_name"] = "yasuo unforgiven metal"
metal["riftbound_id"] = "ORI-042-M"
index = build_card_index([base, metal])
assert [e["clean_name"] for e in index] == ["yasuo unforgiven"]
def test_build_card_index_prefers_base_name_when_variant_appears_first(yasuo):
"""When the variant precedes the base in the input, the shorter base name still wins."""
metal = copy.deepcopy(yasuo)
metal["metadata"]["clean_name"] = "yasuo metal"
metal["riftbound_id"] = "ORI-042-M"
metal["media"]["image_url"] = "https://example.com/shared.png"
base = copy.deepcopy(yasuo)
base["metadata"]["clean_name"] = "yasuo"
base["riftbound_id"] = "ORI-042"
base["media"]["image_url"] = "https://example.com/shared.png"
index = build_card_index([metal, base])
assert [e["clean_name"] for e in index] == ["yasuo"]
def test_build_card_index_entry_has_exact_four_fields(sample_cards):
index = build_card_index(sample_cards)
for entry in index:
assert set(entry.keys()) == {"clean_name", "image_url", "set_label", "riftbound_id"}
def test_build_card_index_maps_fields_correctly(sample_cards):
index = build_card_index(sample_cards)
yasuo_entry = next(e for e in index if e["clean_name"] == "yasuo")
assert yasuo_entry["set_label"] == "Origins"
assert yasuo_entry["riftbound_id"] == "ORI-042"
assert yasuo_entry["image_url"].startswith("https://")
def test_build_card_index_skips_card_missing_clean_name():
broken = {
"name": "Broken",
"riftbound_id": "BR-1",
"metadata": {"alternate_art": False, "signature": False, "overnumbered": False},
"media": {"image_url": "https://x"},
"set": {"label": "Test"},
}
assert build_card_index([broken]) == []
def test_build_card_index_skips_card_missing_image_url(yasuo):
"""A card without an image_url can't be previewed — drop it from the index."""
no_img = copy.deepcopy(yasuo)
no_img["media"] = {}
assert build_card_index([no_img]) == []
def test_build_card_index_returns_full_sample(sample_cards):
"""End-to-end on the fixture: 3 unique cards after all filters."""
index = build_card_index(sample_cards)
clean_names = sorted(e["clean_name"] for e in index)
assert clean_names == ["counterspell", "shen", "yasuo"]
# ---------------------------------------------------------------------------
# serialize_card_index_ts — emit a TypeScript module
# ---------------------------------------------------------------------------
def test_serialize_ts_starts_with_auto_generated_banner(sample_cards):
ts = serialize_card_index_ts(build_card_index(sample_cards))
assert ts.startswith("// AUTO-GENERATED")
def test_serialize_ts_exports_interface(sample_cards):
ts = serialize_card_index_ts(build_card_index(sample_cards))
assert "export interface CardIndexEntry" in ts
assert "clean_name: string" in ts
assert "image_url: string" in ts
assert "set_label: string" in ts
assert "riftbound_id: string" in ts
def test_serialize_ts_exports_const_array_as_const(sample_cards):
ts = serialize_card_index_ts(build_card_index(sample_cards))
assert "export const CARD_INDEX: readonly CardIndexEntry[]" in ts
assert "as const" in ts
def test_serialize_ts_contains_entry_data(sample_cards):
ts = serialize_card_index_ts(build_card_index(sample_cards))
assert '"yasuo"' in ts
assert '"shen"' in ts
assert '"counterspell"' in ts
def test_serialize_ts_escapes_special_characters_safely():
"""If a card name contains a double quote, the TS literal must remain parseable."""
entries = [{
"clean_name": 'name with "quote"',
"image_url": "https://x",
"set_label": "Test",
"riftbound_id": "TS-1",
}]
ts = serialize_card_index_ts(entries)
# JSON-encoded quote is \" — must appear, must not appear as unescaped
assert '\\"quote\\"' in ts