BMAD-METHOD/src/scripts/tests/test_resolve_customization_...

154 lines
6.6 KiB
Python

import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).resolve().parents[1] / "resolve_customization.py"
def make_skill(parent: Path, name: str, defaults_toml: str, module: str | None = None) -> Path:
"""Create a fake skill dir, optionally under a `{module}-skills/` ancestor."""
if module:
base = parent / f"{module}-skills"
base.mkdir(parents=True, exist_ok=True)
else:
base = parent
skill_dir = base / name
skill_dir.mkdir()
(skill_dir / "customize.toml").write_text(defaults_toml, encoding="utf-8")
return skill_dir
def run(skill_dir: Path, key=None, env_overrides=None):
env = os.environ.copy()
env.setdefault("BMAD_HOME", "/nonexistent-bmad-home-default")
if env_overrides:
env.update(env_overrides)
args = [sys.executable, str(SCRIPT), "--skill", str(skill_dir)]
if key:
args.extend(["--key", key])
result = subprocess.run(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, check=False
)
stderr = result.stderr.decode("utf-8", errors="replace")
if result.returncode != 0:
raise AssertionError(f"resolve_customization failed ({result.returncode}): {stderr}")
return json.loads(result.stdout.decode("utf-8"))
class ResolveCustomizationCascadeTests(unittest.TestCase):
def test_defaults_pass_through_when_no_overrides(self):
with tempfile.TemporaryDirectory() as t:
skill = make_skill(Path(t), "bmad-prd", '[knobs]\ndepth = "low"\n')
data = run(skill)
self.assertEqual(data["knobs"]["depth"], "low")
def test_skills_section_in_global_customize_overrides_default(self):
with tempfile.TemporaryDirectory() as t, tempfile.TemporaryDirectory() as global_dir:
skill = make_skill(Path(t), "bmad-prd", '[knobs]\ndepth = "low"\n')
(Path(global_dir) / "customize.toml").write_text(
'[skills.bmad-prd.knobs]\ndepth = "high"\n',
encoding="utf-8",
)
data = run(skill, env_overrides={"BMAD_HOME": global_dir})
self.assertEqual(data["knobs"]["depth"], "high")
def test_config_toml_is_not_consulted_for_skills_section(self):
# [skills.X] in config.toml must NOT be honored — only customize.toml is.
with tempfile.TemporaryDirectory() as t, tempfile.TemporaryDirectory() as g:
skill = make_skill(Path(t), "bmad-prd", '[knobs]\ndepth = "low"\n')
(Path(g) / "config.toml").write_text(
'[skills.bmad-prd.knobs]\ndepth = "should-be-ignored"\n',
encoding="utf-8",
)
data = run(skill, env_overrides={"BMAD_HOME": g})
self.assertEqual(data["knobs"]["depth"], "low")
def test_specificity_within_layer(self):
# Within ONE customize layer: exact skill name beats wildcard.
with tempfile.TemporaryDirectory() as t, tempfile.TemporaryDirectory() as g:
skill = make_skill(Path(t), "bmad-prd", '[knobs]\ndepth = "low"\n')
(Path(g) / "customize.toml").write_text(
"[skills.\"*\".knobs]\ndepth = \"catchall\"\n"
"[skills.bmad-prd.knobs]\ndepth = \"exact\"\n",
encoding="utf-8",
)
data = run(skill, env_overrides={"BMAD_HOME": g})
self.assertEqual(data["knobs"]["depth"], "exact")
def test_layer_precedence_overrides_specificity(self):
# Across layers: higher layer wins even with less-specific pattern.
# Project custom user (highest customize layer) uses '*';
# global (lowest) uses exact — project custom should still win.
with tempfile.TemporaryDirectory() as t, tempfile.TemporaryDirectory() as g:
project = Path(t) / "project"
bmad = project / "_bmad"
(bmad / "custom").mkdir(parents=True)
skill = make_skill(project, "bmad-prd", '[knobs]\ndepth = "low"\n')
(Path(g) / "customize.toml").write_text(
"[skills.bmad-prd.knobs]\ndepth = \"global-exact\"\n",
encoding="utf-8",
)
(bmad / "custom" / "customize.user.toml").write_text(
"[skills.\"*\".knobs]\ndepth = \"custom-wildcard\"\n",
encoding="utf-8",
)
data = run(skill, env_overrides={"BMAD_HOME": g})
self.assertEqual(data["knobs"]["depth"], "custom-wildcard")
def test_per_skill_custom_file_beats_customize_section(self):
# Per-skill _bmad/custom/{skill}.user.toml is still the highest tier.
with tempfile.TemporaryDirectory() as t:
project = Path(t) / "project"
bmad = project / "_bmad"
(bmad / "custom").mkdir(parents=True)
skill = make_skill(project, "bmad-prd", '[knobs]\ndepth = "low"\n')
(bmad / "custom" / "customize.user.toml").write_text(
"[skills.bmad-prd.knobs]\ndepth = \"via-section\"\n",
encoding="utf-8",
)
(bmad / "custom" / "bmad-prd.user.toml").write_text(
"[knobs]\ndepth = \"via-per-skill-file\"\n",
encoding="utf-8",
)
data = run(skill)
self.assertEqual(data["knobs"]["depth"], "via-per-skill-file")
def test_qualified_module_pattern_matches(self):
# Skill under bmm-skills/ → qualified name 'bmm/bmad-prd'.
# Pattern '[skills."bmm/*"]' should match.
with tempfile.TemporaryDirectory() as t, tempfile.TemporaryDirectory() as g:
skill = make_skill(
Path(t), "bmad-prd", '[knobs]\ndepth = "low"\n', module="bmm"
)
(Path(g) / "customize.toml").write_text(
"[skills.\"bmm/*\".knobs]\ndepth = \"all-bmm\"\n",
encoding="utf-8",
)
data = run(skill, env_overrides={"BMAD_HOME": g})
self.assertEqual(data["knobs"]["depth"], "all-bmm")
def test_qualified_exact_beats_module_wildcard(self):
# Within one layer: 'bmm/bmad-prd' (exact) beats 'bmm/*' (wildcard).
with tempfile.TemporaryDirectory() as t, tempfile.TemporaryDirectory() as g:
skill = make_skill(
Path(t), "bmad-prd", '[knobs]\ndepth = "low"\n', module="bmm"
)
(Path(g) / "customize.toml").write_text(
"[skills.\"bmm/*\".knobs]\ndepth = \"module-wide\"\n"
"[skills.\"bmm/bmad-prd\".knobs]\ndepth = \"pinned\"\n",
encoding="utf-8",
)
data = run(skill, env_overrides={"BMAD_HOME": g})
self.assertEqual(data["knobs"]["depth"], "pinned")
if __name__ == "__main__":
unittest.main()