fix(dashboard): DETTE-024 — download fleet, fallback legacy rendu visible

_resolve_lea_zip_template() reste résolu à la volée (full buildé après démarrage OK) ;
ajout d'un WARNING explicite quand le full est absent et qu'on retombe sur le ZIP
léger non autoportant (plus de fallback silencieux). Fonction injectable pour tests.
4 tests + 32 non-régression verts. refs DETTE-024

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-06-30 16:20:28 +02:00
parent 3ed9798f06
commit a50057d499
2 changed files with 114 additions and 5 deletions

View File

@@ -0,0 +1,75 @@
"""Tests unitaires pour _resolve_lea_zip_template (DETTE-024).
La fonction est injectable (full_path, legacy_path en paramètres)
→ testable sans instancier Flask ni lire le vrai deploy/.
Pattern anti-DETTE-013 : os.environ.setdefault avant l'import du module.
"""
import os
os.environ.setdefault("DASHBOARD_AUTH_DISABLED", "true")
import pytest # noqa: E402
from web_dashboard.app import _resolve_lea_zip_template # noqa: E402
class TestResolveLéaZipTemplate:
"""DETTE-024 — sélection du ZIP template pour le download fleet."""
def test_full_present_retourne_full(self, tmp_path):
"""Si le ZIP complet autoportant est présent, il est retourné."""
full = tmp_path / "Lea_full_v1.0.1.zip"
legacy = tmp_path / "Lea_v1.0.0.zip"
full.write_bytes(b"full-stub")
legacy.write_bytes(b"legacy-stub")
result = _resolve_lea_zip_template(full_path=full, legacy_path=legacy)
assert result == full, f"Attendu full ({full}), obtenu {result}"
def test_full_absent_retourne_legacy_avec_warning(self, tmp_path, caplog):
"""Si le ZIP complet est absent, le legacy est retourné + WARNING loggué.
Le WARNING est le signal observable en production (DETTE-024) :
sans lui, le fallback silencieux rendait le problème invisible.
"""
import logging
full = tmp_path / "Lea_full_v1.0.1.zip"
legacy = tmp_path / "Lea_v1.0.0.zip"
# full intentionnellement absent
legacy.write_bytes(b"legacy-stub")
with caplog.at_level(logging.WARNING):
result = _resolve_lea_zip_template(full_path=full, legacy_path=legacy)
assert result == legacy, f"Attendu legacy ({legacy}), obtenu {result}"
# Le WARNING DETTE-024 doit apparaître dans les logs
assert any(
"DETTE-024" in record.message for record in caplog.records
), (
"Un WARNING DETTE-024 doit être émis quand le ZIP complet est absent "
f"(logs: {[r.message for r in caplog.records]})"
)
def test_full_et_legacy_absents_retourne_none(self, tmp_path):
"""Si aucun ZIP n'existe, retourne None (la route renvoie 500)."""
full = tmp_path / "Lea_full_v1.0.1.zip"
legacy = tmp_path / "Lea_v1.0.0.zip"
# aucun des deux créés
result = _resolve_lea_zip_template(full_path=full, legacy_path=legacy)
assert result is None, f"Attendu None, obtenu {result}"
def test_full_prime_sur_legacy(self, tmp_path):
"""Le full est retourné même si le legacy existe aussi (priorité correcte)."""
full = tmp_path / "Lea_full_v1.0.1.zip"
legacy = tmp_path / "Lea_v1.0.0.zip"
full.write_bytes(b"full-stub")
legacy.write_bytes(b"legacy-stub")
result = _resolve_lea_zip_template(full_path=full, legacy_path=legacy)
assert result == full
assert result != legacy

View File

@@ -2231,16 +2231,37 @@ _LEA_ZIP_TEMPLATE_FULL = BASE_PATH / "deploy" / "build" / "Lea_full_v1.0.1.zip"
_LEA_ZIP_TEMPLATE_LEGACY = BASE_PATH / "deploy" / "Lea_v1.0.0.zip" _LEA_ZIP_TEMPLATE_LEGACY = BASE_PATH / "deploy" / "Lea_v1.0.0.zip"
def _resolve_lea_zip_template(): def _resolve_lea_zip_template(
full_path: Path = _LEA_ZIP_TEMPLATE_FULL,
legacy_path: Path = _LEA_ZIP_TEMPLATE_LEGACY,
) -> "Path | None":
"""Résout le ZIP à servir, à la volée (le complet peut être buildé """Résout le ZIP à servir, à la volée (le complet peut être buildé
après le démarrage du dashboard). Préfère le ZIP complet autoportant ; après le démarrage du dashboard). Préfère le ZIP complet autoportant ;
retombe sur l'ancien ZIP léger uniquement s'il existe. retombe sur l'ancien ZIP léger uniquement s'il existe.
Retourne None si aucun template n'est présent. Retourne None si aucun template n'est présent.
Les paramètres full_path/legacy_path sont injectables pour les tests
(évite de démarrer Flask — DETTE-013).
⚠️ DETTE-024 : si le ZIP complet est absent, un avertissement est loggué
explicitement pour ne pas masquer silencieusement l'absence du full.
""" """
if _LEA_ZIP_TEMPLATE_FULL.exists(): if full_path.exists():
return _LEA_ZIP_TEMPLATE_FULL return full_path
if _LEA_ZIP_TEMPLATE_LEGACY.exists(): # Full absent → fallback sur le legacy, mais log d'avertissement obligatoire.
return _LEA_ZIP_TEMPLATE_LEGACY if legacy_path.exists():
try:
api_logger.warning(
"DETTE-024 — ZIP Léa complet autoportant ABSENT (%s) ; "
"fallback sur ZIP léger NON autoportant (%s). "
"Le poste recevra un ZIP sans Python embarqué → non installable "
"sans Python système. Exécuter deploy/build_package_full.sh.",
full_path,
legacy_path,
)
except Exception:
pass # api_logger pas encore initialisé au module load (import tardif ok)
return legacy_path
return None return None
@@ -2389,8 +2410,14 @@ def download_agent_package(machine_id):
# Sécurité : l'auth Basic est déjà gérée par before_request # Sécurité : l'auth Basic est déjà gérée par before_request
# 1. Résoudre + vérifier que le ZIP template existe (à la volée) # 1. Résoudre + vérifier que le ZIP template existe (à la volée)
# _resolve_lea_zip_template() logue un WARNING si le full est absent (DETTE-024).
zip_template = _resolve_lea_zip_template() zip_template = _resolve_lea_zip_template()
if zip_template is None: if zip_template is None:
api_logger.error(
"download_agent_package(%s) — aucun ZIP template présent. "
"full=%s legacy=%s",
machine_id, _LEA_ZIP_TEMPLATE_FULL, _LEA_ZIP_TEMPLATE_LEGACY,
)
return jsonify({ return jsonify({
'error': 'ZIP template introuvable', 'error': 'ZIP template introuvable',
'detail': ( 'detail': (
@@ -2399,6 +2426,13 @@ def download_agent_package(machine_id):
'autoportant) ou deploy/build_package.sh (ZIP léger).' 'autoportant) ou deploy/build_package.sh (ZIP léger).'
), ),
}), 500 }), 500
is_full = (zip_template == _LEA_ZIP_TEMPLATE_FULL)
zip_kind = "full-autoportant" if is_full else "legacy-léger⚠"
api_logger.info(
"download_agent_package(%s) — ZIP sélectionné : %s (%s, %d Ko)",
machine_id, zip_template.name, zip_kind,
zip_template.stat().st_size // 1024,
)
# 2. Vérifier que le machine_id est enregistré # 2. Vérifier que le machine_id est enregistré
agent = _fetch_fleet_agent(machine_id) agent = _fetch_fleet_agent(machine_id)