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.
224 lines
7.6 KiB
Python
224 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
# /// script
|
|
# requires-python = ">=3.10"
|
|
# ///
|
|
"""Tests for scaffold-setup-skill.py"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
SCRIPT = Path(__file__).resolve().parent.parent / "scaffold-setup-skill.py"
|
|
TEMPLATE_DIR = Path(__file__).resolve().parent.parent.parent / "assets" / "setup-skill-template"
|
|
|
|
|
|
def run_scaffold(tmp: Path, **kwargs) -> tuple[int, dict]:
|
|
"""Run the scaffold script and return (exit_code, parsed_json)."""
|
|
target_dir = kwargs.get("target_dir", str(tmp / "output"))
|
|
Path(target_dir).mkdir(parents=True, exist_ok=True)
|
|
|
|
module_code = kwargs.get("module_code", "tst")
|
|
module_name = kwargs.get("module_name", "Test Module")
|
|
|
|
yaml_path = tmp / "module.yaml"
|
|
csv_path = tmp / "module-help.csv"
|
|
yaml_path.write_text(kwargs.get("yaml_content", f'code: {module_code}\nname: "{module_name}"\n'))
|
|
csv_path.write_text(
|
|
kwargs.get(
|
|
"csv_content",
|
|
"module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs\n"
|
|
f'{module_name},bmad-{module_code}-example,Example,EX,An example skill,do-thing,,anytime,,,false,output_folder,artifact\n',
|
|
)
|
|
)
|
|
|
|
cmd = [
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--target-dir", target_dir,
|
|
"--module-code", module_code,
|
|
"--module-name", module_name,
|
|
"--module-yaml", str(yaml_path),
|
|
"--module-csv", str(csv_path),
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
try:
|
|
data = json.loads(result.stdout)
|
|
except json.JSONDecodeError:
|
|
data = {"raw_stdout": result.stdout, "raw_stderr": result.stderr}
|
|
return result.returncode, data
|
|
|
|
|
|
def test_basic_scaffold():
|
|
"""Test that scaffolding creates the expected structure."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
target_dir = tmp / "output"
|
|
target_dir.mkdir()
|
|
|
|
code, data = run_scaffold(tmp, target_dir=str(target_dir))
|
|
assert code == 0, f"Script failed: {data}"
|
|
assert data["status"] == "success"
|
|
assert data["setup_skill"] == "bmad-tst-setup"
|
|
|
|
setup_dir = target_dir / "bmad-tst-setup"
|
|
assert setup_dir.is_dir()
|
|
assert (setup_dir / "SKILL.md").is_file()
|
|
assert (setup_dir / "scripts" / "merge-config.py").is_file()
|
|
assert (setup_dir / "scripts" / "merge-help-csv.py").is_file()
|
|
assert (setup_dir / "scripts" / "cleanup-legacy.py").is_file()
|
|
assert (setup_dir / "assets" / "module.yaml").is_file()
|
|
assert (setup_dir / "assets" / "module-help.csv").is_file()
|
|
|
|
|
|
def test_skill_md_frontmatter_substitution():
|
|
"""Test that SKILL.md placeholders are replaced."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
target_dir = tmp / "output"
|
|
target_dir.mkdir()
|
|
|
|
code, data = run_scaffold(
|
|
tmp,
|
|
target_dir=str(target_dir),
|
|
module_code="xyz",
|
|
module_name="XYZ Studio",
|
|
)
|
|
assert code == 0
|
|
|
|
skill_md = (target_dir / "bmad-xyz-setup" / "SKILL.md").read_text()
|
|
assert "bmad-xyz-setup" in skill_md
|
|
assert "XYZ Studio" in skill_md
|
|
assert "{setup-skill-name}" not in skill_md
|
|
assert "{module-name}" not in skill_md
|
|
assert "{module-code}" not in skill_md
|
|
|
|
|
|
def test_generated_files_written():
|
|
"""Test that module.yaml and module-help.csv contain generated content."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
target_dir = tmp / "output"
|
|
target_dir.mkdir()
|
|
|
|
custom_yaml = 'code: abc\nname: "ABC Module"\ndescription: "Custom desc"\n'
|
|
custom_csv = "module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs\nABC Module,bmad-abc-thing,Do Thing,DT,Does the thing,run,,anytime,,,false,output_folder,report\n"
|
|
|
|
code, data = run_scaffold(
|
|
tmp,
|
|
target_dir=str(target_dir),
|
|
module_code="abc",
|
|
module_name="ABC Module",
|
|
yaml_content=custom_yaml,
|
|
csv_content=custom_csv,
|
|
)
|
|
assert code == 0
|
|
|
|
yaml_content = (target_dir / "bmad-abc-setup" / "assets" / "module.yaml").read_text()
|
|
assert "ABC Module" in yaml_content
|
|
assert "Custom desc" in yaml_content
|
|
|
|
csv_content = (target_dir / "bmad-abc-setup" / "assets" / "module-help.csv").read_text()
|
|
assert "bmad-abc-thing" in csv_content
|
|
assert "DT" in csv_content
|
|
|
|
|
|
def test_anti_zombie_replaces_existing():
|
|
"""Test that an existing setup skill is replaced cleanly."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
target_dir = tmp / "output"
|
|
target_dir.mkdir()
|
|
|
|
# First scaffold
|
|
run_scaffold(tmp, target_dir=str(target_dir))
|
|
stale_file = target_dir / "bmad-tst-setup" / "stale-marker.txt"
|
|
stale_file.write_text("should be removed")
|
|
|
|
# Second scaffold should remove stale file
|
|
code, data = run_scaffold(tmp, target_dir=str(target_dir))
|
|
assert code == 0
|
|
assert not stale_file.exists()
|
|
|
|
|
|
def test_missing_target_dir():
|
|
"""Test error when target directory doesn't exist."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
nonexistent = tmp / "nonexistent"
|
|
|
|
# Write valid source files
|
|
yaml_path = tmp / "module.yaml"
|
|
csv_path = tmp / "module-help.csv"
|
|
yaml_path.write_text('code: tst\nname: "Test"\n')
|
|
csv_path.write_text("header\n")
|
|
|
|
cmd = [
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--target-dir", str(nonexistent),
|
|
"--module-code", "tst",
|
|
"--module-name", "Test",
|
|
"--module-yaml", str(yaml_path),
|
|
"--module-csv", str(csv_path),
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
assert result.returncode == 2
|
|
data = json.loads(result.stdout)
|
|
assert data["status"] == "error"
|
|
|
|
|
|
def test_missing_source_file():
|
|
"""Test error when module.yaml source doesn't exist."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
target_dir = tmp / "output"
|
|
target_dir.mkdir()
|
|
|
|
# Remove the yaml after creation to simulate missing file
|
|
yaml_path = tmp / "module.yaml"
|
|
csv_path = tmp / "module-help.csv"
|
|
csv_path.write_text("header\n")
|
|
# Don't create yaml_path
|
|
|
|
cmd = [
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--target-dir", str(target_dir),
|
|
"--module-code", "tst",
|
|
"--module-name", "Test",
|
|
"--module-yaml", str(yaml_path),
|
|
"--module-csv", str(csv_path),
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
assert result.returncode == 2
|
|
data = json.loads(result.stdout)
|
|
assert data["status"] == "error"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
tests = [
|
|
test_basic_scaffold,
|
|
test_skill_md_frontmatter_substitution,
|
|
test_generated_files_written,
|
|
test_anti_zombie_replaces_existing,
|
|
test_missing_target_dir,
|
|
test_missing_source_file,
|
|
]
|
|
passed = 0
|
|
failed = 0
|
|
for test in tests:
|
|
try:
|
|
test()
|
|
print(f" PASS: {test.__name__}")
|
|
passed += 1
|
|
except AssertionError as e:
|
|
print(f" FAIL: {test.__name__}: {e}")
|
|
failed += 1
|
|
except Exception as e:
|
|
print(f" ERROR: {test.__name__}: {e}")
|
|
failed += 1
|
|
print(f"\n{passed} passed, {failed} failed")
|
|
sys.exit(1 if failed else 0)
|