- 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
330 lines
11 KiB
Python
330 lines
11 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["pyyaml"]
|
|
# ///
|
|
|
|
#!/usr/bin/env python3
|
|
"""Unit tests for bmad_init.py"""
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from bmad_init import (
|
|
find_project_root,
|
|
parse_var_specs,
|
|
resolve_project_root_placeholder,
|
|
expand_template,
|
|
apply_result_template,
|
|
load_module_yaml,
|
|
find_core_module_yaml,
|
|
find_target_module_yaml,
|
|
load_config_file,
|
|
load_module_config,
|
|
)
|
|
|
|
|
|
class TestFindProjectRoot(unittest.TestCase):
|
|
|
|
def test_finds_bmad_folder(self):
|
|
temp_dir = tempfile.mkdtemp()
|
|
try:
|
|
(Path(temp_dir) / '_bmad').mkdir()
|
|
original_cwd = os.getcwd()
|
|
try:
|
|
os.chdir(temp_dir)
|
|
result = find_project_root()
|
|
self.assertEqual(result.resolve(), Path(temp_dir).resolve())
|
|
finally:
|
|
os.chdir(original_cwd)
|
|
finally:
|
|
shutil.rmtree(temp_dir)
|
|
|
|
def test_llm_provided_with_bmad(self):
|
|
temp_dir = tempfile.mkdtemp()
|
|
try:
|
|
(Path(temp_dir) / '_bmad').mkdir()
|
|
result = find_project_root(llm_provided=temp_dir)
|
|
self.assertEqual(result.resolve(), Path(temp_dir).resolve())
|
|
finally:
|
|
shutil.rmtree(temp_dir)
|
|
|
|
def test_llm_provided_without_bmad_still_returns_dir(self):
|
|
"""First-run case: LLM provides path but _bmad doesn't exist yet."""
|
|
temp_dir = tempfile.mkdtemp()
|
|
try:
|
|
result = find_project_root(llm_provided=temp_dir)
|
|
self.assertEqual(result.resolve(), Path(temp_dir).resolve())
|
|
finally:
|
|
shutil.rmtree(temp_dir)
|
|
|
|
|
|
class TestParseVarSpecs(unittest.TestCase):
|
|
|
|
def test_vars_with_defaults(self):
|
|
specs = parse_var_specs('var1:value1,var2:value2')
|
|
self.assertEqual(len(specs), 2)
|
|
self.assertEqual(specs[0]['name'], 'var1')
|
|
self.assertEqual(specs[0]['default'], 'value1')
|
|
|
|
def test_vars_without_defaults(self):
|
|
specs = parse_var_specs('var1,var2')
|
|
self.assertEqual(len(specs), 2)
|
|
self.assertIsNone(specs[0]['default'])
|
|
|
|
def test_mixed_vars(self):
|
|
specs = parse_var_specs('required_var,var2:default2')
|
|
self.assertIsNone(specs[0]['default'])
|
|
self.assertEqual(specs[1]['default'], 'default2')
|
|
|
|
def test_colon_in_default(self):
|
|
specs = parse_var_specs('path:{project-root}/some/path')
|
|
self.assertEqual(specs[0]['default'], '{project-root}/some/path')
|
|
|
|
def test_empty_string(self):
|
|
self.assertEqual(parse_var_specs(''), [])
|
|
|
|
def test_none(self):
|
|
self.assertEqual(parse_var_specs(None), [])
|
|
|
|
|
|
class TestResolveProjectRootPlaceholder(unittest.TestCase):
|
|
|
|
def test_resolve_placeholder(self):
|
|
result = resolve_project_root_placeholder('{project-root}/output', Path('/test'))
|
|
self.assertEqual(result, '/test/output')
|
|
|
|
def test_no_placeholder(self):
|
|
result = resolve_project_root_placeholder('/absolute/path', Path('/test'))
|
|
self.assertEqual(result, '/absolute/path')
|
|
|
|
def test_none(self):
|
|
self.assertIsNone(resolve_project_root_placeholder(None, Path('/test')))
|
|
|
|
def test_non_string(self):
|
|
self.assertEqual(resolve_project_root_placeholder(42, Path('/test')), 42)
|
|
|
|
|
|
class TestExpandTemplate(unittest.TestCase):
|
|
|
|
def test_basic_expansion(self):
|
|
result = expand_template('{project-root}/output', {'project-root': '/test'})
|
|
self.assertEqual(result, '/test/output')
|
|
|
|
def test_multiple_placeholders(self):
|
|
result = expand_template(
|
|
'{output_folder}/planning',
|
|
{'output_folder': '_bmad-output', 'project-root': '/test'}
|
|
)
|
|
self.assertEqual(result, '_bmad-output/planning')
|
|
|
|
def test_none_value(self):
|
|
self.assertIsNone(expand_template(None, {}))
|
|
|
|
def test_non_string(self):
|
|
self.assertEqual(expand_template(42, {}), 42)
|
|
|
|
|
|
class TestApplyResultTemplate(unittest.TestCase):
|
|
|
|
def test_with_result_template(self):
|
|
var_def = {'result': '{project-root}/{value}'}
|
|
result = apply_result_template(var_def, '_bmad-output', {'project-root': '/test'})
|
|
self.assertEqual(result, '/test/_bmad-output')
|
|
|
|
def test_without_result_template(self):
|
|
result = apply_result_template({}, 'raw_value', {})
|
|
self.assertEqual(result, 'raw_value')
|
|
|
|
def test_value_only_template(self):
|
|
var_def = {'result': '{value}'}
|
|
result = apply_result_template(var_def, 'English', {})
|
|
self.assertEqual(result, 'English')
|
|
|
|
|
|
class TestLoadModuleYaml(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_loads_core_module_yaml(self):
|
|
path = Path(self.temp_dir) / 'module.yaml'
|
|
path.write_text(
|
|
'code: core\n'
|
|
'name: "BMad Core Module"\n'
|
|
'header: "Core Config"\n'
|
|
'user_name:\n'
|
|
' prompt: "What should agents call you?"\n'
|
|
' default: "BMad"\n'
|
|
' result: "{value}"\n'
|
|
)
|
|
result = load_module_yaml(path)
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result['meta']['code'], 'core')
|
|
self.assertEqual(result['meta']['name'], 'BMad Core Module')
|
|
self.assertIn('user_name', result['variables'])
|
|
self.assertEqual(result['variables']['user_name']['prompt'], 'What should agents call you?')
|
|
|
|
def test_loads_module_with_directories(self):
|
|
path = Path(self.temp_dir) / 'module.yaml'
|
|
path.write_text(
|
|
'code: bmm\n'
|
|
'name: "BMad Method"\n'
|
|
'project_name:\n'
|
|
' prompt: "Project name?"\n'
|
|
' default: "{directory_name}"\n'
|
|
' result: "{value}"\n'
|
|
'directories:\n'
|
|
' - "{planning_artifacts}"\n'
|
|
)
|
|
result = load_module_yaml(path)
|
|
self.assertEqual(result['directories'], ['{planning_artifacts}'])
|
|
|
|
def test_returns_none_for_missing(self):
|
|
result = load_module_yaml(Path(self.temp_dir) / 'nonexistent.yaml')
|
|
self.assertIsNone(result)
|
|
|
|
def test_returns_none_for_empty(self):
|
|
path = Path(self.temp_dir) / 'empty.yaml'
|
|
path.write_text('')
|
|
result = load_module_yaml(path)
|
|
self.assertIsNone(result)
|
|
|
|
|
|
class TestFindCoreModuleYaml(unittest.TestCase):
|
|
|
|
def test_returns_path_to_resources(self):
|
|
path = find_core_module_yaml()
|
|
self.assertTrue(str(path).endswith('resources/core-module.yaml'))
|
|
|
|
|
|
class TestFindTargetModuleYaml(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.project_root = Path(self.temp_dir)
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_finds_in_skill_assets(self):
|
|
skill_path = self.project_root / 'skills' / 'test-skill'
|
|
assets = skill_path / 'assets'
|
|
assets.mkdir(parents=True)
|
|
(assets / 'module.yaml').write_text('code: test\n')
|
|
|
|
result = find_target_module_yaml('test', self.project_root, str(skill_path))
|
|
self.assertIsNotNone(result)
|
|
self.assertTrue(str(result).endswith('assets/module.yaml'))
|
|
|
|
def test_finds_in_skill_root(self):
|
|
skill_path = self.project_root / 'skills' / 'test-skill'
|
|
skill_path.mkdir(parents=True)
|
|
(skill_path / 'module.yaml').write_text('code: test\n')
|
|
|
|
result = find_target_module_yaml('test', self.project_root, str(skill_path))
|
|
self.assertIsNotNone(result)
|
|
|
|
def test_finds_in_bmad_module_dir(self):
|
|
module_dir = self.project_root / '_bmad' / 'mymod'
|
|
module_dir.mkdir(parents=True)
|
|
(module_dir / 'module.yaml').write_text('code: mymod\n')
|
|
|
|
result = find_target_module_yaml('mymod', self.project_root)
|
|
self.assertIsNotNone(result)
|
|
|
|
def test_returns_none_when_not_found(self):
|
|
result = find_target_module_yaml('missing', self.project_root)
|
|
self.assertIsNone(result)
|
|
|
|
def test_skill_path_takes_priority(self):
|
|
"""Skill assets module.yaml takes priority over _bmad/{module}/."""
|
|
skill_path = self.project_root / 'skills' / 'test-skill'
|
|
assets = skill_path / 'assets'
|
|
assets.mkdir(parents=True)
|
|
(assets / 'module.yaml').write_text('code: test\nname: from-skill\n')
|
|
|
|
module_dir = self.project_root / '_bmad' / 'test'
|
|
module_dir.mkdir(parents=True)
|
|
(module_dir / 'module.yaml').write_text('code: test\nname: from-bmad\n')
|
|
|
|
result = find_target_module_yaml('test', self.project_root, str(skill_path))
|
|
self.assertTrue('assets' in str(result))
|
|
|
|
|
|
class TestLoadConfigFile(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_loads_flat_yaml(self):
|
|
path = Path(self.temp_dir) / 'config.yaml'
|
|
path.write_text('user_name: Test\ncommunication_language: English\n')
|
|
result = load_config_file(path)
|
|
self.assertEqual(result['user_name'], 'Test')
|
|
|
|
def test_returns_none_for_missing(self):
|
|
result = load_config_file(Path(self.temp_dir) / 'missing.yaml')
|
|
self.assertIsNone(result)
|
|
|
|
|
|
class TestLoadModuleConfig(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.project_root = Path(self.temp_dir)
|
|
bmad_core = self.project_root / '_bmad' / 'core'
|
|
bmad_core.mkdir(parents=True)
|
|
(bmad_core / 'config.yaml').write_text(
|
|
'user_name: TestUser\n'
|
|
'communication_language: English\n'
|
|
'document_output_language: English\n'
|
|
'output_folder: "{project-root}/_bmad-output"\n'
|
|
)
|
|
bmad_bmb = self.project_root / '_bmad' / 'bmb'
|
|
bmad_bmb.mkdir(parents=True)
|
|
(bmad_bmb / 'config.yaml').write_text(
|
|
'user_name: TestUser\n'
|
|
'communication_language: English\n'
|
|
'document_output_language: English\n'
|
|
'output_folder: "{project-root}/_bmad-output"\n'
|
|
'bmad_builder_output_folder: "{project-root}/_bmad-output/skills"\n'
|
|
'bmad_builder_reports: "{project-root}/_bmad-output/reports"\n'
|
|
)
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_load_core(self):
|
|
result = load_module_config('core', self.project_root)
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result['user_name'], 'TestUser')
|
|
|
|
def test_load_module_includes_core_vars(self):
|
|
result = load_module_config('bmb', self.project_root)
|
|
self.assertIsNotNone(result)
|
|
# Module-specific var
|
|
self.assertIn('bmad_builder_output_folder', result)
|
|
# Core vars also present
|
|
self.assertEqual(result['user_name'], 'TestUser')
|
|
|
|
def test_missing_module(self):
|
|
result = load_module_config('nonexistent', self.project_root)
|
|
self.assertIsNone(result)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|