mtg_python_deckbuilder/code/tests/test_theme_linter.py
mwisnowski 1ebc2fcb3c
Some checks are pending
CI / build (push) Waiting to run
Editorial Lint / lint-editorial (push) Waiting to run
feat: add theme editorial quality system with scoring, linting, and comprehensive documentation (#54)
2026-03-19 10:06:29 -07:00

287 lines
10 KiB
Python

"""Tests for M4 linter functionality in validate_theme_catalog.py"""
import pytest
import sys
from pathlib import Path
from typing import Dict, List
from type_definitions_theme_catalog import ThemeYAMLFile, DescriptionSource
from web.services.theme_editorial_service import ThemeEditorialService
from web.services.theme_catalog_loader import load_index
class TestLinterDuplicationChecks:
"""Test M4 linter duplication ratio checks"""
def test_high_duplication_flagged(self):
"""Themes with high duplication ratio should be flagged"""
service = ThemeEditorialService()
# Get actual total themes from catalog
index = load_index()
total_themes = len(index.slug_to_entry)
# Mock global frequency: Sol Ring in 60% of themes, Lightning Greaves in 50%
# Use actual total to get realistic frequencies
global_card_freq = {
"Sol Ring": int(total_themes * 0.6),
"Lightning Greaves": int(total_themes * 0.5),
"Unique Card A": 5,
"Unique Card B": 3
}
# Theme with mostly generic cards (2/4 = 50% are generic)
example_cards = ["Sol Ring", "Lightning Greaves", "Unique Card A", "Unique Card B"]
dup_ratio = service.calculate_duplication_ratio(
example_cards=example_cards,
global_card_freq=global_card_freq,
duplication_threshold=0.4 # >40% = duplicated
)
# Should flag: 2 out of 4 cards appear in >40% of themes
assert dup_ratio == 0.5 # 50% duplication
def test_low_duplication_not_flagged(self):
"""Themes with unique cards should not be flagged"""
service = ThemeEditorialService()
# All unique cards
global_card_freq = {
"Unique Card A": 5,
"Unique Card B": 3,
"Unique Card C": 8,
"Unique Card D": 2
}
example_cards = ["Unique Card A", "Unique Card B", "Unique Card C", "Unique Card D"]
dup_ratio = service.calculate_duplication_ratio(
example_cards=example_cards,
global_card_freq=global_card_freq,
duplication_threshold=0.4
)
assert dup_ratio == 0.0 # No duplication
def test_empty_cards_no_duplication(self):
"""Empty example cards should return 0.0 duplication"""
service = ThemeEditorialService()
global_card_freq = {"Sol Ring": 60}
dup_ratio = service.calculate_duplication_ratio(
example_cards=[],
global_card_freq=global_card_freq,
duplication_threshold=0.4
)
assert dup_ratio == 0.0
class TestLinterQualityScoring:
"""Test M4 linter quality score checks"""
def test_low_quality_score_flagged(self):
"""Themes with low quality scores should be flagged"""
from type_definitions_theme_catalog import ThemeEntry
service = ThemeEditorialService()
# Low quality theme: few cards, generic description, no uniqueness
theme_entry = ThemeEntry(
theme="Test Theme",
example_cards=["Sol Ring", "Command Tower"], # Only 2 cards
description_source="generic"
)
global_card_freq = {
"Sol Ring": 80, # Very common
"Command Tower": 75 # Very common
}
tier, score = service.calculate_enhanced_quality_score(
theme_entry=theme_entry,
global_card_freq=global_card_freq
)
assert tier in ["Poor", "Fair"]
assert score < 0.5 # Below typical threshold
def test_high_quality_score_not_flagged(self):
"""Themes with high quality scores should not be flagged"""
from type_definitions_theme_catalog import ThemeEntry
service = ThemeEditorialService()
# High quality theme: many unique cards, manual description
theme_entry = ThemeEntry(
theme="Test Theme",
example_cards=[f"Unique Card {i}" for i in range(10)], # 10 unique cards
description_source="manual"
)
global_card_freq = {f"Unique Card {i}": 2 for i in range(10)} # All rare
tier, score = service.calculate_enhanced_quality_score(
theme_entry=theme_entry,
global_card_freq=global_card_freq
)
assert tier in ["Good", "Excellent"]
assert score >= 0.6 # Above typical threshold
class TestLinterSuggestions:
"""Test M4 linter suggestion generation"""
def test_suggestions_for_few_cards(self):
"""Should suggest adding more cards when count is low"""
example_cards = ["Card A", "Card B", "Card C"] # Only 3 cards
suggestions = []
if len(example_cards) < 5:
suggestions.append("Add more example cards (target: 8+)")
assert len(suggestions) == 1
assert "Add more example cards" in suggestions[0]
def test_suggestions_for_generic_description(self):
"""Should suggest upgrading description when generic"""
description_source = "generic"
suggestions = []
if description_source == "generic":
suggestions.append("Upgrade to manual or rule-based description")
assert len(suggestions) == 1
assert "Upgrade to manual or rule-based" in suggestions[0]
def test_suggestions_for_generic_cards(self):
"""Should suggest replacing generic cards when duplication high"""
dup_ratio = 0.6 # 60% duplication
suggestions = []
if dup_ratio > 0.4:
suggestions.append("Replace generic staples with unique cards")
assert len(suggestions) == 1
assert "Replace generic staples" in suggestions[0]
def test_multiple_suggestions_combined(self):
"""Should provide multiple suggestions when multiple issues exist"""
example_cards = ["Card A", "Card B"] # Few cards
description_source = "generic"
dup_ratio = 0.5 # High duplication
suggestions = []
if len(example_cards) < 5:
suggestions.append("Add more example cards (target: 8+)")
if description_source == "generic":
suggestions.append("Upgrade to manual or rule-based description")
if dup_ratio > 0.4:
suggestions.append("Replace generic staples with unique cards")
assert len(suggestions) == 3
assert "Add more example cards" in suggestions[0]
assert "Upgrade to manual or rule-based" in suggestions[1]
assert "Replace generic staples" in suggestions[2]
class TestLinterThresholds:
"""Test M4 linter configurable thresholds"""
def test_duplication_threshold_configurable(self):
"""Duplication threshold should be configurable"""
service = ThemeEditorialService()
# Get actual total themes from catalog
index = load_index()
total_themes = len(index.slug_to_entry)
# Sol Ring at 45% frequency
global_card_freq = {
"Sol Ring": int(total_themes * 0.45),
"Unique Card": 5
}
example_cards = ["Sol Ring", "Unique Card"]
# With threshold 0.5 (50%), Sol Ring not flagged
dup_ratio_high = service.calculate_duplication_ratio(
example_cards=example_cards,
global_card_freq=global_card_freq,
duplication_threshold=0.5
)
assert dup_ratio_high == 0.0 # 45% < 50%
# With threshold 0.4 (40%), Sol Ring IS flagged
dup_ratio_low = service.calculate_duplication_ratio(
example_cards=example_cards,
global_card_freq=global_card_freq,
duplication_threshold=0.4
)
assert dup_ratio_low == 0.5 # 45% > 40%, so 1/2 cards flagged
def test_quality_threshold_configurable(self):
"""Quality threshold determines what gets flagged"""
# Threshold 0.3 would flag scores < 0.3
score_fair = 0.45
assert score_fair < 0.5 # Would be flagged with threshold 0.5
assert score_fair >= 0.3 # Would NOT be flagged with threshold 0.3
class TestLinterIntegration:
"""Integration tests for linter with ThemeYAMLFile validation"""
def test_yaml_file_to_theme_entry_conversion(self):
"""Should correctly convert ThemeYAMLFile to ThemeEntry for linting"""
from type_definitions_theme_catalog import ThemeEntry
# Simulate a ThemeYAMLFile object
yaml_data = {
"id": "test-theme",
"display_name": "Test Theme",
"synergies": ["Synergy A", "Synergy B"],
"example_cards": ["Card A", "Card B", "Card C"],
"description_source": "manual",
"description": "A test theme for linting"
}
yaml_file = ThemeYAMLFile(**yaml_data)
# Convert to ThemeEntry for linting
theme_entry = ThemeEntry(
theme=yaml_file.display_name,
example_cards=yaml_file.example_cards,
description_source=yaml_file.description_source
)
assert theme_entry.theme == "Test Theme"
assert len(theme_entry.example_cards) == 3
assert theme_entry.description_source == "manual"
def test_linter_handles_missing_optional_fields(self):
"""Linter should handle themes with missing optional fields gracefully"""
from type_definitions_theme_catalog import ThemeEntry
# Theme with minimal required fields
theme_entry = ThemeEntry(
theme="Minimal Theme",
example_cards=["Card A"],
description_source=None # Missing description_source
)
service = ThemeEditorialService()
# Should not crash
tier, score = service.calculate_enhanced_quality_score(
theme_entry=theme_entry,
global_card_freq={"Card A": 1}
)
assert isinstance(tier, str)
assert 0.0 <= score <= 1.0
if __name__ == '__main__':
pytest.main([__file__, '-v'])