mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 15:40:12 +01:00
370 lines
14 KiB
Python
370 lines
14 KiB
Python
"""Tests for consolidated theme enrichment pipeline.
|
|
|
|
These tests verify that the new consolidated pipeline produces the same results
|
|
as the old 7-script approach, but much faster.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any, Dict
|
|
|
|
import pytest
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
yaml = None
|
|
|
|
from code.tagging.theme_enrichment import (
|
|
ThemeEnrichmentPipeline,
|
|
EnrichmentStats,
|
|
run_enrichment_pipeline,
|
|
)
|
|
|
|
|
|
# Skip all tests if PyYAML not available
|
|
pytestmark = pytest.mark.skipif(yaml is None, reason="PyYAML not installed")
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_catalog_dir(tmp_path: Path) -> Path:
|
|
"""Create temporary catalog directory with test themes."""
|
|
catalog_dir = tmp_path / 'config' / 'themes' / 'catalog'
|
|
catalog_dir.mkdir(parents=True)
|
|
return catalog_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_root(tmp_path: Path, temp_catalog_dir: Path) -> Path:
|
|
"""Create temporary project root."""
|
|
# Create theme_list.json
|
|
theme_json = tmp_path / 'config' / 'themes' / 'theme_list.json'
|
|
theme_json.parent.mkdir(parents=True, exist_ok=True)
|
|
theme_json.write_text('{"themes": []}', encoding='utf-8')
|
|
return tmp_path
|
|
|
|
|
|
def write_theme(catalog_dir: Path, filename: str, data: Dict[str, Any]) -> Path:
|
|
"""Helper to write a theme YAML file."""
|
|
path = catalog_dir / filename
|
|
path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding='utf-8')
|
|
return path
|
|
|
|
|
|
def read_theme(path: Path) -> Dict[str, Any]:
|
|
"""Helper to read a theme YAML file."""
|
|
return yaml.safe_load(path.read_text(encoding='utf-8'))
|
|
|
|
|
|
class TestThemeEnrichmentPipeline:
|
|
"""Tests for ThemeEnrichmentPipeline class."""
|
|
|
|
def test_init(self, temp_root: Path):
|
|
"""Test pipeline initialization."""
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root, min_examples=5)
|
|
|
|
assert pipeline.root == temp_root
|
|
assert pipeline.min_examples == 5
|
|
assert pipeline.catalog_dir == temp_root / 'config' / 'themes' / 'catalog'
|
|
assert len(pipeline.themes) == 0
|
|
|
|
def test_load_themes_empty_dir(self, temp_root: Path):
|
|
"""Test loading themes from empty directory."""
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
|
|
assert len(pipeline.themes) == 0
|
|
assert pipeline.stats.total_themes == 0
|
|
|
|
def test_load_themes_with_valid_files(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test loading valid theme files."""
|
|
write_theme(temp_catalog_dir, 'landfall.yml', {
|
|
'display_name': 'Landfall',
|
|
'synergies': ['Ramp', 'Tokens'],
|
|
'example_commanders': []
|
|
})
|
|
write_theme(temp_catalog_dir, 'reanimate.yml', {
|
|
'display_name': 'Reanimate',
|
|
'synergies': ['Graveyard', 'Mill'],
|
|
'example_commanders': ['Meren of Clan Nel Toth']
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
|
|
assert len(pipeline.themes) == 2
|
|
assert pipeline.stats.total_themes == 2
|
|
|
|
def test_autofill_placeholders_empty_examples(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test autofill adds placeholders to themes with no examples."""
|
|
write_theme(temp_catalog_dir, 'tokens.yml', {
|
|
'display_name': 'Tokens Matter',
|
|
'synergies': ['Sacrifice', 'Aristocrats'],
|
|
'example_commanders': []
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
pipeline.autofill_placeholders()
|
|
|
|
assert pipeline.stats.autofilled == 1
|
|
theme = list(pipeline.themes.values())[0]
|
|
assert theme.modified
|
|
assert 'Tokens Matter Anchor' in theme.data['example_commanders']
|
|
assert 'Sacrifice Anchor' in theme.data['example_commanders']
|
|
assert 'Aristocrats Anchor' in theme.data['example_commanders']
|
|
assert theme.data.get('editorial_quality') == 'draft'
|
|
|
|
def test_autofill_skips_themes_with_examples(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test autofill skips themes that already have examples."""
|
|
write_theme(temp_catalog_dir, 'landfall.yml', {
|
|
'display_name': 'Landfall',
|
|
'synergies': ['Ramp'],
|
|
'example_commanders': ['Tatyova, Benthic Druid']
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
pipeline.autofill_placeholders()
|
|
|
|
assert pipeline.stats.autofilled == 0
|
|
theme = list(pipeline.themes.values())[0]
|
|
assert not theme.modified
|
|
|
|
def test_pad_examples_to_minimum(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test padding adds placeholders to reach minimum threshold."""
|
|
write_theme(temp_catalog_dir, 'ramp.yml', {
|
|
'display_name': 'Ramp',
|
|
'synergies': ['Landfall', 'BigSpells', 'Hydras'],
|
|
'example_commanders': ['Ramp Anchor', 'Landfall Anchor']
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root, min_examples=5)
|
|
pipeline.load_all_themes()
|
|
pipeline.pad_examples()
|
|
|
|
assert pipeline.stats.padded == 1
|
|
theme = list(pipeline.themes.values())[0]
|
|
assert theme.modified
|
|
assert len(theme.data['example_commanders']) == 5
|
|
# Should add synergies first (3rd synergy), then letter suffixes
|
|
assert 'Hydras Anchor' in theme.data['example_commanders']
|
|
# Should also have letter suffixes for remaining slots
|
|
assert any('Anchor B' in cmd or 'Anchor C' in cmd for cmd in theme.data['example_commanders'])
|
|
|
|
def test_pad_skips_mixed_real_and_placeholder(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test padding skips lists with both real and placeholder examples."""
|
|
write_theme(temp_catalog_dir, 'tokens.yml', {
|
|
'display_name': 'Tokens',
|
|
'synergies': ['Sacrifice'],
|
|
'example_commanders': ['Krenko, Mob Boss', 'Tokens Anchor']
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root, min_examples=5)
|
|
pipeline.load_all_themes()
|
|
pipeline.pad_examples()
|
|
|
|
assert pipeline.stats.padded == 0
|
|
theme = list(pipeline.themes.values())[0]
|
|
assert not theme.modified
|
|
|
|
def test_cleanup_removes_placeholders_when_real_present(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test cleanup removes placeholders when real examples are present.
|
|
|
|
Note: cleanup only removes entries ending with ' Anchor' (no suffix).
|
|
Purge step removes entries with ' Anchor' or ' Anchor X' pattern.
|
|
"""
|
|
write_theme(temp_catalog_dir, 'lifegain.yml', {
|
|
'display_name': 'Lifegain',
|
|
'synergies': [],
|
|
'example_commanders': [
|
|
'Oloro, Ageless Ascetic',
|
|
'Lifegain Anchor', # Will be removed
|
|
'Trelasarra, Moon Dancer',
|
|
]
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
pipeline.cleanup_placeholders()
|
|
|
|
assert pipeline.stats.cleaned == 1
|
|
theme = list(pipeline.themes.values())[0]
|
|
assert theme.modified
|
|
assert len(theme.data['example_commanders']) == 2
|
|
assert 'Oloro, Ageless Ascetic' in theme.data['example_commanders']
|
|
assert 'Trelasarra, Moon Dancer' in theme.data['example_commanders']
|
|
assert 'Lifegain Anchor' not in theme.data['example_commanders']
|
|
|
|
def test_purge_removes_all_anchors(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test purge removes all anchor placeholders (even if no real examples)."""
|
|
write_theme(temp_catalog_dir, 'counters.yml', {
|
|
'display_name': 'Counters',
|
|
'synergies': [],
|
|
'example_commanders': [
|
|
'Counters Anchor',
|
|
'Counters Anchor B',
|
|
'Counters Anchor C'
|
|
]
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
pipeline.purge_anchors()
|
|
|
|
assert pipeline.stats.purged == 1
|
|
theme = list(pipeline.themes.values())[0]
|
|
assert theme.modified
|
|
assert theme.data['example_commanders'] == []
|
|
|
|
def test_augment_from_catalog(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test augmentation adds missing fields from catalog."""
|
|
# Create catalog JSON
|
|
catalog_json = temp_root / 'config' / 'themes' / 'theme_list.json'
|
|
catalog_data = {
|
|
'themes': [
|
|
{
|
|
'theme': 'Landfall',
|
|
'description': 'Triggers from lands entering',
|
|
'popularity_bucket': 'common',
|
|
'popularity_hint': 'Very popular',
|
|
'deck_archetype': 'Lands'
|
|
}
|
|
]
|
|
}
|
|
import json
|
|
catalog_json.write_text(json.dumps(catalog_data), encoding='utf-8')
|
|
|
|
write_theme(temp_catalog_dir, 'landfall.yml', {
|
|
'display_name': 'Landfall',
|
|
'synergies': ['Ramp'],
|
|
'example_commanders': ['Tatyova, Benthic Druid']
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
pipeline.augment_from_catalog()
|
|
|
|
assert pipeline.stats.augmented == 1
|
|
theme = list(pipeline.themes.values())[0]
|
|
assert theme.modified
|
|
assert theme.data['description'] == 'Triggers from lands entering'
|
|
assert theme.data['popularity_bucket'] == 'common'
|
|
assert theme.data['popularity_hint'] == 'Very popular'
|
|
assert theme.data['deck_archetype'] == 'Lands'
|
|
|
|
def test_validate_min_examples_warning(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test validation warns about insufficient examples."""
|
|
write_theme(temp_catalog_dir, 'ramp.yml', {
|
|
'display_name': 'Ramp',
|
|
'synergies': [],
|
|
'example_commanders': ['Ramp Commander']
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root, min_examples=5)
|
|
pipeline.load_all_themes()
|
|
pipeline.validate(enforce_min=False)
|
|
|
|
assert pipeline.stats.lint_warnings > 0
|
|
assert pipeline.stats.lint_errors == 0
|
|
|
|
def test_validate_min_examples_error(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test validation errors on insufficient examples when enforced."""
|
|
write_theme(temp_catalog_dir, 'ramp.yml', {
|
|
'display_name': 'Ramp',
|
|
'synergies': [],
|
|
'example_commanders': ['Ramp Commander']
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root, min_examples=5)
|
|
pipeline.load_all_themes()
|
|
pipeline.validate(enforce_min=True)
|
|
|
|
assert pipeline.stats.lint_errors > 0
|
|
|
|
def test_write_themes_dry_run(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test dry run doesn't write files."""
|
|
theme_path = write_theme(temp_catalog_dir, 'tokens.yml', {
|
|
'display_name': 'Tokens',
|
|
'synergies': [],
|
|
'example_commanders': []
|
|
})
|
|
|
|
original_content = theme_path.read_text(encoding='utf-8')
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
pipeline.autofill_placeholders()
|
|
# Don't call write_all_themes()
|
|
|
|
# File should be unchanged
|
|
assert theme_path.read_text(encoding='utf-8') == original_content
|
|
|
|
def test_write_themes_saves_changes(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test write_all_themes saves modified files."""
|
|
theme_path = write_theme(temp_catalog_dir, 'tokens.yml', {
|
|
'display_name': 'Tokens',
|
|
'synergies': ['Sacrifice'],
|
|
'example_commanders': []
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root)
|
|
pipeline.load_all_themes()
|
|
pipeline.autofill_placeholders()
|
|
pipeline.write_all_themes()
|
|
|
|
# File should be updated
|
|
updated_data = read_theme(theme_path)
|
|
assert len(updated_data['example_commanders']) > 0
|
|
assert 'Tokens Anchor' in updated_data['example_commanders']
|
|
|
|
def test_run_all_full_pipeline(self, temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test running the complete enrichment pipeline."""
|
|
write_theme(temp_catalog_dir, 'landfall.yml', {
|
|
'display_name': 'Landfall',
|
|
'synergies': ['Ramp', 'Lands'],
|
|
'example_commanders': []
|
|
})
|
|
write_theme(temp_catalog_dir, 'reanimate.yml', {
|
|
'display_name': 'Reanimate',
|
|
'synergies': ['Graveyard'],
|
|
'example_commanders': []
|
|
})
|
|
|
|
pipeline = ThemeEnrichmentPipeline(root=temp_root, min_examples=5)
|
|
stats = pipeline.run_all(write=True, enforce_min=False, strict_lint=False)
|
|
|
|
assert stats.total_themes == 2
|
|
assert stats.autofilled >= 2
|
|
assert stats.padded >= 2
|
|
|
|
# Verify files were updated
|
|
landfall_data = read_theme(temp_catalog_dir / 'landfall.yml')
|
|
assert len(landfall_data['example_commanders']) >= 5
|
|
assert landfall_data.get('editorial_quality') == 'draft'
|
|
|
|
|
|
def test_run_enrichment_pipeline_convenience_function(temp_root: Path, temp_catalog_dir: Path):
|
|
"""Test the convenience function wrapper."""
|
|
write_theme(temp_catalog_dir, 'tokens.yml', {
|
|
'display_name': 'Tokens',
|
|
'synergies': ['Sacrifice'],
|
|
'example_commanders': []
|
|
})
|
|
|
|
stats = run_enrichment_pipeline(
|
|
root=temp_root,
|
|
min_examples=3,
|
|
write=True,
|
|
enforce_min=False,
|
|
strict=False,
|
|
progress_callback=None,
|
|
)
|
|
|
|
assert isinstance(stats, EnrichmentStats)
|
|
assert stats.total_themes == 1
|
|
assert stats.autofilled >= 1
|
|
|
|
# Verify file was written
|
|
tokens_data = read_theme(temp_catalog_dir / 'tokens.yml')
|
|
assert len(tokens_data['example_commanders']) >= 3
|