mtg_python_deckbuilder/code/tests/test_random_multi_theme_webflows.py
matt 49f1f8b2eb
Some checks failed
Editorial Lint / lint-editorial (push) Has been cancelled
feat(random): finalize multi-theme telemetry and polish
- document random theme exclusions, perf guard tooling, and roadmap completion

- tighten random reroll UX: strict theme persistence, throttle handling, export parity, diagnostics updates

- add regression coverage for telemetry counters, multi-theme flows, and locked rerolls; refresh README and notes

Tests: pytest -q (fast random + telemetry suites)
2025-09-26 18:15:52 -07:00

204 lines
No EOL
7.2 KiB
Python

from __future__ import annotations
import base64
import json
import os
from typing import Any, Dict, Iterator, List
from urllib.parse import urlencode
import importlib
import pytest
from fastapi.testclient import TestClient
from deck_builder.random_entrypoint import RandomFullBuildResult
def _decode_state_token(token: str) -> Dict[str, Any]:
pad = "=" * (-len(token) % 4)
raw = base64.urlsafe_b64decode((token + pad).encode("ascii")).decode("utf-8")
return json.loads(raw)
@pytest.fixture()
def client(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestClient]:
monkeypatch.setenv("RANDOM_MODES", "1")
monkeypatch.setenv("RANDOM_UI", "1")
monkeypatch.setenv("CSV_FILES_DIR", os.path.join("csv_files", "testdata"))
web_app_module = importlib.import_module("code.web.app")
web_app_module = importlib.reload(web_app_module)
from code.web.services import tasks
tasks._SESSIONS.clear()
with TestClient(web_app_module.app) as test_client:
yield test_client
tasks._SESSIONS.clear()
def _make_full_result(seed: int) -> RandomFullBuildResult:
return RandomFullBuildResult(
seed=seed,
commander=f"Commander-{seed}",
theme="Aggro",
constraints={},
primary_theme="Aggro",
secondary_theme="Tokens",
tertiary_theme="Equipment",
resolved_themes=["aggro", "tokens", "equipment"],
combo_fallback=False,
synergy_fallback=False,
fallback_reason=None,
decklist=[{"name": "Sample Card", "count": 1}],
diagnostics={"elapsed_ms": 5},
summary={"meta": {"existing": True}},
csv_path=None,
txt_path=None,
compliance=None,
)
def test_random_multi_theme_reroll_same_commander_preserves_resolved(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
import deck_builder.random_entrypoint as random_entrypoint
import headless_runner
from code.web.services import tasks
build_calls: List[Dict[str, Any]] = []
def fake_build_random_full_deck(*, theme, constraints, seed, attempts, timeout_s, primary_theme, secondary_theme, tertiary_theme):
build_calls.append(
{
"theme": theme,
"primary": primary_theme,
"secondary": secondary_theme,
"tertiary": tertiary_theme,
"seed": seed,
}
)
return _make_full_result(int(seed))
monkeypatch.setattr(random_entrypoint, "build_random_full_deck", fake_build_random_full_deck)
class DummyBuilder:
def __init__(self, commander: str, seed: int) -> None:
self.commander_name = commander
self.commander = commander
self.deck_list_final: List[Dict[str, Any]] = []
self.last_csv_path = None
self.last_txt_path = None
self.custom_export_base = commander
def build_deck_summary(self) -> Dict[str, Any]:
return {"meta": {"rebuild": True}}
def export_decklist_csv(self) -> str:
return "deck_files/placeholder.csv"
def export_decklist_text(self, filename: str | None = None) -> str:
return "deck_files/placeholder.txt"
def compute_and_print_compliance(self, base_stem: str | None = None) -> Dict[str, Any]:
return {"ok": True}
reroll_runs: List[Dict[str, Any]] = []
def fake_run(command_name: str, seed: int | None = None):
reroll_runs.append({"commander": command_name, "seed": seed})
return DummyBuilder(command_name, seed or 0)
monkeypatch.setattr(headless_runner, "run", fake_run)
tasks._SESSIONS.clear()
resp1 = client.post(
"/hx/random_reroll",
json={
"mode": "surprise",
"primary_theme": "Aggro",
"secondary_theme": "Tokens",
"tertiary_theme": "Equipment",
"seed": 1010,
},
)
assert resp1.status_code == 200, resp1.text
assert build_calls and build_calls[0]["primary"] == "Aggro"
assert "value=\"aggro||tokens||equipment\"" in resp1.text
sid = client.cookies.get("sid")
assert sid
session = tasks.get_session(sid)
resolved_list = session.get("random_build", {}).get("resolved_theme_info", {}).get("resolved_list")
assert resolved_list == ["aggro", "tokens", "equipment"]
commander = f"Commander-{build_calls[0]['seed']}"
form_payload = [
("mode", "reroll_same_commander"),
("commander", commander),
("seed", str(build_calls[0]["seed"])),
("resolved_themes", "aggro||tokens||equipment"),
]
encoded = urlencode(form_payload, doseq=True)
resp2 = client.post(
"/hx/random_reroll",
content=encoded,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
assert resp2.status_code == 200, resp2.text
assert len(build_calls) == 1
assert reroll_runs and reroll_runs[0]["commander"] == commander
assert "value=\"aggro||tokens||equipment\"" in resp2.text
session_after = tasks.get_session(sid)
resolved_after = session_after.get("random_build", {}).get("resolved_theme_info", {}).get("resolved_list")
assert resolved_after == ["aggro", "tokens", "equipment"]
def test_random_multi_theme_permalink_roundtrip(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
import deck_builder.random_entrypoint as random_entrypoint
from code.web.services import tasks
seeds_seen: List[int] = []
def fake_build_random_full_deck(*, theme, constraints, seed, attempts, timeout_s, primary_theme, secondary_theme, tertiary_theme):
seeds_seen.append(int(seed))
return _make_full_result(int(seed))
monkeypatch.setattr(random_entrypoint, "build_random_full_deck", fake_build_random_full_deck)
tasks._SESSIONS.clear()
resp = client.post(
"/api/random_full_build",
json={
"seed": 4242,
"primary_theme": "Aggro",
"secondary_theme": "Tokens",
"tertiary_theme": "Equipment",
},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["primary_theme"] == "Aggro"
assert body["secondary_theme"] == "Tokens"
assert body["tertiary_theme"] == "Equipment"
assert body["resolved_themes"] == ["aggro", "tokens", "equipment"]
permalink = body["permalink"]
assert permalink and permalink.startswith("/build/from?state=")
visit = client.get(permalink)
assert visit.status_code == 200
state_resp = client.get("/build/permalink")
assert state_resp.status_code == 200, state_resp.text
state_payload = state_resp.json()
token = state_payload["permalink"].split("state=", 1)[1]
decoded = _decode_state_token(token)
random_section = decoded.get("random") or {}
assert random_section.get("primary_theme") == "Aggro"
assert random_section.get("secondary_theme") == "Tokens"
assert random_section.get("tertiary_theme") == "Equipment"
assert random_section.get("resolved_themes") == ["aggro", "tokens", "equipment"]
requested = random_section.get("requested_themes") or {}
assert requested.get("primary") == "Aggro"
assert requested.get("secondary") == "Tokens"
assert requested.get("tertiary") == "Equipment"
assert seeds_seen == [4242]