test-scaffold-setup-skill.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. #!/usr/bin/env python3
  2. # /// script
  3. # requires-python = ">=3.10"
  4. # ///
  5. """Tests for scaffold-setup-skill.py"""
  6. import json
  7. import subprocess
  8. import sys
  9. import tempfile
  10. from pathlib import Path
  11. SCRIPT = Path(__file__).resolve().parent.parent / "scaffold-setup-skill.py"
  12. TEMPLATE_DIR = Path(__file__).resolve().parent.parent.parent / "assets" / "setup-skill-template"
  13. def run_scaffold(tmp: Path, **kwargs) -> tuple[int, dict]:
  14. """Run the scaffold script and return (exit_code, parsed_json)."""
  15. target_dir = kwargs.get("target_dir", str(tmp / "output"))
  16. Path(target_dir).mkdir(parents=True, exist_ok=True)
  17. module_code = kwargs.get("module_code", "tst")
  18. module_name = kwargs.get("module_name", "Test Module")
  19. yaml_path = tmp / "module.yaml"
  20. csv_path = tmp / "module-help.csv"
  21. yaml_path.write_text(kwargs.get("yaml_content", f'code: {module_code}\nname: "{module_name}"\n'))
  22. csv_path.write_text(
  23. kwargs.get(
  24. "csv_content",
  25. "module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs\n"
  26. f'{module_name},{module_code}-example,Example,EX,An example skill,do-thing,,anytime,,,false,output_folder,artifact\n',
  27. )
  28. )
  29. cmd = [
  30. sys.executable,
  31. str(SCRIPT),
  32. "--target-dir", target_dir,
  33. "--module-code", module_code,
  34. "--module-name", module_name,
  35. "--module-yaml", str(yaml_path),
  36. "--module-csv", str(csv_path),
  37. ]
  38. result = subprocess.run(cmd, capture_output=True, text=True)
  39. try:
  40. data = json.loads(result.stdout)
  41. except json.JSONDecodeError:
  42. data = {"raw_stdout": result.stdout, "raw_stderr": result.stderr}
  43. return result.returncode, data
  44. def test_basic_scaffold():
  45. """Test that scaffolding creates the expected structure."""
  46. with tempfile.TemporaryDirectory() as tmp:
  47. tmp = Path(tmp)
  48. target_dir = tmp / "output"
  49. target_dir.mkdir()
  50. code, data = run_scaffold(tmp, target_dir=str(target_dir))
  51. assert code == 0, f"Script failed: {data}"
  52. assert data["status"] == "success"
  53. assert data["setup_skill"] == "tst-setup"
  54. setup_dir = target_dir / "tst-setup"
  55. assert setup_dir.is_dir()
  56. assert (setup_dir / "SKILL.md").is_file()
  57. assert (setup_dir / "scripts" / "merge-config.py").is_file()
  58. assert (setup_dir / "scripts" / "merge-help-csv.py").is_file()
  59. assert (setup_dir / "scripts" / "cleanup-legacy.py").is_file()
  60. assert (setup_dir / "assets" / "module.yaml").is_file()
  61. assert (setup_dir / "assets" / "module-help.csv").is_file()
  62. def test_skill_md_frontmatter_substitution():
  63. """Test that SKILL.md placeholders are replaced."""
  64. with tempfile.TemporaryDirectory() as tmp:
  65. tmp = Path(tmp)
  66. target_dir = tmp / "output"
  67. target_dir.mkdir()
  68. code, data = run_scaffold(
  69. tmp,
  70. target_dir=str(target_dir),
  71. module_code="xyz",
  72. module_name="XYZ Studio",
  73. )
  74. assert code == 0
  75. skill_md = (target_dir / "xyz-setup" / "SKILL.md").read_text()
  76. assert "xyz-setup" in skill_md
  77. assert "XYZ Studio" in skill_md
  78. assert "{setup-skill-name}" not in skill_md
  79. assert "{module-name}" not in skill_md
  80. assert "{module-code}" not in skill_md
  81. def test_template_frontmatter_uses_quoted_name_placeholder():
  82. """Test that the template frontmatter is valid before substitution."""
  83. template_skill_md = (TEMPLATE_DIR / "SKILL.md").read_text()
  84. assert 'name: "{setup-skill-name}"' in template_skill_md
  85. def test_generated_files_written():
  86. """Test that module.yaml and module-help.csv contain generated content."""
  87. with tempfile.TemporaryDirectory() as tmp:
  88. tmp = Path(tmp)
  89. target_dir = tmp / "output"
  90. target_dir.mkdir()
  91. custom_yaml = 'code: abc\nname: "ABC Module"\ndescription: "Custom desc"\n'
  92. custom_csv = "module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs\nABC Module,bmad-abc-thing,Do Thing,DT,Does the thing,run,,anytime,,,false,output_folder,report\n"
  93. code, data = run_scaffold(
  94. tmp,
  95. target_dir=str(target_dir),
  96. module_code="abc",
  97. module_name="ABC Module",
  98. yaml_content=custom_yaml,
  99. csv_content=custom_csv,
  100. )
  101. assert code == 0
  102. yaml_content = (target_dir / "abc-setup" / "assets" / "module.yaml").read_text()
  103. assert "ABC Module" in yaml_content
  104. assert "Custom desc" in yaml_content
  105. csv_content = (target_dir / "abc-setup" / "assets" / "module-help.csv").read_text()
  106. assert "bmad-abc-thing" in csv_content
  107. assert "DT" in csv_content
  108. def test_anti_zombie_replaces_existing():
  109. """Test that an existing setup skill is replaced cleanly."""
  110. with tempfile.TemporaryDirectory() as tmp:
  111. tmp = Path(tmp)
  112. target_dir = tmp / "output"
  113. target_dir.mkdir()
  114. # First scaffold
  115. run_scaffold(tmp, target_dir=str(target_dir))
  116. stale_file = target_dir / "tst-setup" / "stale-marker.txt"
  117. stale_file.write_text("should be removed")
  118. # Second scaffold should remove stale file
  119. code, data = run_scaffold(tmp, target_dir=str(target_dir))
  120. assert code == 0
  121. assert not stale_file.exists()
  122. def test_missing_target_dir():
  123. """Test error when target directory doesn't exist."""
  124. with tempfile.TemporaryDirectory() as tmp:
  125. tmp = Path(tmp)
  126. nonexistent = tmp / "nonexistent"
  127. # Write valid source files
  128. yaml_path = tmp / "module.yaml"
  129. csv_path = tmp / "module-help.csv"
  130. yaml_path.write_text('code: tst\nname: "Test"\n')
  131. csv_path.write_text("header\n")
  132. cmd = [
  133. sys.executable,
  134. str(SCRIPT),
  135. "--target-dir", str(nonexistent),
  136. "--module-code", "tst",
  137. "--module-name", "Test",
  138. "--module-yaml", str(yaml_path),
  139. "--module-csv", str(csv_path),
  140. ]
  141. result = subprocess.run(cmd, capture_output=True, text=True)
  142. assert result.returncode == 2
  143. data = json.loads(result.stdout)
  144. assert data["status"] == "error"
  145. def test_missing_source_file():
  146. """Test error when module.yaml source doesn't exist."""
  147. with tempfile.TemporaryDirectory() as tmp:
  148. tmp = Path(tmp)
  149. target_dir = tmp / "output"
  150. target_dir.mkdir()
  151. # Remove the yaml after creation to simulate missing file
  152. yaml_path = tmp / "module.yaml"
  153. csv_path = tmp / "module-help.csv"
  154. csv_path.write_text("header\n")
  155. # Don't create yaml_path
  156. cmd = [
  157. sys.executable,
  158. str(SCRIPT),
  159. "--target-dir", str(target_dir),
  160. "--module-code", "tst",
  161. "--module-name", "Test",
  162. "--module-yaml", str(yaml_path),
  163. "--module-csv", str(csv_path),
  164. ]
  165. result = subprocess.run(cmd, capture_output=True, text=True)
  166. assert result.returncode == 2
  167. data = json.loads(result.stdout)
  168. assert data["status"] == "error"
  169. if __name__ == "__main__":
  170. tests = [
  171. test_basic_scaffold,
  172. test_skill_md_frontmatter_substitution,
  173. test_template_frontmatter_uses_quoted_name_placeholder,
  174. test_generated_files_written,
  175. test_anti_zombie_replaces_existing,
  176. test_missing_target_dir,
  177. test_missing_source_file,
  178. ]
  179. passed = 0
  180. failed = 0
  181. for test in tests:
  182. try:
  183. test()
  184. print(f" PASS: {test.__name__}")
  185. passed += 1
  186. except AssertionError as e:
  187. print(f" FAIL: {test.__name__}: {e}")
  188. failed += 1
  189. except Exception as e:
  190. print(f" ERROR: {test.__name__}: {e}")
  191. failed += 1
  192. print(f"\n{passed} passed, {failed} failed")
  193. sys.exit(1 if failed else 0)