import json import os import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] BUILD_SCRIPT = ROOT / 'code' / 'scripts' / 'build_theme_catalog.py' OUTPUT_JSON = ROOT / 'config' / 'themes' / 'theme_list.json' def run_builder(): env = os.environ.copy() env['THEME_CATALOG_MODE'] = 'merge' result = subprocess.run([sys.executable, str(BUILD_SCRIPT), '--limit', '0'], capture_output=True, text=True, env=env) assert result.returncode == 0, f"build_theme_catalog failed: {result.stderr or result.stdout}" assert OUTPUT_JSON.exists(), "Expected theme_list.json to exist after merge build" def load_catalog(): data = json.loads(OUTPUT_JSON.read_text(encoding='utf-8')) themes = {t['theme']: t for t in data.get('themes', []) if isinstance(t, dict) and 'theme' in t} return data, themes def test_phase_b_merge_metadata_info_and_precedence(): run_builder() data, themes = load_catalog() # metadata_info block required (legacy 'provenance' accepted transiently) meta = data.get('metadata_info') or data.get('provenance') assert isinstance(meta, dict), 'metadata_info block missing' assert meta.get('mode') == 'merge', 'metadata_info mode should be merge' assert 'generated_at' in meta, 'generated_at missing in metadata_info' assert 'curated_yaml_files' in meta, 'curated_yaml_files missing in metadata_info' # Sample anchors to verify curated/enforced precedence not truncated under cap # Choose +1/+1 Counters (curated + enforced) and Reanimate (curated + enforced) for anchor in ['+1/+1 Counters', 'Reanimate']: assert anchor in themes, f'Missing anchor theme {anchor}' syn = themes[anchor]['synergies'] # Ensure enforced present if anchor == '+1/+1 Counters': assert 'Proliferate' in syn and 'Counters Matter' in syn, 'Counters enforced synergies missing' if anchor == 'Reanimate': assert 'Graveyard Matters' in syn, 'Reanimate enforced synergy missing' # If synergy list length equals cap, ensure enforced not last-only list while curated missing # (Simplistic check: curated expectation contains at least one of baseline curated anchors) if anchor == 'Reanimate': # baseline curated includes Enter the Battlefield assert 'Enter the Battlefield' in syn, 'Curated synergy lost due to capping' # Ensure cap respected (soft exceed allowed only if curated+enforced exceed cap) cap = (data.get('metadata_info') or {}).get('synergy_cap') or 0 if cap: for t, entry in list(themes.items())[:50]: # sample first 50 for speed if len(entry['synergies']) > cap: # Validate that over-cap entries contain all enforced + curated combined beyond cap (soft exceed case) # We cannot reconstruct curated exactly here without re-running logic; accept soft exceed. continue assert len(entry['synergies']) <= cap, f"Synergy cap exceeded for {t}: {entry['synergies']}"