Merge pull request #27 from mwisnowski/bugfix/fix-ci-testing

fix(ci): relax headless commander validation
This commit is contained in:
mwisnowski 2025-10-02 17:11:54 -07:00 committed by GitHub
commit 6db46daee4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 42 additions and 13 deletions

View file

@ -28,5 +28,7 @@
- QA documentation: added `docs/qa/mdfc_staging_checklist.md` outlining the staging validation pass required before removing the MDFC compatibility guard. - QA documentation: added `docs/qa/mdfc_staging_checklist.md` outlining the staging validation pass required before removing the MDFC compatibility guard.
## Fixed ## Fixed
- Documented friendly handling for missing `commander_cards.csv` data during manual QA drills to prevent white-screen failures.
- Headless runner commander validation now accepts fuzzy commander prefixes so automated builds using short commander names keep working.
- Setup filtering now applies security-stamp exclusions case-insensitively, preventing Acorn/Heart promo cards from entering Commander pools. - Setup filtering now applies security-stamp exclusions case-insensitively, preventing Acorn/Heart promo cards from entering Commander pools.
- Commander browser thumbnails restore the double-faced flip control so MDFC commanders expose both faces directly in the catalog. - Commander browser thumbnails restore the double-faced flip control so MDFC commanders expose both faces directly in the catalog.

View file

@ -3,7 +3,9 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os import os
import re
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from functools import lru_cache
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from deck_builder.builder import DeckBuilder from deck_builder.builder import DeckBuilder
@ -11,7 +13,6 @@ from deck_builder import builder_constants as bc
from file_setup.setup import initial_setup from file_setup.setup import initial_setup
from tagging import tagger from tagging import tagger
from exceptions import CommanderValidationError from exceptions import CommanderValidationError
from commander_exclusions import lookup_commander_detail
def _is_stale(file1: str, file2: str) -> bool: def _is_stale(file1: str, file2: str) -> bool:
"""Return True if file2 is missing or older than file1.""" """Return True if file2 is missing or older than file1."""
@ -73,7 +74,15 @@ def _normalize_commander_name(value: Any) -> str:
return str(value or "").strip().casefold() return str(value or "").strip().casefold()
def _load_commander_name_lookup() -> set[str]: def _tokenize_commander_name(value: Any) -> List[str]:
normalized = _normalize_commander_name(value)
if not normalized:
return []
return [token for token in re.split(r"[^a-z0-9]+", normalized) if token]
@lru_cache(maxsize=1)
def _load_commander_name_lookup() -> Tuple[set[str], Tuple[str, ...]]:
builder = DeckBuilder( builder = DeckBuilder(
headless=True, headless=True,
log_outputs=False, log_outputs=False,
@ -81,16 +90,19 @@ def _load_commander_name_lookup() -> set[str]:
input_func=lambda *_: "", input_func=lambda *_: "",
) )
df = builder.load_commander_data() df = builder.load_commander_data()
names: set[str] = set() raw_names: List[str] = []
for column in ("name", "faceName"): for column in ("name", "faceName"):
if column not in df.columns: if column not in df.columns:
continue continue
series = df[column].dropna().astype(str) series = df[column].dropna().astype(str)
for raw in series: raw_names.extend(series.tolist())
normalized = _normalize_commander_name(raw) normalized = {
if normalized: norm
names.add(normalized) for norm in (_normalize_commander_name(name) for name in raw_names)
return names if norm
}
ordered_raw = tuple(dict.fromkeys(raw_names))
return normalized, ordered_raw
def _validate_commander_available(command_name: str) -> None: def _validate_commander_available(command_name: str) -> None:
@ -98,11 +110,27 @@ def _validate_commander_available(command_name: str) -> None:
if not normalized: if not normalized:
return return
available = _load_commander_name_lookup() available, raw_names = _load_commander_name_lookup()
if normalized in available: if normalized in available:
return return
info = lookup_commander_detail(command_name) query_tokens = _tokenize_commander_name(command_name)
for candidate in raw_names:
candidate_norm = _normalize_commander_name(candidate)
if not candidate_norm:
continue
if candidate_norm.startswith(normalized):
return
candidate_tokens = _tokenize_commander_name(candidate)
if query_tokens and all(token in candidate_tokens for token in query_tokens):
return
try:
from commander_exclusions import lookup_commander_detail as _lookup_commander_detail # type: ignore[import-not-found]
except ImportError: # pragma: no cover
_lookup_commander_detail = None
info = _lookup_commander_detail(command_name) if _lookup_commander_detail else None
if info is not None: if info is not None:
primary_face = str(info.get("primary_face") or info.get("name") or "").strip() primary_face = str(info.get("primary_face") or info.get("name") or "").strip()
eligible_faces = info.get("eligible_faces") eligible_faces = info.get("eligible_faces")
@ -170,7 +198,6 @@ def run(
trimmed_commander = (command_name or "").strip() trimmed_commander = (command_name or "").strip()
if trimmed_commander: if trimmed_commander:
_validate_commander_available(trimmed_commander) _validate_commander_available(trimmed_commander)
command_name = trimmed_commander
owned_prompt_inputs: List[str] = [] owned_prompt_inputs: List[str] = []
owned_files_available = _headless_list_owned_files() owned_files_available = _headless_list_owned_files()

View file

@ -1,6 +1,6 @@
name,faceName,edhrecRank,colorIdentity,colors,manaCost,manaValue,type,creatureTypes,text,power,toughness,keywords,themeTags,layout,side name,faceName,edhrecRank,colorIdentity,colors,manaCost,manaValue,type,creatureTypes,text,power,toughness,keywords,themeTags,layout,side
Shock,,,,R,{R},1,Instant,,Deal 2 damage to any target.,,,,[Burn],normal, Shock,,,,R,{R},1,Instant,,Deal 2 damage to any target.,,,,[Burn],normal,
Plains,,,,W,,0,Land,,{T}: Add {W}.,,,,[Land],normal,name,faceName,edhrecRank,colorIdentity,colors,manaCost,manaValue,type,creatureTypes,text,power,toughness,keywords,themeTags,layout,side Plains,,,,W,,0,Land,,{T}: Add {W}.,,,,[Land],normal,
Sol Ring,,1,Colorless,,{1},{1},Artifact,,{T}: Add {C}{C}.,,,Mana,Utility,normal, Sol Ring,,1,Colorless,,{1},{1},Artifact,,{T}: Add {C}{C}.,,,Mana,Utility,normal,
Llanowar Elves,,5000,G,G,{G},{1},Creature,Elf Druid,{T}: Add {G}.,1,1,Mana,Tribal;Ramp,normal, Llanowar Elves,,5000,G,G,{G},{1},Creature,Elf Druid,{T}: Add {G}.,1,1,Mana,Tribal;Ramp,normal,
Island,,9999,U,U,,,Land,,{T}: Add {U}.,,,Land,,normal, Island,,9999,U,U,,,Land,,{T}: Add {U}.,,,Land,,normal,

1 name,faceName,edhrecRank,colorIdentity,colors,manaCost,manaValue,type,creatureTypes,text,power,toughness,keywords,themeTags,layout,side name faceName edhrecRank colorIdentity colors manaCost manaValue type creatureTypes text power toughness keywords themeTags layout side
2 Shock,,,,R,{R},1,Instant,,Deal 2 damage to any target.,,,,[Burn],normal, Shock R {R} 1 Instant Deal 2 damage to any target. [Burn] normal
3 Plains,,,,W,,0,Land,,{T}: Add {W}.,,,,[Land],normal,name,faceName,edhrecRank,colorIdentity,colors,manaCost,manaValue,type,creatureTypes,text,power,toughness,keywords,themeTags,layout,side Plains W 0 Land {T}: Add {W}. [Land] normal
4 Sol Ring,,1,Colorless,,{1},{1},Artifact,,{T}: Add {C}{C}.,,,Mana,Utility,normal, Sol Ring 1 Colorless {1} {1} Artifact {T}: Add {C}{C}. Mana Utility normal
5 Llanowar Elves,,5000,G,G,{G},{1},Creature,Elf Druid,{T}: Add {G}.,1,1,Mana,Tribal;Ramp,normal, Llanowar Elves 5000 G G {G} {1} Creature Elf Druid {T}: Add {G}. 1 1 Mana Tribal;Ramp normal
6 Island,,9999,U,U,,,Land,,{T}: Add {U}.,,,Land,,normal, Island 9999 U U Land {T}: Add {U}. Land normal