feat: Calculer automatiquement les moyennes après chaque saisie de notes
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

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.
This commit is contained in:
2026-03-30 06:22:03 +02:00
parent b70d5ec2ad
commit b7dc27f2a5
786 changed files with 118783 additions and 316 deletions

View File

@@ -0,0 +1,223 @@
#!/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)