| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- #!/usr/bin/env python3
- # /// script
- # requires-python = ">=3.10"
- # ///
- """Tests for scaffold-standalone-module.py"""
- import json
- import subprocess
- import sys
- import tempfile
- from pathlib import Path
- SCRIPT = Path(__file__).resolve().parent.parent / "scaffold-standalone-module.py"
- def make_skill_dir(tmp: Path, name: str = "my-skill") -> Path:
- """Create a minimal skill directory with SKILL.md and assets/module.yaml."""
- skill_dir = tmp / name
- skill_dir.mkdir(parents=True, exist_ok=True)
- (skill_dir / "SKILL.md").write_text("---\nname: my-skill\ndescription: A test skill\n---\n# My Skill\n")
- assets = skill_dir / "assets"
- assets.mkdir(exist_ok=True)
- (assets / "module.yaml").write_text(
- 'code: tst\nname: "Test Module"\ndescription: "A test module"\nmodule_version: 1.0.0\n'
- )
- (assets / "module-help.csv").write_text(
- "module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs\n"
- "Test Module,my-skill,Do Thing,DT,Does the thing,run,,anytime,,,false,output_folder,artifact\n"
- )
- return skill_dir
- def run_scaffold(skill_dir: Path, **kwargs) -> tuple[int, dict]:
- """Run the standalone scaffold script and return (exit_code, parsed_json)."""
- cmd = [
- sys.executable,
- str(SCRIPT),
- "--skill-dir", str(skill_dir),
- "--module-code", kwargs.get("module_code", "tst"),
- "--module-name", kwargs.get("module_name", "Test Module"),
- ]
- if "marketplace_dir" in kwargs:
- cmd.extend(["--marketplace-dir", str(kwargs["marketplace_dir"])])
- if kwargs.get("verbose"):
- cmd.append("--verbose")
- result = subprocess.run(cmd, 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_basic_scaffold():
- """Test that scaffolding copies all expected template files."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- skill_dir = make_skill_dir(tmp)
- code, data = run_scaffold(skill_dir)
- assert code == 0, f"Script failed: {data}"
- assert data["status"] == "success"
- assert data["module_code"] == "tst"
- # module-setup.md placed alongside module.yaml in assets/
- assert (skill_dir / "assets" / "module-setup.md").is_file()
- # merge scripts placed in scripts/
- assert (skill_dir / "scripts" / "merge-config.py").is_file()
- assert (skill_dir / "scripts" / "merge-help-csv.py").is_file()
- # marketplace.json at parent level
- assert (tmp / ".claude-plugin" / "marketplace.json").is_file()
- def test_marketplace_json_content():
- """Test that marketplace.json contains correct module metadata."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- skill_dir = make_skill_dir(tmp, name="bmad-exc-tools")
- code, data = run_scaffold(
- skill_dir, module_code="exc", module_name="Excalidraw Tools"
- )
- assert code == 0
- marketplace = json.loads(
- (tmp / ".claude-plugin" / "marketplace.json").read_text()
- )
- assert marketplace["name"] == "bmad-exc"
- plugin = marketplace["plugins"][0]
- assert plugin["name"] == "bmad-exc"
- assert plugin["skills"] == ["./bmad-exc-tools"]
- assert plugin["description"] == "A test module"
- assert plugin["version"] == "1.0.0"
- def test_does_not_overwrite_existing_scripts():
- """Test that existing scripts are skipped with a warning."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- skill_dir = make_skill_dir(tmp)
- # Pre-create a merge-config.py with custom content
- scripts_dir = skill_dir / "scripts"
- scripts_dir.mkdir(exist_ok=True)
- existing_script = scripts_dir / "merge-config.py"
- existing_script.write_text("# my custom script\n")
- code, data = run_scaffold(skill_dir)
- assert code == 0
- # Should be skipped
- assert "scripts/merge-config.py" in data["files_skipped"]
- assert len(data["warnings"]) >= 1
- assert any("merge-config.py" in w for w in data["warnings"])
- # Content should be preserved
- assert existing_script.read_text() == "# my custom script\n"
- # merge-help-csv.py should still be created
- assert "scripts/merge-help-csv.py" in data["files_created"]
- def test_creates_missing_subdirectories():
- """Test that scripts/ directory is created if it doesn't exist."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- skill_dir = make_skill_dir(tmp)
- # Verify scripts/ doesn't exist yet
- assert not (skill_dir / "scripts").exists()
- code, data = run_scaffold(skill_dir)
- assert code == 0
- assert (skill_dir / "scripts").is_dir()
- assert (skill_dir / "scripts" / "merge-config.py").is_file()
- def test_preserves_existing_skill_files():
- """Test that existing skill files are not modified or deleted."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- skill_dir = make_skill_dir(tmp)
- # Add extra files
- (skill_dir / "build-process.md").write_text("# Build\n")
- refs_dir = skill_dir / "references"
- refs_dir.mkdir()
- (refs_dir / "my-ref.md").write_text("# Reference\n")
- original_skill_md = (skill_dir / "SKILL.md").read_text()
- code, data = run_scaffold(skill_dir)
- assert code == 0
- # Original files untouched
- assert (skill_dir / "SKILL.md").read_text() == original_skill_md
- assert (skill_dir / "build-process.md").read_text() == "# Build\n"
- assert (refs_dir / "my-ref.md").read_text() == "# Reference\n"
- def test_missing_skill_dir():
- """Test error when skill directory doesn't exist."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- nonexistent = tmp / "nonexistent-skill"
- cmd = [
- sys.executable, str(SCRIPT),
- "--skill-dir", str(nonexistent),
- "--module-code", "tst",
- "--module-name", "Test",
- ]
- result = subprocess.run(cmd, capture_output=True, text=True)
- assert result.returncode == 2
- data = json.loads(result.stdout)
- assert data["status"] == "error"
- def test_missing_skill_md():
- """Test error when skill directory has no SKILL.md."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- skill_dir = tmp / "empty-skill"
- skill_dir.mkdir()
- (skill_dir / "assets").mkdir()
- (skill_dir / "assets" / "module.yaml").write_text("code: tst\n")
- cmd = [
- sys.executable, str(SCRIPT),
- "--skill-dir", str(skill_dir),
- "--module-code", "tst",
- "--module-name", "Test",
- ]
- result = subprocess.run(cmd, capture_output=True, text=True)
- assert result.returncode == 2
- data = json.loads(result.stdout)
- assert data["status"] == "error"
- assert "SKILL.md" in data["message"]
- def test_missing_module_yaml():
- """Test error when assets/module.yaml hasn't been written yet."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- skill_dir = tmp / "skill-no-yaml"
- skill_dir.mkdir()
- (skill_dir / "SKILL.md").write_text("---\nname: test\n---\n")
- cmd = [
- sys.executable, str(SCRIPT),
- "--skill-dir", str(skill_dir),
- "--module-code", "tst",
- "--module-name", "Test",
- ]
- result = subprocess.run(cmd, capture_output=True, text=True)
- assert result.returncode == 2
- data = json.loads(result.stdout)
- assert data["status"] == "error"
- assert "module.yaml" in data["message"]
- def test_custom_marketplace_dir():
- """Test that --marketplace-dir places marketplace.json in a custom location."""
- with tempfile.TemporaryDirectory() as tmp:
- tmp = Path(tmp)
- skill_dir = make_skill_dir(tmp)
- custom_dir = tmp / "custom-root"
- custom_dir.mkdir()
- code, data = run_scaffold(skill_dir, marketplace_dir=custom_dir)
- assert code == 0
- # Should be at custom location, not default parent
- assert (custom_dir / ".claude-plugin" / "marketplace.json").is_file()
- assert not (tmp / ".claude-plugin" / "marketplace.json").exists()
- assert data["marketplace_json"] == str((custom_dir / ".claude-plugin" / "marketplace.json").resolve())
- if __name__ == "__main__":
- tests = [
- test_basic_scaffold,
- test_marketplace_json_content,
- test_does_not_overwrite_existing_scripts,
- test_creates_missing_subdirectories,
- test_preserves_existing_skill_files,
- test_missing_skill_dir,
- test_missing_skill_md,
- test_missing_module_yaml,
- test_custom_marketplace_dir,
- ]
- 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)
|