fix(dashboard,worker): vérité produit P0 — dashboard+worker+VWB export
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user