#!/usr/bin/env python3 # /// script # requires-python = ">=3.10" # /// """Tests for validate-module.py""" import json import subprocess import sys import tempfile from pathlib import Path SCRIPT = Path(__file__).resolve().parent.parent / "validate-module.py" CSV_HEADER = "module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs\n" def create_module(tmp: Path, skills: list[str] | None = None, csv_rows: str = "", yaml_content: str = "", setup_name: str = "bmad-tst-setup") -> Path: """Create a minimal module structure for testing.""" module_dir = tmp / "module" module_dir.mkdir() # Setup skill setup = module_dir / setup_name setup.mkdir() (setup / "SKILL.md").write_text("---\nname: " + setup_name + "\n---\n# Setup\n") (setup / "assets").mkdir() (setup / "assets" / "module.yaml").write_text( yaml_content or 'code: tst\nname: "Test Module"\ndescription: "A test module"\n' ) (setup / "assets" / "module-help.csv").write_text(CSV_HEADER + csv_rows) # Other skills for skill in (skills or []): skill_dir = module_dir / skill skill_dir.mkdir() (skill_dir / "SKILL.md").write_text(f"---\nname: {skill}\n---\n# {skill}\n") return module_dir def run_validate(module_dir: Path) -> tuple[int, dict]: """Run the validation script and return (exit_code, parsed_json).""" result = subprocess.run( [sys.executable, str(SCRIPT), str(module_dir)], capture_output=True, text=True, ) try: data = json.loads(result.stdout) except json.JSONDecodeError: data = {"raw_stdout": result.stdout, "raw_stderr": result.stderr} return result.returncode, data def test_valid_module(): """A well-formed module should pass.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) csv_rows = 'Test Module,bmad-tst-foo,Do Foo,DF,Does the foo thing,run,,anytime,,,false,output_folder,report\n' module_dir = create_module(tmp, skills=["bmad-tst-foo"], csv_rows=csv_rows) code, data = run_validate(module_dir) assert code == 0, f"Expected pass: {data}" assert data["status"] == "pass" assert data["summary"]["total_findings"] == 0 def test_missing_setup_skill(): """Module with no setup skill should fail critically.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) module_dir = tmp / "module" module_dir.mkdir() skill = module_dir / "bmad-tst-foo" skill.mkdir() (skill / "SKILL.md").write_text("---\nname: bmad-tst-foo\n---\n") code, data = run_validate(module_dir) assert code == 1 assert any(f["category"] == "structure" for f in data["findings"]) def test_missing_csv_entry(): """Skill without a CSV entry should be flagged.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) module_dir = create_module(tmp, skills=["bmad-tst-foo", "bmad-tst-bar"], csv_rows='Test Module,bmad-tst-foo,Do Foo,DF,Does foo,run,,anytime,,,false,output_folder,report\n') code, data = run_validate(module_dir) assert code == 1 missing = [f for f in data["findings"] if f["category"] == "missing-entry"] assert len(missing) == 1 assert "bmad-tst-bar" in missing[0]["message"] def test_orphan_csv_entry(): """CSV entry for nonexistent skill should be flagged.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) csv_rows = 'Test Module,bmad-tst-ghost,Ghost,GH,Does not exist,run,,anytime,,,false,output_folder,report\n' module_dir = create_module(tmp, skills=[], csv_rows=csv_rows) code, data = run_validate(module_dir) orphans = [f for f in data["findings"] if f["category"] == "orphan-entry"] assert len(orphans) == 1 assert "bmad-tst-ghost" in orphans[0]["message"] def test_duplicate_menu_codes(): """Duplicate menu codes should be flagged.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) csv_rows = ( 'Test Module,bmad-tst-foo,Do Foo,DF,Does foo,run,,anytime,,,false,output_folder,report\n' 'Test Module,bmad-tst-foo,Also Foo,DF,Also does foo,other,,anytime,,,false,output_folder,report\n' ) module_dir = create_module(tmp, skills=["bmad-tst-foo"], csv_rows=csv_rows) code, data = run_validate(module_dir) dupes = [f for f in data["findings"] if f["category"] == "duplicate-menu-code"] assert len(dupes) == 1 assert "DF" in dupes[0]["message"] def test_invalid_before_after_ref(): """Before/after references to nonexistent capabilities should be flagged.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) csv_rows = 'Test Module,bmad-tst-foo,Do Foo,DF,Does foo,run,,anytime,bmad-tst-ghost:phantom,,false,output_folder,report\n' module_dir = create_module(tmp, skills=["bmad-tst-foo"], csv_rows=csv_rows) code, data = run_validate(module_dir) refs = [f for f in data["findings"] if f["category"] == "invalid-ref"] assert len(refs) == 1 assert "bmad-tst-ghost:phantom" in refs[0]["message"] def test_missing_yaml_fields(): """module.yaml with missing required fields should be flagged.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) csv_rows = 'Test Module,bmad-tst-foo,Do Foo,DF,Does foo,run,,anytime,,,false,output_folder,report\n' module_dir = create_module(tmp, skills=["bmad-tst-foo"], csv_rows=csv_rows, yaml_content='code: tst\n') code, data = run_validate(module_dir) yaml_findings = [f for f in data["findings"] if f["category"] == "yaml"] assert len(yaml_findings) >= 1 # at least name or description missing def test_empty_csv(): """CSV with header but no rows should be flagged.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) module_dir = create_module(tmp, skills=["bmad-tst-foo"], csv_rows="") code, data = run_validate(module_dir) assert code == 1 empty = [f for f in data["findings"] if f["category"] == "csv-empty"] assert len(empty) == 1 def test_nonexistent_directory(): """Nonexistent path should return error.""" result = subprocess.run( [sys.executable, str(SCRIPT), "/nonexistent/path"], capture_output=True, text=True, ) assert result.returncode == 2 data = json.loads(result.stdout) assert data["status"] == "error" if __name__ == "__main__": tests = [ test_valid_module, test_missing_setup_skill, test_missing_csv_entry, test_orphan_csv_entry, test_duplicate_menu_codes, test_invalid_before_after_ref, test_missing_yaml_fields, test_empty_csv, test_nonexistent_directory, ] passed = 0 failed = 0 for test in tests: try: test() print(f" PASS: {test.__name__}") passed += 1 except AssertionError as e: print(f" FAIL: {test.__name__}: {e}") failed += 1 except Exception as e: print(f" ERROR: {test.__name__}: {e}") failed += 1 print(f"\n{passed} passed, {failed} failed") sys.exit(1 if failed else 0)