chore(dgx): snapshot consolidation WIP pour transfert poc DGX
Regroupe le WIP non committé requis pour le clone/runtime DGX (Option A) : - api_stream.py : préflight replay + smoke santé modèles + handler 403 WP-B - de-hardcode VLM : vlm_config, gpu/*, vram_orchestrator, ollama_manager - stream_processor, semantic_matcher, agent_chat (app/planner/intent) - workflows.db (acquis ; le transfert artifacts le mettra à jour + rewrite chemins) - docs : plans DGX, benchmarks VLM/grounders, recherche SOTA, coordination 8 juin Snapshot destiné à la branche poc-dgx poussée sur Gitea pour cloner le DGX. Scan anti-secret : clean. graphify (repo embarqué) exclu. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
78
tests/unit/test_replay_preflight.py
Normal file
78
tests/unit/test_replay_preflight.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Tests Job 1 — préflight replay non destructif.
|
||||
|
||||
Vérifie les fonctions pures du préflight (`_detect_dialogs_static`,
|
||||
`_build_preflight_report`) et la garantie de non-destruction (aucune mutation des
|
||||
états replay). Le préflight prouve `commande → workflow → actions non vides →
|
||||
dialogues statiquement détectables` SANS injecter d'action ni poser de lock.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
os.environ.setdefault("RPA_AUTH_DISABLED", "true")
|
||||
|
||||
from agent_v0.server_v1 import api_stream
|
||||
|
||||
|
||||
# --- Workflow dict minimal avec un node dialogue "Enregistrer sous" ---
|
||||
def _wf_with_save_dialog():
|
||||
return {
|
||||
"name": "Bloc-notes save test",
|
||||
"nodes": [
|
||||
{"node_id": "n1", "template": {"window": {"title_contains": "Bloc-notes"}}},
|
||||
{"node_id": "n2", "template": {"window": {"title_contains": "Enregistrer sous"}}},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _wf_without_dialog():
|
||||
return {
|
||||
"name": "Explorateur simple",
|
||||
"nodes": [
|
||||
{"node_id": "n1", "template": {"window": {"title_contains": "Explorateur"}}},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
_ACTIONS = [
|
||||
{"type": "key_combo", "keys": "ctrl+s"},
|
||||
{"type": "text_input", "text": "test_lea.txt"},
|
||||
{"type": "click", "x_pct": 0.45, "y_pct": 0.61},
|
||||
]
|
||||
|
||||
|
||||
def test_detect_dialogs_static_finds_save_as():
|
||||
dialogs = api_stream._detect_dialogs_static(_wf_with_save_dialog())
|
||||
assert any("enregistrer sous" in d.lower() for d in dialogs)
|
||||
|
||||
|
||||
def test_detect_dialogs_static_empty_when_none():
|
||||
assert api_stream._detect_dialogs_static(_wf_without_dialog()) == []
|
||||
|
||||
|
||||
def test_build_preflight_report_shape():
|
||||
report = api_stream._build_preflight_report(
|
||||
_wf_with_save_dialog(), "wf_123", _ACTIONS
|
||||
)
|
||||
assert report["workflow_known"] is True
|
||||
assert report["workflow_id"] == "wf_123"
|
||||
assert report["n_actions"] == 3
|
||||
assert report["action_types"]["key_combo"] == 1
|
||||
assert report["action_types"]["click"] == 1
|
||||
assert report["non_destructive"] is True
|
||||
assert any("enregistrer sous" in d.lower() for d in report["dialogs_detected"])
|
||||
# sample_actions limité (≤3) et présent
|
||||
assert 0 < len(report["sample_actions"]) <= 3
|
||||
|
||||
|
||||
def test_build_preflight_report_does_not_mutate_replay_state():
|
||||
before_q = dict(api_stream._replay_queues)
|
||||
before_s = dict(api_stream._replay_states)
|
||||
api_stream._build_preflight_report(_wf_with_save_dialog(), "wf_123", _ACTIONS)
|
||||
assert dict(api_stream._replay_queues) == before_q
|
||||
assert dict(api_stream._replay_states) == before_s
|
||||
@@ -196,6 +196,22 @@ class TestSemanticMatcher:
|
||||
|
||||
workflows = matcher.get_all_workflows()
|
||||
assert len(workflows) == 2
|
||||
|
||||
def test_load_workflows_recursively(self, temp_workflows_dir):
|
||||
"""Les workflows appris dans des sous-dossiers machine sont visibles."""
|
||||
machine_dir = temp_workflows_dir / "DESKTOP-TEST_windows"
|
||||
machine_dir.mkdir()
|
||||
nested = {
|
||||
"name": "Bloc-notes Enregistrer",
|
||||
"description": "Workflow appris dans un sous-dossier machine",
|
||||
"tags": ["bloc-notes"],
|
||||
}
|
||||
with open(machine_dir / "notepad_save.json", "w") as f:
|
||||
json.dump(nested, f)
|
||||
|
||||
matcher = SemanticMatcher(str(temp_workflows_dir), use_llm=False)
|
||||
|
||||
assert matcher.get_workflow("notepad_save") is not None
|
||||
|
||||
def test_find_workflow_exact_match(self, temp_workflows_dir):
|
||||
"""Test matching exact."""
|
||||
@@ -221,6 +237,52 @@ class TestSemanticMatcher:
|
||||
match = matcher.find_workflow("créer une facture")
|
||||
assert match is not None
|
||||
assert "Facturation" in match.workflow_name
|
||||
|
||||
def test_find_learned_save_workflow_from_node_text(self, temp_workflows_dir):
|
||||
"""Les textes appris dans les nodes alimentent le matching sémantique."""
|
||||
workflow = {
|
||||
"name": "Tâche Bloc-notes",
|
||||
"description": "Auto-generated workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "State Pattern 0",
|
||||
"template": {
|
||||
"window": {
|
||||
"title_contains": "Sans titre – Bloc-notes",
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "State Pattern 1",
|
||||
"template": {
|
||||
"window": {
|
||||
"title_contains": "Enregistrer sous",
|
||||
},
|
||||
"text": {
|
||||
"required_texts": ["Nom du fichier", "Enregistrer"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"action": {
|
||||
"type": "click",
|
||||
"target_text": "Enregistrer",
|
||||
},
|
||||
"expected_window_title": "Enregistrer sous",
|
||||
}
|
||||
],
|
||||
}
|
||||
with open(temp_workflows_dir / "notepad_save_as.json", "w") as f:
|
||||
json.dump(workflow, f)
|
||||
|
||||
matcher = SemanticMatcher(str(temp_workflows_dir), use_llm=False)
|
||||
|
||||
match = matcher.find_workflow("sauvegarde le fichier notepad", min_confidence=0.2)
|
||||
assert match is not None
|
||||
assert match.workflow_id == "notepad_save_as"
|
||||
assert "enregistrer" in matcher.get_workflow("notepad_save_as").keywords
|
||||
|
||||
def test_extract_params(self, temp_workflows_dir):
|
||||
"""Test extraction de paramètres."""
|
||||
|
||||
Reference in New Issue
Block a user