fix(backend,ci): repare trois angles morts de la surveillance de derive
Airflow / Construction de l'image (push) Successful in 1m2s
Backend / Analyse statique de sécurité (push) Successful in 7s
Backend / Tests exigeant une base (push) Failing after 5m3s
Airflow / Lint et intégrité des DAGs (push) Successful in 9m41s
ML / Analyse statique de sécurité (push) Successful in 6s
Backend / Lint, typage et tests (push) Successful in 10m10s
Backend / Audit des dépendances (push) Successful in 9m36s
ML / ML - DB et chaîne ML - DB - API (push) Failing after 5m6s
SonarQube / test-ml (push) Failing after 6m6s
ML / Lint, typage et tests (push) Successful in 11m31s
SonarQube / build-front (push) Successful in 9m49s
SonarQube / build-back (push) Successful in 9m54s
SonarQube / test-front (push) Failing after 5m10s
SonarQube / test-back (push) Failing after 5m13s
SonarQube / SonarQube (push) Skipped

Revue de la branche : trois defauts empechaient la surveillance de tenir ce qu'elle annonce.

- `evaluate()` gardait les microsecondes de `now()` dans `window_end`, la cle de
  `uq_drift_report_window`. Deux executions ne collidaient donc jamais et l'index ne
  dedoublonnait rien, contrairement a ce qu'affirmaient l'ADR 0011, 20-backend et le docstring
  du DAG. L'instant de reference est desormais tronque a l'heure.
- Un site qui cessait d'etre score disparaissait du rapport : la liste des sites ne venait que
  de la fenetre recente. La panne que cette surveillance existe pour dire etait exactement
  celle qu'elle taisait. La fenetre de reference entre maintenant dans l'union, et le site
  recoit sa ligne `indetermine` a zero observation.
- Sans fenetre de reference, `_plafond` rendait `None` et le verdict tombait sur `stable`, une
  affirmation que la donnee ne portait pas. C'est `indetermine` desormais.

`ml.yml` ecoute `apps/backend/app/**` et non les seuls modeles : ce workflow est le seul a
jouer `-m chaine`, or la chaine traverse les endpoints, les services et les schemas jusqu'a
`GET /predictions`. Une PR touchant `predictions.py` ne declenchait pas le test qui l'assert.

Hygiene de tests : le nettoyage des fixtures API connait `drift_report` (cle etrangere RESTRICT
vers `site`), le test sans rapport rend ses overrides en teardown, `test_chaine_ml_api` compare
les `created_at` strictement (un `>=` passait aussi quand l'API resservait la premiere ligne),
et `test_data_integration` filtre sur le site seme au lieu de juger tout le contenu d'une
fenetre dans une base partagee.

Docs remises d'aplomb : sept revisions Alembic et non six, `derive.py` dans l'inventaire de
etl/README, et le diagramme de 20-backend gagne DriftService, le depot drift et sa treizieme
table.
This commit is contained in:
Johan LEROY
2026-09-22 14:58:34 +02:00
parent cb961ec2c5
commit d506e8f7ef
13 changed files with 131 additions and 43 deletions
+12 -6
View File
@@ -16,6 +16,12 @@ from tests.conftest import ANCRAGE, insere_lecture, insere_lectures, insere_site
pytestmark = pytest.mark.integration
def du_site(frame: pd.DataFrame, site_id: str) -> pd.DataFrame:
"""Piege : les chargeurs ne filtrent pas par site, et `enervision_test` est partagee avec
les tests qui valident leurs ecritures. Juger le contenu de toute la fenetre les couplerait."""
return frame[frame["site_id"] == site_id].reset_index(drop=True)
def test_load_from_database_returns_the_nine_contract_columns(connexion_ml: Connection) -> None:
site_id = insere_site(connexion_ml)
insere_lectures(connexion_ml, site_id, heures=3, fin=ANCRAGE)
@@ -49,7 +55,7 @@ def test_load_recent_from_database_excludes_readings_before_the_since_bound(
connexion_ml, since=ANCRAGE - timedelta(hours=2), until=ANCRAGE
)
assert list(frame["timestamp"]) == [
assert list(du_site(frame, site_id)["timestamp"]) == [
ANCRAGE - timedelta(hours=2),
ANCRAGE - timedelta(hours=1),
ANCRAGE,
@@ -66,7 +72,7 @@ def test_load_recent_from_database_includes_a_reading_exactly_at_the_since_bound
connexion_ml, since=ANCRAGE, until=ANCRAGE + timedelta(hours=3)
)
assert len(frame) == 1
assert len(du_site(frame, site_id)) == 1
def test_load_recent_from_database_keeps_timestamps_timezone_aware(
@@ -93,7 +99,7 @@ def test_load_recent_from_database_orders_readings_by_site_then_timestamp(
connexion_ml, since=ANCRAGE, until=ANCRAGE + timedelta(hours=3)
)
assert list(frame["timestamp"]) == [
assert list(du_site(frame, site_id)["timestamp"]) == [
ANCRAGE,
ANCRAGE + timedelta(hours=1),
ANCRAGE + timedelta(hours=2),
@@ -122,7 +128,7 @@ def test_load_recent_from_database_types_a_fully_null_capacity_kw_as_float64(
)
assert frame["capacity_kw"].dtype == "float64"
assert frame["capacity_kw"].isna().all()
assert du_site(frame, site_id)["capacity_kw"].isna().all()
def test_load_recent_from_database_types_a_null_is_working_hours_as_float64(
@@ -139,7 +145,7 @@ def test_load_recent_from_database_types_a_null_is_working_hours_as_float64(
)
assert frame["is_working_hours"].dtype == "float64"
assert list(frame["is_working_hours"].isna()) == [True, False]
assert list(du_site(frame, site_id)["is_working_hours"].isna()) == [True, False]
def test_both_loaders_produce_the_same_columns_in_the_same_order(
@@ -180,4 +186,4 @@ def test_load_recent_from_database_excludes_readings_after_the_until_bound(
connexion_ml, since=ANCRAGE - timedelta(days=1), until=ANCRAGE
)
assert list(frame["timestamp"]) == [ANCRAGE]
assert list(du_site(frame, site_id)["timestamp"]) == [ANCRAGE]