- 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
238 lines
9.2 KiB
Python
238 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
# /// script
|
|
# requires-python = ">=3.9"
|
|
# dependencies = []
|
|
# ///
|
|
"""Unit tests for merge-help-csv.py."""
|
|
|
|
import csv
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from io import StringIO
|
|
from pathlib import Path
|
|
|
|
# Import merge_help_csv module
|
|
from importlib.util import spec_from_file_location, module_from_spec
|
|
|
|
_spec = spec_from_file_location(
|
|
"merge_help_csv",
|
|
str(Path(__file__).parent.parent / "merge-help-csv.py"),
|
|
)
|
|
merge_help_csv_mod = module_from_spec(_spec)
|
|
_spec.loader.exec_module(merge_help_csv_mod)
|
|
|
|
extract_module_codes = merge_help_csv_mod.extract_module_codes
|
|
filter_rows = merge_help_csv_mod.filter_rows
|
|
read_csv_rows = merge_help_csv_mod.read_csv_rows
|
|
write_csv = merge_help_csv_mod.write_csv
|
|
cleanup_legacy_csvs = merge_help_csv_mod.cleanup_legacy_csvs
|
|
HEADER = merge_help_csv_mod.HEADER
|
|
|
|
|
|
SAMPLE_ROWS = [
|
|
["bmb", "", "bmad-bmb-module-init", "Install Module", "IM", "install", "", "Install BMad Builder.", "anytime", "", "", "false", "", "config", ""],
|
|
["bmb", "", "bmad-agent-builder", "Build Agent", "BA", "build-process", "", "Create an agent.", "anytime", "", "", "false", "output_folder", "agent skill", ""],
|
|
]
|
|
|
|
|
|
class TestExtractModuleCodes(unittest.TestCase):
|
|
def test_extracts_codes(self):
|
|
codes = extract_module_codes(SAMPLE_ROWS)
|
|
self.assertEqual(codes, {"bmb"})
|
|
|
|
def test_multiple_codes(self):
|
|
rows = SAMPLE_ROWS + [
|
|
["cis", "", "cis-skill", "CIS Skill", "CS", "run", "", "A skill.", "anytime", "", "", "false", "", "", ""],
|
|
]
|
|
codes = extract_module_codes(rows)
|
|
self.assertEqual(codes, {"bmb", "cis"})
|
|
|
|
def test_empty_rows(self):
|
|
codes = extract_module_codes([])
|
|
self.assertEqual(codes, set())
|
|
|
|
|
|
class TestFilterRows(unittest.TestCase):
|
|
def test_removes_matching_rows(self):
|
|
result = filter_rows(SAMPLE_ROWS, "bmb")
|
|
self.assertEqual(len(result), 0)
|
|
|
|
def test_preserves_non_matching_rows(self):
|
|
mixed_rows = SAMPLE_ROWS + [
|
|
["cis", "", "cis-skill", "CIS Skill", "CS", "run", "", "A skill.", "anytime", "", "", "false", "", "", ""],
|
|
]
|
|
result = filter_rows(mixed_rows, "bmb")
|
|
self.assertEqual(len(result), 1)
|
|
self.assertEqual(result[0][0], "cis")
|
|
|
|
def test_no_match_preserves_all(self):
|
|
result = filter_rows(SAMPLE_ROWS, "xyz")
|
|
self.assertEqual(len(result), 2)
|
|
|
|
|
|
class TestReadWriteCSV(unittest.TestCase):
|
|
def test_nonexistent_file_returns_empty(self):
|
|
header, rows = read_csv_rows("/nonexistent/path/file.csv")
|
|
self.assertEqual(header, [])
|
|
self.assertEqual(rows, [])
|
|
|
|
def test_round_trip(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
path = os.path.join(tmpdir, "test.csv")
|
|
write_csv(path, HEADER, SAMPLE_ROWS)
|
|
|
|
header, rows = read_csv_rows(path)
|
|
self.assertEqual(len(rows), 2)
|
|
self.assertEqual(rows[0][0], "bmb")
|
|
self.assertEqual(rows[0][2], "bmad-bmb-module-init")
|
|
|
|
def test_creates_parent_dirs(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
path = os.path.join(tmpdir, "sub", "dir", "test.csv")
|
|
write_csv(path, HEADER, SAMPLE_ROWS)
|
|
self.assertTrue(os.path.exists(path))
|
|
|
|
|
|
class TestEndToEnd(unittest.TestCase):
|
|
def _write_source(self, tmpdir, rows):
|
|
path = os.path.join(tmpdir, "source.csv")
|
|
write_csv(path, HEADER, rows)
|
|
return path
|
|
|
|
def _write_target(self, tmpdir, rows):
|
|
path = os.path.join(tmpdir, "target.csv")
|
|
write_csv(path, HEADER, rows)
|
|
return path
|
|
|
|
def test_fresh_install_no_existing_target(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
source_path = self._write_source(tmpdir, SAMPLE_ROWS)
|
|
target_path = os.path.join(tmpdir, "target.csv")
|
|
|
|
# Target doesn't exist
|
|
self.assertFalse(os.path.exists(target_path))
|
|
|
|
# Simulate merge
|
|
_, source_rows = read_csv_rows(source_path)
|
|
source_codes = extract_module_codes(source_rows)
|
|
write_csv(target_path, HEADER, source_rows)
|
|
|
|
_, result_rows = read_csv_rows(target_path)
|
|
self.assertEqual(len(result_rows), 2)
|
|
|
|
def test_merge_into_existing_with_other_module(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
other_rows = [
|
|
["cis", "", "cis-skill", "CIS Skill", "CS", "run", "", "A skill.", "anytime", "", "", "false", "", "", ""],
|
|
]
|
|
target_path = self._write_target(tmpdir, other_rows)
|
|
source_path = self._write_source(tmpdir, SAMPLE_ROWS)
|
|
|
|
# Read both
|
|
_, target_rows = read_csv_rows(target_path)
|
|
_, source_rows = read_csv_rows(source_path)
|
|
source_codes = extract_module_codes(source_rows)
|
|
|
|
# Anti-zombie filter + append
|
|
filtered = target_rows
|
|
for code in source_codes:
|
|
filtered = filter_rows(filtered, code)
|
|
merged = filtered + source_rows
|
|
|
|
write_csv(target_path, HEADER, merged)
|
|
|
|
_, result_rows = read_csv_rows(target_path)
|
|
self.assertEqual(len(result_rows), 3) # 1 cis + 2 bmb
|
|
|
|
def test_anti_zombie_replaces_stale_entries(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Existing target has old bmb entries + cis entry
|
|
old_bmb_rows = [
|
|
["bmb", "", "old-skill", "Old Skill", "OS", "run", "", "Old.", "anytime", "", "", "false", "", "", ""],
|
|
["bmb", "", "another-old", "Another", "AO", "run", "", "Old too.", "anytime", "", "", "false", "", "", ""],
|
|
]
|
|
cis_rows = [
|
|
["cis", "", "cis-skill", "CIS Skill", "CS", "run", "", "A skill.", "anytime", "", "", "false", "", "", ""],
|
|
]
|
|
target_path = self._write_target(tmpdir, old_bmb_rows + cis_rows)
|
|
source_path = self._write_source(tmpdir, SAMPLE_ROWS)
|
|
|
|
# Read both
|
|
_, target_rows = read_csv_rows(target_path)
|
|
_, source_rows = read_csv_rows(source_path)
|
|
source_codes = extract_module_codes(source_rows)
|
|
|
|
# Anti-zombie filter + append
|
|
filtered = target_rows
|
|
for code in source_codes:
|
|
filtered = filter_rows(filtered, code)
|
|
merged = filtered + source_rows
|
|
|
|
write_csv(target_path, HEADER, merged)
|
|
|
|
_, result_rows = read_csv_rows(target_path)
|
|
# Should have 1 cis + 2 new bmb = 3 (old bmb removed)
|
|
self.assertEqual(len(result_rows), 3)
|
|
module_codes = [r[0] for r in result_rows]
|
|
self.assertEqual(module_codes.count("bmb"), 2)
|
|
self.assertEqual(module_codes.count("cis"), 1)
|
|
# Old skills should be gone
|
|
skill_names = [r[2] for r in result_rows]
|
|
self.assertNotIn("old-skill", skill_names)
|
|
self.assertNotIn("another-old", skill_names)
|
|
|
|
|
|
class TestCleanupLegacyCsvs(unittest.TestCase):
|
|
def test_deletes_module_and_core_csvs(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
legacy_dir = os.path.join(tmpdir, "_bmad")
|
|
for subdir in ("core", "bmb"):
|
|
d = os.path.join(legacy_dir, subdir)
|
|
os.makedirs(d)
|
|
with open(os.path.join(d, "module-help.csv"), "w") as f:
|
|
f.write("header\nrow\n")
|
|
|
|
deleted = cleanup_legacy_csvs(legacy_dir, "bmb")
|
|
self.assertEqual(len(deleted), 2)
|
|
self.assertFalse(os.path.exists(os.path.join(legacy_dir, "core", "module-help.csv")))
|
|
self.assertFalse(os.path.exists(os.path.join(legacy_dir, "bmb", "module-help.csv")))
|
|
# Directories still exist
|
|
self.assertTrue(os.path.isdir(os.path.join(legacy_dir, "core")))
|
|
self.assertTrue(os.path.isdir(os.path.join(legacy_dir, "bmb")))
|
|
|
|
def test_leaves_other_module_csvs_alone(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
legacy_dir = os.path.join(tmpdir, "_bmad")
|
|
for subdir in ("bmb", "cis"):
|
|
d = os.path.join(legacy_dir, subdir)
|
|
os.makedirs(d)
|
|
with open(os.path.join(d, "module-help.csv"), "w") as f:
|
|
f.write("header\nrow\n")
|
|
|
|
deleted = cleanup_legacy_csvs(legacy_dir, "bmb")
|
|
self.assertEqual(len(deleted), 1) # only bmb, not cis
|
|
self.assertTrue(os.path.exists(os.path.join(legacy_dir, "cis", "module-help.csv")))
|
|
|
|
def test_no_legacy_files_returns_empty(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
deleted = cleanup_legacy_csvs(tmpdir, "bmb")
|
|
self.assertEqual(deleted, [])
|
|
|
|
def test_handles_only_core_no_module(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
legacy_dir = os.path.join(tmpdir, "_bmad")
|
|
core_dir = os.path.join(legacy_dir, "core")
|
|
os.makedirs(core_dir)
|
|
with open(os.path.join(core_dir, "module-help.csv"), "w") as f:
|
|
f.write("header\nrow\n")
|
|
|
|
deleted = cleanup_legacy_csvs(legacy_dir, "bmb")
|
|
self.assertEqual(len(deleted), 1)
|
|
self.assertFalse(os.path.exists(os.path.join(core_dir, "module-help.csv")))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|