mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-03-07 22:22:34 +01:00
314 lines
12 KiB
Python
314 lines
12 KiB
Python
|
|
"""
|
||
|
|
Comprehensive Partner Suggestions Tests
|
||
|
|
|
||
|
|
This file consolidates partner suggestions tests from multiple sources:
|
||
|
|
- test_partner_suggestions_service.py (2 tests)
|
||
|
|
- test_partner_suggestions_pipeline.py (1 test)
|
||
|
|
|
||
|
|
Total: 3 tests organized into logical sections
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from code.web.services.partner_suggestions import (
|
||
|
|
configure_dataset_path,
|
||
|
|
get_partner_suggestions,
|
||
|
|
)
|
||
|
|
from code.scripts import build_partner_suggestions as pipeline
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Helper Functions & Test Data
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
def _write_dataset(path: Path) -> Path:
|
||
|
|
payload = {
|
||
|
|
"metadata": {
|
||
|
|
"generated_at": "2025-10-06T12:00:00Z",
|
||
|
|
"version": "test-fixture",
|
||
|
|
},
|
||
|
|
"commanders": {
|
||
|
|
"akiri_line_slinger": {
|
||
|
|
"name": "Akiri, Line-Slinger",
|
||
|
|
"display_name": "Akiri, Line-Slinger",
|
||
|
|
"color_identity": ["R", "W"],
|
||
|
|
"themes": ["Artifacts", "Aggro", "Legends Matter", "Partner"],
|
||
|
|
"role_tags": ["Aggro"],
|
||
|
|
"partner": {
|
||
|
|
"has_partner": True,
|
||
|
|
"partner_with": ["Silas Renn, Seeker Adept"],
|
||
|
|
"supports_backgrounds": False,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"silas_renn_seeker_adept": {
|
||
|
|
"name": "Silas Renn, Seeker Adept",
|
||
|
|
"display_name": "Silas Renn, Seeker Adept",
|
||
|
|
"color_identity": ["U", "B"],
|
||
|
|
"themes": ["Artifacts", "Value"],
|
||
|
|
"role_tags": ["Value"],
|
||
|
|
"partner": {
|
||
|
|
"has_partner": True,
|
||
|
|
"partner_with": ["Akiri, Line-Slinger"],
|
||
|
|
"supports_backgrounds": False,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"ishai_ojutai_dragonspeaker": {
|
||
|
|
"name": "Ishai, Ojutai Dragonspeaker",
|
||
|
|
"display_name": "Ishai, Ojutai Dragonspeaker",
|
||
|
|
"color_identity": ["W", "U"],
|
||
|
|
"themes": ["Artifacts", "Counters", "Historics Matter", "Partner - Survivors"],
|
||
|
|
"role_tags": ["Aggro"],
|
||
|
|
"partner": {
|
||
|
|
"has_partner": True,
|
||
|
|
"partner_with": [],
|
||
|
|
"supports_backgrounds": False,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"reyhan_last_of_the_abzan": {
|
||
|
|
"name": "Reyhan, Last of the Abzan",
|
||
|
|
"display_name": "Reyhan, Last of the Abzan",
|
||
|
|
"color_identity": ["B", "G"],
|
||
|
|
"themes": ["Counters", "Artifacts", "Partner"],
|
||
|
|
"role_tags": ["Counters"],
|
||
|
|
"partner": {
|
||
|
|
"has_partner": True,
|
||
|
|
"partner_with": [],
|
||
|
|
"supports_backgrounds": False,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"pairings": {
|
||
|
|
"records": [
|
||
|
|
{
|
||
|
|
"mode": "partner_with",
|
||
|
|
"primary_canonical": "akiri_line_slinger",
|
||
|
|
"secondary_canonical": "silas_renn_seeker_adept",
|
||
|
|
"count": 12,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"mode": "partner",
|
||
|
|
"primary_canonical": "akiri_line_slinger",
|
||
|
|
"secondary_canonical": "ishai_ojutai_dragonspeaker",
|
||
|
|
"count": 6,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"mode": "partner",
|
||
|
|
"primary_canonical": "akiri_line_slinger",
|
||
|
|
"secondary_canonical": "reyhan_last_of_the_abzan",
|
||
|
|
"count": 4,
|
||
|
|
},
|
||
|
|
]
|
||
|
|
},
|
||
|
|
}
|
||
|
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
||
|
|
return path
|
||
|
|
|
||
|
|
|
||
|
|
CSV_CONTENT = """name,faceName,colorIdentity,themeTags,roleTags,text,type,partnerWith,supportsBackgrounds,isPartner,isBackground,isDoctor,isDoctorsCompanion
|
||
|
|
"Halana, Kessig Ranger","Halana, Kessig Ranger","['G']","['Counters','Partner']","['Aggro']","Reach. Partner with Alena, Kessig Trapper.","Legendary Creature — Human Archer","['Alena, Kessig Trapper']",False,True,False,False,False
|
||
|
|
"Alena, Kessig Trapper","Alena, Kessig Trapper","['R']","['Aggro','Partner']","['Ramp']","First strike. Partner with Halana, Kessig Ranger.","Legendary Creature — Human Scout","['Halana, Kessig Ranger']",False,True,False,False,False
|
||
|
|
"Wilson, Refined Grizzly","Wilson, Refined Grizzly","['G']","['Teamwork','Backgrounds Matter']","['Aggro']","Choose a Background (You can have a Background as a second commander.)","Legendary Creature — Bear Warrior","[]",True,False,False,False,False
|
||
|
|
"Guild Artisan","Guild Artisan","['R']","['Background']","[]","Commander creatures you own have \"Whenever this creature attacks...\"","Legendary Enchantment — Background","[]",False,False,True,False,False
|
||
|
|
"The Tenth Doctor","The Tenth Doctor","['U','R','G']","['Time Travel']","[]","Doctor's companion (You can have two commanders if the other is a Doctor's companion.)","Legendary Creature — Time Lord Doctor","[]",False,False,False,True,False
|
||
|
|
"Rose Tyler","Rose Tyler","['W']","['Companions']","[]","Doctor's companion","Legendary Creature — Human","[]",False,False,False,False,True
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def _write_summary(path: Path, primary: str, secondary: str | None, mode: str, tags: list[str]) -> None:
|
||
|
|
payload = {
|
||
|
|
"meta": {
|
||
|
|
"commander": primary,
|
||
|
|
"tags": tags,
|
||
|
|
},
|
||
|
|
"summary": {
|
||
|
|
"commander": {
|
||
|
|
"names": [name for name in [primary, secondary] if name],
|
||
|
|
"primary": primary,
|
||
|
|
"secondary": secondary,
|
||
|
|
"partner_mode": mode,
|
||
|
|
"color_identity": [],
|
||
|
|
"combined": {
|
||
|
|
"primary_name": primary,
|
||
|
|
"secondary_name": secondary,
|
||
|
|
"partner_mode": mode,
|
||
|
|
"color_identity": [],
|
||
|
|
},
|
||
|
|
}
|
||
|
|
},
|
||
|
|
}
|
||
|
|
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def _write_text(path: Path, primary: str, secondary: str | None, mode: str) -> None:
|
||
|
|
lines = []
|
||
|
|
if secondary:
|
||
|
|
lines.append(f"# Commanders: {primary}, {secondary}")
|
||
|
|
else:
|
||
|
|
lines.append(f"# Commander: {primary}")
|
||
|
|
lines.append(f"# Partner Mode: {mode}")
|
||
|
|
lines.append(f"1 {primary}")
|
||
|
|
if secondary:
|
||
|
|
lines.append(f"1 {secondary}")
|
||
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Partner Suggestions Service Tests
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
def test_get_partner_suggestions_produces_visible_and_hidden(tmp_path: Path) -> None:
|
||
|
|
dataset_path = _write_dataset(tmp_path / "partner_synergy.json")
|
||
|
|
try:
|
||
|
|
configure_dataset_path(dataset_path)
|
||
|
|
result = get_partner_suggestions("Akiri, Line-Slinger", limit_per_mode=5)
|
||
|
|
assert result is not None
|
||
|
|
assert result.total >= 3
|
||
|
|
partner_names = [
|
||
|
|
"Silas Renn, Seeker Adept",
|
||
|
|
"Ishai, Ojutai Dragonspeaker",
|
||
|
|
"Reyhan, Last of the Abzan",
|
||
|
|
]
|
||
|
|
visible, hidden = result.flatten(partner_names, [], visible_limit=2)
|
||
|
|
assert len(visible) == 2
|
||
|
|
assert any(item["name"] == "Silas Renn, Seeker Adept" for item in visible)
|
||
|
|
assert hidden, "expected additional hidden suggestions"
|
||
|
|
assert result.metadata.get("generated_at") == "2025-10-06T12:00:00Z"
|
||
|
|
finally:
|
||
|
|
configure_dataset_path(None)
|
||
|
|
|
||
|
|
|
||
|
|
def test_noise_themes_suppressed_in_shared_theme_summary(tmp_path: Path) -> None:
|
||
|
|
dataset_path = _write_dataset(tmp_path / "partner_synergy.json")
|
||
|
|
try:
|
||
|
|
configure_dataset_path(dataset_path)
|
||
|
|
result = get_partner_suggestions("Akiri, Line-Slinger", limit_per_mode=5)
|
||
|
|
assert result is not None
|
||
|
|
partner_entries = result.by_mode.get("partner") or []
|
||
|
|
target = next((entry for entry in partner_entries if entry["name"] == "Ishai, Ojutai Dragonspeaker"), None)
|
||
|
|
assert target is not None, "expected Ishai suggestions to be present"
|
||
|
|
assert "Legends Matter" not in target["shared_themes"]
|
||
|
|
assert "Historics Matter" not in target["shared_themes"]
|
||
|
|
assert "Partner" not in target["shared_themes"]
|
||
|
|
assert "Partner - Survivors" not in target["shared_themes"]
|
||
|
|
assert all(theme not in {"Legends Matter", "Historics Matter", "Partner", "Partner - Survivors"} for theme in target["candidate_themes"])
|
||
|
|
assert "Legends Matter" not in target["summary"]
|
||
|
|
assert "Partner" not in target["summary"]
|
||
|
|
finally:
|
||
|
|
configure_dataset_path(None)
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Partner Suggestions Pipeline Tests
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
def test_build_partner_suggestions_creates_dataset(tmp_path: Path) -> None:
|
||
|
|
commander_csv = tmp_path / "commander_cards.csv"
|
||
|
|
commander_csv.write_text(CSV_CONTENT, encoding="utf-8")
|
||
|
|
|
||
|
|
deck_dir = tmp_path / "deck_files"
|
||
|
|
deck_dir.mkdir()
|
||
|
|
|
||
|
|
# Partner deck
|
||
|
|
_write_summary(
|
||
|
|
deck_dir / "halana_partner.summary.json",
|
||
|
|
primary="Halana, Kessig Ranger",
|
||
|
|
secondary="Alena, Kessig Trapper",
|
||
|
|
mode="partner",
|
||
|
|
tags=["Counters", "Aggro"],
|
||
|
|
)
|
||
|
|
_write_text(
|
||
|
|
deck_dir / "halana_partner.txt",
|
||
|
|
primary="Halana, Kessig Ranger",
|
||
|
|
secondary="Alena, Kessig Trapper",
|
||
|
|
mode="partner",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Background deck
|
||
|
|
_write_summary(
|
||
|
|
deck_dir / "wilson_background.summary.json",
|
||
|
|
primary="Wilson, Refined Grizzly",
|
||
|
|
secondary="Guild Artisan",
|
||
|
|
mode="background",
|
||
|
|
tags=["Teamwork", "Aggro"],
|
||
|
|
)
|
||
|
|
_write_text(
|
||
|
|
deck_dir / "wilson_background.txt",
|
||
|
|
primary="Wilson, Refined Grizzly",
|
||
|
|
secondary="Guild Artisan",
|
||
|
|
mode="background",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Doctor/Companion deck
|
||
|
|
_write_summary(
|
||
|
|
deck_dir / "doctor_companion.summary.json",
|
||
|
|
primary="The Tenth Doctor",
|
||
|
|
secondary="Rose Tyler",
|
||
|
|
mode="doctor_companion",
|
||
|
|
tags=["Time Travel", "Companions"],
|
||
|
|
)
|
||
|
|
_write_text(
|
||
|
|
deck_dir / "doctor_companion.txt",
|
||
|
|
primary="The Tenth Doctor",
|
||
|
|
secondary="Rose Tyler",
|
||
|
|
mode="doctor_companion",
|
||
|
|
)
|
||
|
|
|
||
|
|
output_path = tmp_path / "partner_synergy.json"
|
||
|
|
result = pipeline.build_partner_suggestions(
|
||
|
|
commander_csv=commander_csv,
|
||
|
|
deck_dir=deck_dir,
|
||
|
|
output_path=output_path,
|
||
|
|
max_examples=3,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert output_path.exists(), "Expected partner synergy dataset to be created"
|
||
|
|
data = json.loads(output_path.read_text(encoding="utf-8"))
|
||
|
|
|
||
|
|
metadata = data["metadata"]
|
||
|
|
assert metadata["deck_exports_processed"] == 3
|
||
|
|
assert metadata["deck_exports_with_pairs"] == 3
|
||
|
|
assert "version_hash" in metadata
|
||
|
|
|
||
|
|
overrides = data["curated_overrides"]
|
||
|
|
assert overrides["version"] == metadata["version_hash"]
|
||
|
|
assert overrides["entries"] == {}
|
||
|
|
|
||
|
|
mode_counts = data["pairings"]["mode_counts"]
|
||
|
|
assert mode_counts == {
|
||
|
|
"background": 1,
|
||
|
|
"doctor_companion": 1,
|
||
|
|
"partner": 1,
|
||
|
|
}
|
||
|
|
|
||
|
|
records = data["pairings"]["records"]
|
||
|
|
partner_entry = next(item for item in records if item["mode"] == "partner")
|
||
|
|
assert partner_entry["primary"] == "Halana, Kessig Ranger"
|
||
|
|
assert partner_entry["secondary"] == "Alena, Kessig Trapper"
|
||
|
|
assert partner_entry["combined_colors"] == ["R", "G"]
|
||
|
|
|
||
|
|
commanders = data["commanders"]
|
||
|
|
halana = commanders["halana, kessig ranger"]
|
||
|
|
assert halana["partner"]["has_partner"] is True
|
||
|
|
guild_artisan = commanders["guild artisan"]
|
||
|
|
assert guild_artisan["partner"]["is_background"] is True
|
||
|
|
|
||
|
|
themes = data["themes"]
|
||
|
|
aggro = themes["aggro"]
|
||
|
|
assert aggro["deck_count"] == 2
|
||
|
|
assert set(aggro["co_occurrence"].keys()) == {"counters", "teamwork"}
|
||
|
|
|
||
|
|
doctor_usage = commanders["the tenth doctor"]["usage"]
|
||
|
|
assert doctor_usage == {"primary": 1, "secondary": 0, "total": 1}
|
||
|
|
|
||
|
|
rose_usage = commanders["rose tyler"]["usage"]
|
||
|
|
assert rose_usage == {"primary": 0, "secondary": 1, "total": 1}
|
||
|
|
|
||
|
|
partner_tags = partner_entry["tags"]
|
||
|
|
assert partner_tags == ["Aggro", "Counters"]
|
||
|
|
|
||
|
|
# round-trip result returned from function should mirror file payload
|
||
|
|
assert result == data
|