Files
Keep/.claude/skills/bmad-builder-setup/scripts/tests/test-cleanup-legacy.py
Sepehr Ramezani fa7e166f3e feat: add reminders page, BMad skills upgrade, MCP server refactor
- Add reminders page with navigation support
- Upgrade BMad builder module to skills-based architecture
- Refactor MCP server: extract tools and auth into separate modules
- Add connections cache, custom AI provider support
- Update prisma schema and generated client
- Various UI/UX improvements and i18n updates
- Add service worker for PWA support

Made-with: Cursor
2026-04-13 21:02:53 +02:00

430 lines
18 KiB
Python

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# dependencies = []
# ///
"""Unit tests for cleanup-legacy.py."""
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
# Add parent directory to path so we can import the module
sys.path.insert(0, str(Path(__file__).parent.parent))
from importlib.util import spec_from_file_location, module_from_spec
# Import cleanup_legacy module
_spec = spec_from_file_location(
"cleanup_legacy",
str(Path(__file__).parent.parent / "cleanup-legacy.py"),
)
cleanup_legacy_mod = module_from_spec(_spec)
_spec.loader.exec_module(cleanup_legacy_mod)
find_skill_dirs = cleanup_legacy_mod.find_skill_dirs
verify_skills_installed = cleanup_legacy_mod.verify_skills_installed
count_files = cleanup_legacy_mod.count_files
cleanup_directories = cleanup_legacy_mod.cleanup_directories
def _make_skill_dir(base, *path_parts):
"""Create a skill directory with a SKILL.md file."""
skill_dir = os.path.join(base, *path_parts)
os.makedirs(skill_dir, exist_ok=True)
with open(os.path.join(skill_dir, "SKILL.md"), "w") as f:
f.write("---\nname: test-skill\n---\n# Test\n")
return skill_dir
def _make_file(base, *path_parts, content="placeholder"):
"""Create a file at the given path."""
file_path = os.path.join(base, *path_parts)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w") as f:
f.write(content)
return file_path
class TestFindSkillDirs(unittest.TestCase):
def test_finds_dirs_with_skill_md(self):
with tempfile.TemporaryDirectory() as tmpdir:
_make_skill_dir(tmpdir, "skills", "bmad-agent-builder")
_make_skill_dir(tmpdir, "skills", "bmad-workflow-builder")
result = find_skill_dirs(tmpdir)
self.assertEqual(result, ["bmad-agent-builder", "bmad-workflow-builder"])
def test_ignores_dirs_without_skill_md(self):
with tempfile.TemporaryDirectory() as tmpdir:
_make_skill_dir(tmpdir, "skills", "real-skill")
os.makedirs(os.path.join(tmpdir, "skills", "not-a-skill"))
_make_file(tmpdir, "skills", "not-a-skill", "README.md")
result = find_skill_dirs(tmpdir)
self.assertEqual(result, ["real-skill"])
def test_empty_directory(self):
with tempfile.TemporaryDirectory() as tmpdir:
result = find_skill_dirs(tmpdir)
self.assertEqual(result, [])
def test_nonexistent_directory(self):
result = find_skill_dirs("/nonexistent/path")
self.assertEqual(result, [])
def test_finds_nested_skills_in_phase_subdirs(self):
"""Skills nested in phase directories like bmm/1-analysis/bmad-agent-analyst/."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_skill_dir(tmpdir, "1-analysis", "bmad-agent-analyst")
_make_skill_dir(tmpdir, "2-plan", "bmad-agent-pm")
_make_skill_dir(tmpdir, "4-impl", "bmad-agent-dev")
result = find_skill_dirs(tmpdir)
self.assertEqual(
result, ["bmad-agent-analyst", "bmad-agent-dev", "bmad-agent-pm"]
)
def test_deduplicates_skill_names(self):
"""If the same skill name appears in multiple locations, only listed once."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_skill_dir(tmpdir, "a", "my-skill")
_make_skill_dir(tmpdir, "b", "my-skill")
result = find_skill_dirs(tmpdir)
self.assertEqual(result, ["my-skill"])
class TestVerifySkillsInstalled(unittest.TestCase):
def test_all_skills_present(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
# Legacy: bmb has two skills
_make_skill_dir(bmad_dir, "bmb", "skills", "skill-a")
_make_skill_dir(bmad_dir, "bmb", "skills", "skill-b")
# Installed: both exist
os.makedirs(os.path.join(skills_dir, "skill-a"))
os.makedirs(os.path.join(skills_dir, "skill-b"))
result = verify_skills_installed(bmad_dir, ["bmb"], skills_dir)
self.assertEqual(result, ["skill-a", "skill-b"])
def test_missing_skill_exits_1(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
_make_skill_dir(bmad_dir, "bmb", "skills", "skill-a")
_make_skill_dir(bmad_dir, "bmb", "skills", "skill-missing")
# Only skill-a installed
os.makedirs(os.path.join(skills_dir, "skill-a"))
with self.assertRaises(SystemExit) as ctx:
verify_skills_installed(bmad_dir, ["bmb"], skills_dir)
self.assertEqual(ctx.exception.code, 1)
def test_empty_legacy_dir_passes(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
os.makedirs(bmad_dir)
os.makedirs(skills_dir)
result = verify_skills_installed(bmad_dir, ["bmb"], skills_dir)
self.assertEqual(result, [])
def test_nonexistent_legacy_dir_skipped(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
os.makedirs(skills_dir)
# bmad_dir doesn't exist — should not error
result = verify_skills_installed(bmad_dir, ["bmb"], skills_dir)
self.assertEqual(result, [])
def test_dir_without_skills_skipped(self):
"""Directories like _config/ that have no SKILL.md are not verified."""
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
# _config has files but no SKILL.md
_make_file(bmad_dir, "_config", "manifest.yaml", content="version: 1")
_make_file(bmad_dir, "_config", "help.csv", content="a,b,c")
os.makedirs(skills_dir)
result = verify_skills_installed(bmad_dir, ["_config"], skills_dir)
self.assertEqual(result, [])
def test_verifies_across_multiple_dirs(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
_make_skill_dir(bmad_dir, "bmb", "skills", "skill-a")
_make_skill_dir(bmad_dir, "core", "skills", "skill-b")
os.makedirs(os.path.join(skills_dir, "skill-a"))
os.makedirs(os.path.join(skills_dir, "skill-b"))
result = verify_skills_installed(
bmad_dir, ["bmb", "core"], skills_dir
)
self.assertEqual(result, ["skill-a", "skill-b"])
class TestCountFiles(unittest.TestCase):
def test_counts_files_recursively(self):
with tempfile.TemporaryDirectory() as tmpdir:
_make_file(tmpdir, "a.txt")
_make_file(tmpdir, "sub", "b.txt")
_make_file(tmpdir, "sub", "deep", "c.txt")
self.assertEqual(count_files(Path(tmpdir)), 3)
def test_empty_dir_returns_zero(self):
with tempfile.TemporaryDirectory() as tmpdir:
self.assertEqual(count_files(Path(tmpdir)), 0)
class TestCleanupDirectories(unittest.TestCase):
def test_removes_single_module_dir(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
os.makedirs(os.path.join(bmad_dir, "bmb", "skills"))
_make_file(bmad_dir, "bmb", "skills", "SKILL.md")
removed, not_found, count = cleanup_directories(bmad_dir, ["bmb"])
self.assertEqual(removed, ["bmb"])
self.assertEqual(not_found, [])
self.assertGreater(count, 0)
self.assertFalse(os.path.exists(os.path.join(bmad_dir, "bmb")))
def test_removes_module_core_and_config(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
for dirname in ("bmb", "core", "_config"):
_make_file(bmad_dir, dirname, "some-file.txt")
removed, not_found, count = cleanup_directories(
bmad_dir, ["bmb", "core", "_config"]
)
self.assertEqual(sorted(removed), ["_config", "bmb", "core"])
self.assertEqual(not_found, [])
for dirname in ("bmb", "core", "_config"):
self.assertFalse(os.path.exists(os.path.join(bmad_dir, dirname)))
def test_nonexistent_dir_in_not_found(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
os.makedirs(bmad_dir)
removed, not_found, count = cleanup_directories(bmad_dir, ["bmb"])
self.assertEqual(removed, [])
self.assertEqual(not_found, ["bmb"])
self.assertEqual(count, 0)
def test_preserves_other_module_dirs(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
for dirname in ("bmb", "bmm", "tea"):
_make_file(bmad_dir, dirname, "file.txt")
removed, not_found, count = cleanup_directories(bmad_dir, ["bmb"])
self.assertEqual(removed, ["bmb"])
self.assertTrue(os.path.isdir(os.path.join(bmad_dir, "bmm")))
self.assertTrue(os.path.isdir(os.path.join(bmad_dir, "tea")))
def test_preserves_root_config_files(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
_make_file(bmad_dir, "config.yaml", content="key: val")
_make_file(bmad_dir, "config.user.yaml", content="user: test")
_make_file(bmad_dir, "module-help.csv", content="a,b,c")
_make_file(bmad_dir, "bmb", "stuff.txt")
cleanup_directories(bmad_dir, ["bmb"])
self.assertTrue(os.path.exists(os.path.join(bmad_dir, "config.yaml")))
self.assertTrue(
os.path.exists(os.path.join(bmad_dir, "config.user.yaml"))
)
self.assertTrue(
os.path.exists(os.path.join(bmad_dir, "module-help.csv"))
)
def test_removes_hidden_files(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
_make_file(bmad_dir, "bmb", ".DS_Store")
_make_file(bmad_dir, "bmb", "skills", ".hidden")
removed, not_found, count = cleanup_directories(bmad_dir, ["bmb"])
self.assertEqual(removed, ["bmb"])
self.assertEqual(count, 2)
self.assertFalse(os.path.exists(os.path.join(bmad_dir, "bmb")))
def test_idempotent_rerun(self):
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
_make_file(bmad_dir, "bmb", "file.txt")
# First run
removed1, not_found1, _ = cleanup_directories(bmad_dir, ["bmb"])
self.assertEqual(removed1, ["bmb"])
self.assertEqual(not_found1, [])
# Second run — idempotent
removed2, not_found2, count2 = cleanup_directories(bmad_dir, ["bmb"])
self.assertEqual(removed2, [])
self.assertEqual(not_found2, ["bmb"])
self.assertEqual(count2, 0)
class TestSafetyCheck(unittest.TestCase):
def test_no_skills_dir_skips_check(self):
"""When --skills-dir is not provided, no verification happens."""
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
_make_skill_dir(bmad_dir, "bmb", "skills", "some-skill")
# No skills_dir — cleanup should proceed without verification
removed, not_found, count = cleanup_directories(bmad_dir, ["bmb"])
self.assertEqual(removed, ["bmb"])
def test_missing_skill_blocks_removal(self):
"""When --skills-dir is provided and a skill is missing, exit 1."""
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
_make_skill_dir(bmad_dir, "bmb", "skills", "installed-skill")
_make_skill_dir(bmad_dir, "bmb", "skills", "missing-skill")
os.makedirs(os.path.join(skills_dir, "installed-skill"))
# missing-skill not created in skills_dir
with self.assertRaises(SystemExit) as ctx:
verify_skills_installed(bmad_dir, ["bmb"], skills_dir)
self.assertEqual(ctx.exception.code, 1)
# Directory should NOT have been removed (verification failed before cleanup)
self.assertTrue(os.path.isdir(os.path.join(bmad_dir, "bmb")))
def test_dir_without_skills_not_checked(self):
"""Directories like _config that have no SKILL.md pass verification."""
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
_make_file(bmad_dir, "_config", "manifest.yaml")
os.makedirs(skills_dir)
# Should not raise — _config has no skills to verify
result = verify_skills_installed(bmad_dir, ["_config"], skills_dir)
self.assertEqual(result, [])
class TestEndToEnd(unittest.TestCase):
def test_full_cleanup_with_verification(self):
"""Simulate complete cleanup flow with safety check."""
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
skills_dir = os.path.join(tmpdir, "skills")
# Create legacy structure
_make_skill_dir(bmad_dir, "bmb", "skills", "bmad-agent-builder")
_make_skill_dir(bmad_dir, "bmb", "skills", "bmad-builder-setup")
_make_file(bmad_dir, "bmb", "skills", "bmad-agent-builder", "assets", "template.md")
_make_skill_dir(bmad_dir, "core", "skills", "bmad-brainstorming")
_make_file(bmad_dir, "_config", "manifest.yaml")
_make_file(bmad_dir, "_config", "bmad-help.csv")
# Create root config files that must survive
_make_file(bmad_dir, "config.yaml", content="document_output_language: English")
_make_file(bmad_dir, "config.user.yaml", content="user_name: Test")
_make_file(bmad_dir, "module-help.csv", content="module,name\nbmb,builder")
# Create other module dirs that must survive
_make_file(bmad_dir, "bmm", "config.yaml")
_make_file(bmad_dir, "tea", "config.yaml")
# Create installed skills
os.makedirs(os.path.join(skills_dir, "bmad-agent-builder"))
os.makedirs(os.path.join(skills_dir, "bmad-builder-setup"))
os.makedirs(os.path.join(skills_dir, "bmad-brainstorming"))
# Verify
verified = verify_skills_installed(
bmad_dir, ["bmb", "core", "_config"], skills_dir
)
self.assertIn("bmad-agent-builder", verified)
self.assertIn("bmad-builder-setup", verified)
self.assertIn("bmad-brainstorming", verified)
# Cleanup
removed, not_found, file_count = cleanup_directories(
bmad_dir, ["bmb", "core", "_config"]
)
self.assertEqual(sorted(removed), ["_config", "bmb", "core"])
self.assertEqual(not_found, [])
self.assertGreater(file_count, 0)
# Verify final state
self.assertFalse(os.path.exists(os.path.join(bmad_dir, "bmb")))
self.assertFalse(os.path.exists(os.path.join(bmad_dir, "core")))
self.assertFalse(os.path.exists(os.path.join(bmad_dir, "_config")))
# Root config files survived
self.assertTrue(os.path.exists(os.path.join(bmad_dir, "config.yaml")))
self.assertTrue(os.path.exists(os.path.join(bmad_dir, "config.user.yaml")))
self.assertTrue(os.path.exists(os.path.join(bmad_dir, "module-help.csv")))
# Other modules survived
self.assertTrue(os.path.isdir(os.path.join(bmad_dir, "bmm")))
self.assertTrue(os.path.isdir(os.path.join(bmad_dir, "tea")))
def test_simulate_post_merge_scripts(self):
"""Simulate the full flow: merge scripts run first (delete config files),
then cleanup removes directories."""
with tempfile.TemporaryDirectory() as tmpdir:
bmad_dir = os.path.join(tmpdir, "_bmad")
# Legacy state: config files already deleted by merge scripts
# but directories and skill content remain
_make_skill_dir(bmad_dir, "bmb", "skills", "bmad-agent-builder")
_make_file(bmad_dir, "bmb", "skills", "bmad-agent-builder", "refs", "doc.md")
_make_file(bmad_dir, "bmb", ".DS_Store")
# config.yaml already deleted by merge-config.py
# module-help.csv already deleted by merge-help-csv.py
_make_skill_dir(bmad_dir, "core", "skills", "bmad-help")
# core/config.yaml already deleted
# core/module-help.csv already deleted
# Root files from merge scripts
_make_file(bmad_dir, "config.yaml", content="bmb:\n name: BMad Builder")
_make_file(bmad_dir, "config.user.yaml", content="user_name: Test")
_make_file(bmad_dir, "module-help.csv", content="module,name")
# Cleanup directories
removed, not_found, file_count = cleanup_directories(
bmad_dir, ["bmb", "core"]
)
self.assertEqual(sorted(removed), ["bmb", "core"])
self.assertGreater(file_count, 0)
# Final state: only root config files
remaining = os.listdir(bmad_dir)
self.assertEqual(
sorted(remaining),
["config.user.yaml", "config.yaml", "module-help.csv"],
)
if __name__ == "__main__":
unittest.main()