Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
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()
|