- Add user-decks/ directory and USER_DECKS_DIR config for exported decks - Update export-to-sim to write to user-decks/ with static mount in lab_api - Replace import/paste UI with API-backed "My Decks" dropdown in LAN client and AI Lab, fetching from GET /api/lab/decks with source grouping - Fix GET /api/lab/decks to JOIN deck_cards for totalCards count - Fix deckgenerator: add missing DOM elements (dg-strategy-bar, dg-stats-bar, dg-card-groups) inside #dg-results, null guards on render functions, fix setLoading always hiding results - Replace gpt-oss:20b with claude-opus-4-20250514 across all files - Add user-decks/ to _BUILTIN_DECK_DIRS in deck_service.py - Enrich missing type_line from Forge card cache in DB deck path - Collapse import bar behind <details> toggle in AI Lab Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
105 lines
4.0 KiB
Python
105 lines
4.0 KiB
Python
"""Tests for DeckBuilderAdapter — the bridge between Ollama pipeline and legacy API."""
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
from types import SimpleNamespace
|
|
|
|
from src.commander_ai_lab.deck_builder.adapter import DeckBuilderAdapter
|
|
from src.commander_ai_lab.deck_builder.core.models import (
|
|
BuildResult,
|
|
CardEntry,
|
|
CommanderDeck,
|
|
DeckRatios,
|
|
)
|
|
|
|
|
|
def _fake_build_result():
|
|
"""Return a minimal BuildResult for mocking build_deck."""
|
|
cmdr = CardEntry(name="Atraxa", color_identity=["W", "U", "B", "G"])
|
|
basics = ["Plains", "Island", "Swamp", "Mountain", "Forest"]
|
|
cards = [
|
|
CardEntry(name=basics[i % 5], quantity=1, category="lands", color_identity={"W", "U", "B", "G"})
|
|
for i in range(99)
|
|
]
|
|
deck = CommanderDeck(commander=cmdr, cards=cards)
|
|
return BuildResult(
|
|
deck=deck,
|
|
warnings=["test warning"],
|
|
sources_consulted=["scryfall", "edhrec"],
|
|
build_time_seconds=1.23,
|
|
)
|
|
|
|
|
|
class TestDeckBuilderAdapter:
|
|
def test_init_defaults(self):
|
|
adapter = DeckBuilderAdapter()
|
|
assert adapter.model == "claude-opus-4-20250514"
|
|
assert adapter.db_conn_factory is None
|
|
|
|
@patch("src.commander_ai_lab.deck_builder.adapter.build_deck")
|
|
def test_generate_deck_returns_expected_shape(self, mock_build):
|
|
mock_build.return_value = _fake_build_result()
|
|
adapter = DeckBuilderAdapter()
|
|
result = adapter.generate_deck("Atraxa", strategy="superfriends")
|
|
|
|
assert "commander" in result
|
|
assert result["commander"]["name"] == "Atraxa"
|
|
assert "cards" in result
|
|
assert "stats" in result
|
|
assert result["model"] == "claude-opus-4-20250514"
|
|
assert result["build_time_seconds"] == 1.23
|
|
assert "scryfall" in result["reasoning"]["sources"]
|
|
|
|
@patch("src.commander_ai_lab.deck_builder.adapter.build_deck")
|
|
def test_generate_deck_fills_to_100(self, mock_build):
|
|
mock_build.return_value = _fake_build_result()
|
|
adapter = DeckBuilderAdapter()
|
|
result = adapter.generate_deck("Atraxa")
|
|
total = sum(c.get("count", 1) for c in result["cards"])
|
|
assert total == 100 # 99 cards + commander fills to 100
|
|
|
|
@patch("src.commander_ai_lab.deck_builder.adapter.build_deck")
|
|
def test_ownership_without_db(self, mock_build):
|
|
mock_build.return_value = _fake_build_result()
|
|
adapter = DeckBuilderAdapter(db_conn_factory=None)
|
|
result = adapter.generate_deck("Atraxa")
|
|
for card in result["cards"]:
|
|
assert card["owned"] is False
|
|
assert card["from_collection"] is False
|
|
|
|
|
|
class TestRunSubstitution:
|
|
def test_no_op_returns_namespace(self):
|
|
adapter = DeckBuilderAdapter()
|
|
cards = [
|
|
{"name": "Sol Ring", "owned": True},
|
|
{"name": "Mana Crypt", "owned": False},
|
|
]
|
|
result = adapter.run_substitution(cards, commander="Atraxa")
|
|
assert isinstance(result, SimpleNamespace)
|
|
assert result.owned_count == 1
|
|
assert result.substituted_count == 0
|
|
assert result.missing_count == 1
|
|
|
|
|
|
class TestHelpers:
|
|
def test_fill_basic_lands_adds_missing(self):
|
|
cards = [{"name": "Sol Ring", "count": 1, "category": "ramp"}]
|
|
filled = DeckBuilderAdapter._fill_basic_lands(cards, ["W", "U"], target_total=5)
|
|
total = sum(c.get("count", 1) for c in filled)
|
|
assert total == 5
|
|
|
|
def test_fill_basic_lands_no_excess(self):
|
|
cards = [{"name": f"Card{i}", "count": 1} for i in range(100)]
|
|
filled = DeckBuilderAdapter._fill_basic_lands(cards, ["W"], target_total=100)
|
|
assert len(filled) == 100
|
|
|
|
def test_compute_stats(self):
|
|
cards = [
|
|
{"name": "Sol Ring", "count": 1, "category": "ramp", "cmc": 1},
|
|
{"name": "Plains", "count": 36, "category": "lands", "cmc": 0},
|
|
]
|
|
stats = DeckBuilderAdapter._compute_stats(cards)
|
|
assert stats["total_cards"] == 37
|
|
assert stats["land_count"] == 36
|
|
assert stats["average_cmc"] == 1.0
|