Files
commander-ai-lab/tests/conftest.py
cf4e2981e6 fix: resolve 78 test failures via shared stub cleanup and dependent fixes (#85) (#88)
Centralize and enhance test stubs to implement the full CommanderGameState
interface (battlefield(), living_players(), stack_is_empty(), sim_state,
active_player(), priority_player(), to_dict()). Fix all dependent test
failures across 12 files:

- conftest.py: Enhanced StubCard/StubPlayer/StubGameState with full API
  compatibility; added basic_creature fixture; fixed fresh_app singleton leak
- test_phase3.py: Added battlefield(), active_player(), prompt_builder compat
- test_integration.py: Fixed ThreatAssessor import (class→function), added
  threat_fn dict wrapper, sim_state delegate
- test_phase6.py/test_phase8.py: Replaced deprecated get_event_loop() with
  asyncio.run()
- test_benchmark.py: Same async fix + ThreatAssessor functional API
- sim/test_engine.py: Removed nonexistent seed= param, use run() method
- sim/test_models.py: basic_creature fixture now in conftest
- deck_builder/test_models.py: Updated ValidationError regex match
- state_logger.py: Added monotonic counter for unique snapshot filenames
- ai_opponent.py: Moved _CANNED dict to module-level constant (dataclass fix)
- web/app.py: Mount lobby router (fixes 6 LobbyAPI test failures)
- test_phase5.py: SessionStore + api._store reset fixture

Result: 76 failures + 2 errors → 0 failures, 0 errors (331 passed, 30 skipped)

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 17:32:30 -04:00

284 lines
8.7 KiB
Python

"""
Commander AI Lab — Shared Test Fixtures (Phase 7)
==================================================
Provides pytest fixtures reused across all phase test modules.
"""
from __future__ import annotations
import asyncio
import os
from dataclasses import dataclass, field
from typing import Optional
import pytest
# Headless Pygame for GUI tests
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
# ---------------------------------------------------------------------------
# Stub domain objects (shared across all phases)
# ---------------------------------------------------------------------------
@dataclass
class StubCard:
name: str
type: str = "creature"
type_line: str = ""
pt: Optional[str] = "2/2"
cmc: int = 3
tapped: bool = False
is_commander: bool = False
oracle: str = ""
oracle_text: str = ""
power: str = ""
toughness: str = ""
keywords: list = field(default_factory=list)
def get_power(self) -> int:
if self.power:
try:
return int(self.power)
except ValueError:
return 0
if not self.pt:
return 0
parts = self.pt.split("/")
try:
return int(parts[0])
except (ValueError, IndexError):
return 0
def get_toughness(self) -> int:
if self.toughness:
try:
return int(self.toughness)
except ValueError:
return 0
if not self.pt:
return 0
parts = self.pt.split("/")
try:
return int(parts[1])
except (ValueError, IndexError):
return 0
def is_land(self) -> bool:
return bool(self.type_line and "land" in self.type_line.lower())
def is_creature(self) -> bool:
return bool(self.type_line and "creature" in self.type_line.lower())
def has_keyword(self, kw: str) -> bool:
kw_lower = kw.lower()
if self.keywords and any(k.lower() == kw_lower for k in self.keywords):
return True
return kw_lower in (self.oracle_text or "").lower()
def to_dict(self) -> dict:
return {
"name": self.name,
"type": self.type,
"pt": self.pt,
"cmc": self.cmc,
"tapped": self.tapped,
"is_commander": self.is_commander,
"oracle": self.oracle,
}
@dataclass
class StubPlayer:
name: str
seat: int = 0
life: int = 40
eliminated: bool = False
hand: list = field(default_factory=list)
battlefield: list = field(default_factory=list)
graveyard: list = field(default_factory=list)
exile: list = field(default_factory=list)
command_zone: list = field(default_factory=list)
library: list = field(default_factory=list)
commander_damage: dict = field(default_factory=dict)
mana_pool: dict = field(default_factory=dict)
def to_dict(self, private: bool = False) -> dict:
return {
"name": self.name,
"seat": self.seat,
"life": self.life,
"eliminated": self.eliminated,
"hand_count": len(self.hand),
"hand": [c.to_dict() if hasattr(c, 'to_dict') else c for c in self.hand]
if private else [],
"battlefield": [c.to_dict() if hasattr(c, 'to_dict') else c for c in self.battlefield],
"graveyard": [c.to_dict() if hasattr(c, 'to_dict') else c for c in self.graveyard],
"exile": [c.to_dict() if hasattr(c, 'to_dict') else c for c in self.exile],
"command_zone":[c.to_dict() if hasattr(c, 'to_dict') else c for c in self.command_zone],
}
class _StubSimState:
"""Minimal SimState-compatible stub — delegates to player battlefields."""
def __init__(self, players: list) -> None:
self._players = players
def get_battlefield(self, seat: int) -> list:
if 0 <= seat < len(self._players):
return self._players[seat].battlefield
return []
def add_to_battlefield(self, seat: int, card) -> None:
if 0 <= seat < len(self._players):
self._players[seat].battlefield.append(card)
class StubGameState:
"""
Shared test stub that implements the CommanderGameState interface
used by ThreatAssessor, TurnManager, and other subsystems.
"""
def __init__(
self,
num_players: int = 4,
names: Optional[list[str]] = None,
) -> None:
names = names or [f"P{i}" for i in range(num_players)]
self.players = [StubPlayer(name=n, seat=i) for i, n in enumerate(names)]
self.turn = 1
self.current_phase = "main1"
self.active_player_seat = 0
self.priority_seat = 0
self.stack: list = []
self.game_over = False
self.winner = None
self.commander_damage: dict[tuple, int] = {}
# SimState-compatible sub-object for turn_manager._phase_untap() etc.
self.sim_state = _StubSimState(self.players)
# -- CommanderGameState interface methods --
def battlefield(self, seat: int) -> list:
"""Return the battlefield for *seat* (delegates to sim_state)."""
return self.sim_state.get_battlefield(seat)
def stack_is_empty(self) -> bool:
return len(self.stack) == 0
def living_players(self) -> list:
return [p for p in self.players if not p.eliminated]
def get_legal_moves(self, seat: int) -> list[dict]:
return [
{"id": 1, "category": "pass_priority", "description": "Pass priority"},
{"id": 2, "category": "play_land", "description": "Play a land"},
{"id": 3, "category": "cast_spell", "description": "Cast a spell"},
]
def apply_move(self, seat: int, move_id: int) -> None:
pass
def advance_turn(self) -> None:
self.active_player_seat = (self.active_player_seat + 1) % len(self.players)
if self.active_player_seat == 0:
self.turn += 1
def to_dict(self) -> dict:
return {
"turn": self.turn,
"current_phase": self.current_phase,
"active_player_seat": self.active_player_seat,
"players": [p.to_dict() for p in self.players],
}
# ---------------------------------------------------------------------------
# Pytest fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def game_state():
return StubGameState()
@pytest.fixture
def game_state_2p():
return StubGameState(num_players=2, names=["Alice", "Bob"])
@pytest.fixture
def basic_creature():
"""A simple Card instance for clone and attribute tests."""
from commander_ai_lab.sim.models import Card
return Card(
name="Grizzly Bears",
type_line="Creature — Bear",
cmc=2,
pt="2/2",
power="2",
toughness="2",
)
@pytest.fixture
def targeting_memory():
from commander_ai_lab.sim.politics.memory import TargetingMemory
return TargetingMemory(num_players=4)
@pytest.fixture
def comms_channel():
from commander_ai_lab.sim.politics.comms import PoliticsCommsChannel
return PoliticsCommsChannel()
@pytest.fixture
def politics_engine(targeting_memory, comms_channel):
from commander_ai_lab.sim.politics.engine import PoliticsEngine
return PoliticsEngine(
num_players=4,
memory=targeting_memory,
comms=comms_channel,
personalities={0: "timmy", 1: "spike", 2: "johnny", 3: "aggressive"},
threat_fn=lambda s: {i: 0.3 for i in range(4) if i != s},
)
@pytest.fixture
def threat_assessor():
from commander_ai_lab.sim.threat_assessor import ThreatAssessor
gs = StubGameState()
return ThreatAssessor(game_state=gs, num_players=4)
@pytest.fixture
def fresh_app():
"""FastAPI test app with a clean session store."""
from commander_ai_lab.web.session_store import SessionStore
SessionStore._instance = None
import commander_ai_lab.web.routers.api as api_mod
api_mod._store = SessionStore()
from commander_ai_lab.web.app import create_app
app = create_app()
yield app
SessionStore._instance = None
@pytest.fixture
def http_client(fresh_app):
from fastapi.testclient import TestClient
return TestClient(fresh_app)
@pytest.fixture
def event_loop():
"""Provide a fresh event loop for async tests."""
loop = asyncio.new_event_loop()
yield loop
loop.close()