fix(dashboard,worker): vérité produit P0 — dashboard+worker+VWB export
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m46s
tests / Tests unitaires (sans GPU) (push) Failing after 2m0s
tests / Tests sécurité (critique) (push) Has been skipped

War-room clôture DGX 2026-06-18 (recadrage Dom : graphe/apprentissage/mémoire/dashboard = surface produit P0).
Le dashboard et le statut worker affichaient des états faux ; corrige pour refléter la vérité du produit.

- dashboard FAISS: distingue index brut / metadata HMAC invalide / runtime / absent (plus de faux "inactif")
- dashboard process-mining: 503 explicite missing_dependency (plus de message trompeur)
- dashboard /api/workflows + system/status: lecture DB VWB v3 canonique (total réel = 24, plus de 0)
- worker /processing/status: véridique (lit _worker_health.json) + statut "idle/armé (lazy)" distinct de "dégradé (échec)"
- VWB export: N steps -> N actions/edges (dernière action n'est plus perdue)
- tests: dashboard routes, worker status truthfulness, export VWB

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-06-18 17:50:12 +02:00
parent 6d5ef51c60
commit ec1fb81054
8 changed files with 626 additions and 56 deletions

View File

@@ -1289,3 +1289,158 @@ class TestAPIEndpoints:
assert len(workflows) == 1
assert workflows[0]["workflow_id"] == "wf_api_001"
assert workflows[0]["nodes"] == 2
class TestWorkerStatusTruthfulness:
"""Truthfulness du statut worker exposé par _get_worker_queue_status.
Distingue VEILLE (armé, lazy : worker neuf qui n'a jamais traité de
session, composants chargés à la 1re session) de DÉGRADÉ (init tentée
et en échec). Un worker en veille ne doit JAMAIS être étiqueté 'degraded'.
"""
# Même contrainte que TestAPIEndpoints : api_stream fail-closed à l'import
# si RPA_API_TOKEN absent.
_TEST_API_TOKEN = "test_token_for_worker_status_0123456789abcdef"
@pytest.fixture(autouse=True)
def _ensure_api_token(self, monkeypatch):
monkeypatch.setenv("RPA_API_TOKEN", self._TEST_API_TOKEN)
@pytest.fixture
def status_env(self, tmp_path, monkeypatch):
"""Isole les fichiers worker (health/queue/lock) sur tmp_path."""
from agent_v0.server_v1 import api_stream
health_file = tmp_path / "_worker_health.json"
queue_file = tmp_path / "_worker_queue.txt"
lock_file = tmp_path / "_replay_active.lock"
monkeypatch.setattr(api_stream, "WORKER_HEALTH_FILE", health_file)
monkeypatch.setattr(api_stream, "WORKER_QUEUE_FILE", queue_file)
monkeypatch.setattr(api_stream, "REPLAY_LOCK_FILE", lock_file)
return api_stream, health_file
@staticmethod
def _write_health(health_file, **overrides):
"""Écrit un health file frais (mtime récent => non stale)."""
payload = {
"pid": 1234,
"started_at": "2026-06-18T10:00:00",
"last_cycle": "2026-06-18T10:00:30",
"current_session": None,
"queue_length": 0,
"components": {
"screen_analyzer": False,
"clip_embedder": False,
"faiss_manager": False,
"state_embedding_builder": False,
},
"stats": {
"sessions_processed": 0,
"sessions_failed": 0,
"sessions_skipped": 0,
"total_screenshots_analyzed": 0,
},
"status": "healthy",
}
payload.update(overrides)
health_file.write_text(json.dumps(payload), encoding="utf-8")
def test_fresh_worker_is_idle_not_degraded(self, status_env):
"""Worker neuf : healthy, 0 session, tous composants false
=> statut 'idle' (en veille / armé), PAS 'degraded'."""
api_stream, health_file = status_env
self._write_health(health_file) # défaut = état neuf
status = api_stream._get_worker_queue_status()
assert status["running"] is True
assert status["status"] == "idle", status
assert status["armed"] is True
assert status["components_ready"] is False
# processing_ready reste False tant que les composants ne sont pas chargés
assert status["processing_ready"] is False
assert "veille" in status["status_hint"].lower()
def test_worker_init_failed_is_degraded(self, status_env):
"""Init tentée et en échec : run_worker force status='degraded'
(VLM + ScreenAnalyzer absent) => on conserve 'degraded'."""
api_stream, health_file = status_env
self._write_health(
health_file,
status="degraded", # forcé par run_worker._write_health
components={
"screen_analyzer": False,
"clip_embedder": True,
"faiss_manager": True,
"state_embedding_builder": False,
},
stats={
"sessions_processed": 0,
"sessions_failed": 1, # une session a tenté l'init et échoué
"sessions_skipped": 0,
"total_screenshots_analyzed": 0,
},
)
status = api_stream._get_worker_queue_status()
assert status["running"] is True
assert status["status"] == "degraded", status
assert status["armed"] is False
assert status["processing_ready"] is False
assert "dégradé" in status["status_hint"].lower()
def test_worker_partial_components_after_attempt_is_degraded(self, status_env):
"""Composants partiels après tentative de traitement (sessions_failed>0),
sans status forcé par le worker => 'degraded' (pas 'idle')."""
api_stream, health_file = status_env
self._write_health(
health_file,
status="healthy",
components={
"screen_analyzer": True,
"clip_embedder": True,
"faiss_manager": False, # un composant manquant
"state_embedding_builder": True,
},
stats={
"sessions_processed": 0,
"sessions_failed": 2,
"sessions_skipped": 0,
"total_screenshots_analyzed": 0,
},
)
status = api_stream._get_worker_queue_status()
assert status["status"] == "degraded", status
assert status["armed"] is False
def test_worker_ready_after_processing_is_healthy(self, status_env):
"""Worker ayant traité au moins une session, tous composants chargés
=> 'healthy' et processing_ready=True."""
api_stream, health_file = status_env
self._write_health(
health_file,
status="healthy",
components={
"screen_analyzer": True,
"clip_embedder": True,
"faiss_manager": True,
"state_embedding_builder": True,
},
stats={
"sessions_processed": 3,
"sessions_failed": 0,
"sessions_skipped": 0,
"total_screenshots_analyzed": 42,
},
)
status = api_stream._get_worker_queue_status()
assert status["status"] == "healthy", status
assert status["armed"] is False
assert status["components_ready"] is True
assert status["processing_ready"] is True