Files
Classeo/.agents/skills/bmad-init/scripts/tests/test_bmad_init.py
Mathias STRASSER b7dc27f2a5
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Calculer automatiquement les moyennes après chaque saisie de notes
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.
2026-04-04 02:25:00 +02:00

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()