fix(M8): corrections d'audit FIXME_M8 — appariement, date, déterminisme, validateur

Quatre corrections bloquantes/majeures de l'audit FIXME_M8 :
- Appariement un-à-un déterministe (consommation du candidat sélectionné) ;
  1 réel / 2 théoriques → 1 REMOVED, 2 réels / 1 théorique → 1 ADDED.
- Filtrage strict par date : les cours réels hors target_date sont exclus
  du matching avec un warning logé (décision architecte : pas d'exception).
- Déterminisme des détails : formatage via sorted(set(...)) au lieu de
  set(...) brut, indépendant de PYTHONHASHSEED.
- Validateur AgendaChange strict : ADDED = lesson seule, REMOVED =
  theoretical_lesson seule, MODIFIED = les deux requis.
- Comparaison à la minute près dans _is_modified (cohérent avec _matches).
- Documentation §8.4/§8.5 alignée avec l'implémentation (tolérance 15 min,
  API compare(), normalize_subject référencé, appariement consommé).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
This commit is contained in:
2026-09-07 15:59:17 +02:00
parent d2cf59c713
commit 5907c9aeaf
7 changed files with 562 additions and 225 deletions

View File

@@ -240,7 +240,7 @@ class TestAgendaChangeConsistency:
assert instance.theoretical_lesson is not None
def test_agenda_change_modified_with_lesson_valid(self) -> None:
"""Vérifie que type=MODIFIED avec lesson=<valide> est valide."""
"""Vérifie que type=MODIFIED avec lesson et theoretical_lesson est valide."""
lesson = Lesson(
id="lesson-valid-mod",
start=datetime(2024, 9, 6, 10, 0, 0),
@@ -249,13 +249,97 @@ class TestAgendaChangeConsistency:
group=None,
content=None,
)
theoretical_lesson = TheoreticalLesson(
id="theo-lesson-valid-mod",
day_of_week=0,
start_time=time(10, 0, 0),
end_time=time(11, 30, 0),
subject="Physique",
)
instance = AgendaChange(
type=AgendaChangeType.MODIFIED,
lesson=lesson,
theoretical_lesson=None,
theoretical_lesson=theoretical_lesson,
)
assert instance.type == AgendaChangeType.MODIFIED
assert instance.lesson is not None
assert instance.theoretical_lesson is not None
def test_agenda_change_added_with_theoretical_lesson_invalid(self) -> None:
"""Vérifie que type=ADDED avec theoretical_lesson non-None lève une ValidationError."""
lesson = Lesson(
id="lesson-added-theo",
start=datetime(2024, 9, 6, 8, 0, 0),
end=datetime(2024, 9, 6, 9, 30, 0),
subject="Mathématiques",
group=None,
content=None,
)
theoretical_lesson = TheoreticalLesson(
id="theo-lesson-added",
day_of_week=0,
start_time=time(8, 0, 0),
end_time=time(9, 30, 0),
subject="Mathématiques",
)
with pytest.raises(ValidationError) as exc_info:
AgendaChange(
type=AgendaChangeType.ADDED,
lesson=lesson,
theoretical_lesson=theoretical_lesson,
)
assert any(
"theoretical_lesson doit être None pour le type" in str(error)
for error in exc_info.value.errors()
)
def test_agenda_change_removed_with_lesson_invalid(self) -> None:
"""Vérifie que type=REMOVED avec lesson non-None lève une ValidationError."""
lesson = Lesson(
id="lesson-removed",
start=datetime(2024, 9, 6, 8, 0, 0),
end=datetime(2024, 9, 6, 9, 30, 0),
subject="Mathématiques",
group=None,
content=None,
)
theoretical_lesson = TheoreticalLesson(
id="theo-lesson-removed",
day_of_week=0,
start_time=time(8, 0, 0),
end_time=time(9, 30, 0),
subject="Mathématiques",
)
with pytest.raises(ValidationError) as exc_info:
AgendaChange(
type=AgendaChangeType.REMOVED,
lesson=lesson,
theoretical_lesson=theoretical_lesson,
)
assert any(
"lesson doit être None pour le type" in str(error) for error in exc_info.value.errors()
)
def test_agenda_change_modified_without_theoretical_lesson_invalid(self) -> None:
"""Vérifie que type=MODIFIED sans theoretical_lesson lève une ValidationError."""
lesson = Lesson(
id="lesson-mod-no-theo",
start=datetime(2024, 9, 6, 10, 0, 0),
end=datetime(2024, 9, 6, 11, 30, 0),
subject="Physique",
group=None,
content=None,
)
with pytest.raises(ValidationError) as exc_info:
AgendaChange(
type=AgendaChangeType.MODIFIED,
lesson=lesson,
theoretical_lesson=None,
)
assert any(
"theoretical_lesson est requis pour le type" in str(error)
for error in exc_info.value.errors()
)
class TestCalDAVSyncResultInvariants: