feat(server): store de logs clients par machine_id (push-log-DGX, brique 1)
AgentLogsStore : append/read JSONL rangés par machine_id (fichier par jour), anti path-traversal sur machine_id (entrée réseau), purge_old rétention 30j (garde-fou G4 Qwen). TDD 3/3 vert. Pas encore wired (endpoint = brique 2). refs DETTE-020 DETTE-021 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
78
tests/unit/test_agent_logs_store.py
Normal file
78
tests/unit/test_agent_logs_store.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Tests unitaires du store de logs poussés par les clients Léa (push-log-DGX).
|
||||
|
||||
Le store persiste les logs reçus du client, rangés par `machine_id`, pour
|
||||
consultation au dashboard (diagnostic des postes sans AnyDesk). Stockage
|
||||
fichier (JSONL par machine_id), rétention configurable.
|
||||
|
||||
Branche : feat/push-log-dgx — DETTE-020/021 (observabilité).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Racine projet pour les imports locaux (meme pattern que tests/integration)
|
||||
_ROOT = str(Path(__file__).resolve().parents[2])
|
||||
if _ROOT not in sys.path:
|
||||
sys.path.insert(0, _ROOT)
|
||||
|
||||
|
||||
def test_append_then_read_roundtrip(tmp_path):
|
||||
"""append() persiste un batch ; read() le restitue dans l'ordre."""
|
||||
from agent_v0.server_v1.agent_logs_store import AgentLogsStore
|
||||
|
||||
store = AgentLogsStore(base_dir=tmp_path / "agent_logs")
|
||||
entries = [
|
||||
{"ts": "2026-06-26T16:00:00", "level": "INFO",
|
||||
"logger": "agent_v1.main", "message": "demarrage"},
|
||||
{"ts": "2026-06-26T16:00:01", "level": "WARNING",
|
||||
"logger": "agent_v1.core.executor", "message": "popup detectee"},
|
||||
]
|
||||
|
||||
store.append("lea-emilie-001", entries)
|
||||
got = store.read("lea-emilie-001")
|
||||
|
||||
assert len(got) == 2
|
||||
assert got[0]["message"] == "demarrage"
|
||||
assert got[0]["level"] == "INFO"
|
||||
assert got[1]["level"] == "WARNING"
|
||||
assert got[1]["logger"] == "agent_v1.core.executor"
|
||||
|
||||
|
||||
def test_machine_id_path_traversal_stays_within_base(tmp_path):
|
||||
"""Un machine_id malveillant (entrée réseau) ne doit jamais écrire hors du base_dir."""
|
||||
from agent_v0.server_v1.agent_logs_store import AgentLogsStore
|
||||
|
||||
base = (tmp_path / "agent_logs").resolve()
|
||||
store = AgentLogsStore(base_dir=base)
|
||||
|
||||
store.append("../../../evil", [{"message": "pwn"}])
|
||||
|
||||
written = list(base.rglob("*.jsonl"))
|
||||
assert written, "le batch doit être persisté SOUS base (pas d'évasion ni perte)"
|
||||
for p in written:
|
||||
assert base in p.resolve().parents, f"{p} échappe à {base}"
|
||||
# Aucune fuite hors de base
|
||||
assert not list(tmp_path.glob("evil*"))
|
||||
|
||||
|
||||
def test_purge_old_removes_files_older_than_retention(tmp_path):
|
||||
"""purge_old() supprime les fichiers-jour antérieurs à la rétention (G4 Qwen)."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from agent_v0.server_v1.agent_logs_store import AgentLogsStore
|
||||
|
||||
base = tmp_path / "agent_logs"
|
||||
store = AgentLogsStore(base_dir=base)
|
||||
mdir = base / "lea-001"
|
||||
mdir.mkdir(parents=True)
|
||||
(mdir / "2026-05-01.jsonl").write_text('{"message": "vieux"}\n', encoding="utf-8")
|
||||
(mdir / "2026-06-26.jsonl").write_text('{"message": "recent"}\n', encoding="utf-8")
|
||||
|
||||
now = datetime(2026, 6, 26, tzinfo=timezone.utc)
|
||||
removed = store.purge_old(retention_days=30, now=now)
|
||||
|
||||
remaining = {p.name for p in mdir.glob("*.jsonl")}
|
||||
assert remaining == {"2026-06-26.jsonl"}
|
||||
assert removed == 1
|
||||
Reference in New Issue
Block a user