setup_logging() branche un TimedRotatingFileHandler vers LOG_FILE (rotation quotidienne + rétention 180j, Règlement IA Art.12) + console. Sous pythonw (sans console), basicConfig->stderr était perdu => diagnostic terrain aveugle. main.py appelle setup_logging au démarrage, avec fallback console si le fichier est indisponible (ne jamais empêcher Léa de démarrer). TDD: tests/unit/test_agent_v1_logging.py (3 tests RED->GREEN ; module chargé par chemin pour éviter les imports lourds DETTE-011/013). py_compile main.py OK. refs DETTE-021 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
2.8 KiB
Python
75 lines
2.8 KiB
Python
"""TDD — DETTE-021 : journalisation client Léa effective (vers fichier).
|
|
|
|
Aujourd'hui `LOG_FILE` est défini (`agent_v0/agent_v1/config.py`) mais jamais
|
|
branché ; `basicConfig` écrit sur stderr — perdu car Léa tourne en `pythonw.exe`
|
|
(sans console). On veut une fonction `setup_logging()` qui branche un handler
|
|
FICHIER avec rotation quotidienne + rétention (Règlement IA Art. 12, 180 j).
|
|
|
|
Le module est chargé par chemin (importlib) pour ne dépendre d'aucun import
|
|
lourd du package client (cf. DETTE-011/013).
|
|
"""
|
|
import importlib.util
|
|
import logging
|
|
from logging.handlers import TimedRotatingFileHandler
|
|
from pathlib import Path
|
|
|
|
_MOD_PATH = Path(__file__).resolve().parents[2] / "agent_v0" / "agent_v1" / "logging_setup.py"
|
|
|
|
|
|
def _load_setup_logging():
|
|
spec = importlib.util.spec_from_file_location("lea_logging_setup", _MOD_PATH)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod.setup_logging
|
|
|
|
|
|
def _cleanup_root():
|
|
root = logging.getLogger()
|
|
for h in list(root.handlers):
|
|
if getattr(h, "_lea_managed", False):
|
|
h.close()
|
|
root.removeHandler(h)
|
|
|
|
|
|
def test_setup_logging_ecrit_dans_le_fichier(tmp_path):
|
|
"""Les logs doivent atterrir dans LOG_FILE (et plus seulement sur stderr)."""
|
|
log_file = tmp_path / "agent_v1.log"
|
|
setup_logging = _load_setup_logging()
|
|
try:
|
|
setup_logging(log_file=log_file, level=logging.INFO)
|
|
logging.getLogger("lea.test").info("message de diagnostic")
|
|
for h in logging.getLogger().handlers:
|
|
h.flush()
|
|
assert log_file.exists(), "le fichier de log doit être créé"
|
|
assert "message de diagnostic" in log_file.read_text(encoding="utf-8")
|
|
finally:
|
|
_cleanup_root()
|
|
|
|
|
|
def test_setup_logging_rotation_et_retention(tmp_path):
|
|
"""Rotation quotidienne + rétention configurable (180 j par défaut — Art. 12)."""
|
|
log_file = tmp_path / "agent_v1.log"
|
|
setup_logging = _load_setup_logging()
|
|
try:
|
|
setup_logging(log_file=log_file, retention_days=180)
|
|
handlers = [h for h in logging.getLogger().handlers
|
|
if isinstance(h, TimedRotatingFileHandler)]
|
|
assert handlers, "un TimedRotatingFileHandler doit être branché"
|
|
assert handlers[0].backupCount == 180
|
|
finally:
|
|
_cleanup_root()
|
|
|
|
|
|
def test_setup_logging_idempotent(tmp_path):
|
|
"""Appels répétés n'empilent pas les handlers fichier (pas de doublon)."""
|
|
log_file = tmp_path / "agent_v1.log"
|
|
setup_logging = _load_setup_logging()
|
|
try:
|
|
setup_logging(log_file=log_file)
|
|
setup_logging(log_file=log_file)
|
|
file_handlers = [h for h in logging.getLogger().handlers
|
|
if isinstance(h, TimedRotatingFileHandler)]
|
|
assert len(file_handlers) == 1, "pas de handler fichier en double"
|
|
finally:
|
|
_cleanup_root()
|