mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 15:40:12 +01:00
Some checks failed
Editorial Lint / lint-editorial (push) Has been cancelled
- 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)
204 lines
No EOL
7.2 KiB
Python
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] |