Refonte majeure du système Agent Chat et ajout de nombreux modules : - Chat unifié : suppression du dual Workflows/Agent Libre, tout passe par /api/chat avec résolution en 3 niveaux (workflow → geste → "montre-moi") - GestureCatalog : 38 raccourcis clavier universels Windows avec matching sémantique, substitution automatique dans les replays, et endpoint /api/gestures - Mode Copilot : exécution pas-à-pas des workflows avec validation humaine via WebSocket (approve/skip/abort) avant chaque action - Léa UI (agent_v0/lea_ui/) : interface PyQt5 pour Windows avec overlay transparent pour feedback visuel pendant le replay - Data Extraction (core/extraction/) : moteur d'extraction visuelle de données (OCR + VLM → SQLite), avec schémas YAML et export CSV/Excel - ReplayVerifier (agent_v0/server_v1/) : vérification post-action par comparaison de screenshots, avec logique de retry (max 3) - IntentParser durci : meilleur fallback regex, type GREETING, patterns améliorés - Dashboard : nouvelles pages gestures, streaming, extractions - Tests : 63 tests GestureCatalog, 47 tests extraction, corrections tests existants - Dépréciation : /api/agent/plan et /api/agent/execute retournent HTTP 410, suppression du code hardcodé _plan_to_replay_actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
255 lines
9.0 KiB
Python
255 lines
9.0 KiB
Python
"""
|
|
Tests du GraphToVisualConverter — conversion core Workflow → VWB VisualWorkflow.
|
|
|
|
Vérifie que le pont inverse (GraphBuilder → VWB) fonctionne correctement :
|
|
- Chaque WorkflowNode produit un VisualNode avec position, type, ports
|
|
- Chaque WorkflowEdge produit un VisualEdge avec source/target
|
|
- L'ordre topologique est respecté (entry → end)
|
|
- Les métadonnées visuelles (couleurs, labels) sont cohérentes
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_ROOT = str(Path(__file__).resolve().parents[2])
|
|
if _ROOT not in sys.path:
|
|
sys.path.insert(0, _ROOT)
|
|
|
|
|
|
# =========================================================================
|
|
# Fixtures
|
|
# =========================================================================
|
|
|
|
def _make_core_workflow(num_nodes=3):
|
|
"""Crée un core Workflow minimal pour les tests."""
|
|
from core.models.workflow_graph import (
|
|
Workflow,
|
|
WorkflowNode,
|
|
WorkflowEdge,
|
|
Action,
|
|
TargetSpec,
|
|
ScreenTemplate,
|
|
WindowConstraint,
|
|
TextConstraint,
|
|
UIConstraint,
|
|
EmbeddingPrototype,
|
|
EdgeConstraints,
|
|
PostConditions,
|
|
EdgeStats,
|
|
SafetyRules,
|
|
WorkflowStats,
|
|
LearningConfig,
|
|
)
|
|
|
|
nodes = []
|
|
for i in range(num_nodes):
|
|
node = WorkflowNode(
|
|
node_id=f"node_{i}",
|
|
name=f"Étape {i}",
|
|
description=f"Description nœud {i}",
|
|
template=ScreenTemplate(
|
|
window=WindowConstraint(title_pattern=f"App{i}"),
|
|
text=TextConstraint(),
|
|
ui=UIConstraint(),
|
|
embedding=EmbeddingPrototype(
|
|
provider="test",
|
|
vector_id=f"vec_{i}",
|
|
min_cosine_similarity=0.8,
|
|
sample_count=1,
|
|
),
|
|
),
|
|
is_entry=(i == 0),
|
|
is_end=(i == num_nodes - 1),
|
|
metadata={
|
|
"visual_type": "click" if i > 0 and i < num_nodes - 1 else ("start" if i == 0 else "end"),
|
|
"parameters": {"target": f"button_{i}"},
|
|
},
|
|
)
|
|
nodes.append(node)
|
|
|
|
edges = []
|
|
for i in range(num_nodes - 1):
|
|
edge = WorkflowEdge(
|
|
edge_id=f"edge_{i}_to_{i+1}",
|
|
from_node=f"node_{i}",
|
|
to_node=f"node_{i+1}",
|
|
action=Action(
|
|
type="mouse_click",
|
|
target=TargetSpec(by_text=f"button_{i}"),
|
|
),
|
|
constraints=EdgeConstraints(),
|
|
post_conditions=PostConditions(expected_node=f"node_{i+1}"),
|
|
stats=EdgeStats(),
|
|
)
|
|
edges.append(edge)
|
|
|
|
from datetime import datetime
|
|
now = datetime.now()
|
|
|
|
return Workflow(
|
|
workflow_id="test_wf_001",
|
|
name="Test Workflow",
|
|
description="Workflow de test pour conversion",
|
|
version=1,
|
|
learning_state="OBSERVATION",
|
|
created_at=now,
|
|
updated_at=now,
|
|
entry_nodes=["node_0"],
|
|
end_nodes=[f"node_{num_nodes - 1}"],
|
|
nodes=nodes,
|
|
edges=edges,
|
|
safety_rules=SafetyRules(),
|
|
stats=WorkflowStats(),
|
|
learning=LearningConfig(),
|
|
metadata={"tags": ["test"], "source": "test"},
|
|
)
|
|
|
|
|
|
# =========================================================================
|
|
# Tests
|
|
# =========================================================================
|
|
|
|
|
|
class TestGraphToVisualConverter:
|
|
"""Tests de conversion core Workflow → VisualWorkflow."""
|
|
|
|
def test_basic_conversion(self):
|
|
"""Un workflow 3 nodes se convertit sans erreur."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(3)
|
|
converter = GraphToVisualConverter()
|
|
visual = converter.convert(wf)
|
|
|
|
assert visual.id == "test_wf_001"
|
|
assert visual.name == "Test Workflow"
|
|
assert len(visual.nodes) == 3
|
|
assert len(visual.edges) == 2
|
|
|
|
def test_node_ids_preserved(self):
|
|
"""Les IDs des nodes sont préservés."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(4)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
visual_ids = {n.id for n in visual.nodes}
|
|
assert visual_ids == {"node_0", "node_1", "node_2", "node_3"}
|
|
|
|
def test_edge_source_target_preserved(self):
|
|
"""Les edges connectent les bons nodes."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(3)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
edge_pairs = [(e.source, e.target) for e in visual.edges]
|
|
assert ("node_0", "node_1") in edge_pairs
|
|
assert ("node_1", "node_2") in edge_pairs
|
|
|
|
def test_visual_types_inferred(self):
|
|
"""Les types visuels sont correctement inférés depuis les métadonnées."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(3)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
types = {n.id: n.type for n in visual.nodes}
|
|
assert types["node_0"] == "start"
|
|
assert types["node_1"] == "click"
|
|
assert types["node_2"] == "end"
|
|
|
|
def test_positions_ordered_vertically(self):
|
|
"""Les nodes sont positionnés de haut en bas."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(5)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
y_positions = [n.position.y for n in visual.nodes]
|
|
assert y_positions == sorted(y_positions), "Les nodes doivent être ordonnés verticalement"
|
|
|
|
def test_start_node_has_no_input_port(self):
|
|
"""Le node 'start' n'a pas de port d'entrée."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(3)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
start_node = [n for n in visual.nodes if n.type == "start"][0]
|
|
assert len(start_node.input_ports) == 0
|
|
assert len(start_node.output_ports) == 1
|
|
|
|
def test_end_node_has_no_output_port(self):
|
|
"""Le node 'end' n'a pas de port de sortie."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(3)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
end_node = [n for n in visual.nodes if n.type == "end"][0]
|
|
assert len(end_node.input_ports) == 1
|
|
assert len(end_node.output_ports) == 0
|
|
|
|
def test_to_dict_roundtrip(self):
|
|
"""Le VisualWorkflow produit un dict valide et reconstructible."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(3)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
d = visual.to_dict()
|
|
assert d["id"] == "test_wf_001"
|
|
assert len(d["nodes"]) == 3
|
|
assert len(d["edges"]) == 2
|
|
|
|
# Vérifier que les nodes dict ont les bons champs
|
|
node0 = d["nodes"][0]
|
|
assert "id" in node0
|
|
assert "type" in node0
|
|
assert "position" in node0
|
|
|
|
def test_large_workflow(self):
|
|
"""Un workflow de 20 nodes se convertit correctement."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(20)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
assert len(visual.nodes) == 20
|
|
assert len(visual.edges) == 19
|
|
|
|
def test_colors_assigned(self):
|
|
"""Chaque type de node a une couleur."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import GraphToVisualConverter
|
|
|
|
wf = _make_core_workflow(3)
|
|
visual = GraphToVisualConverter().convert(wf)
|
|
|
|
for node in visual.nodes:
|
|
assert node.color is not None
|
|
assert node.color.startswith("#")
|
|
|
|
def test_utility_function(self):
|
|
"""La fonction utilitaire convert_graph_to_visual fonctionne."""
|
|
sys.path.insert(0, str(Path(_ROOT) / "visual_workflow_builder" / "backend"))
|
|
from services.graph_to_visual_converter import convert_graph_to_visual
|
|
|
|
wf = _make_core_workflow(3)
|
|
visual = convert_graph_to_visual(wf)
|
|
|
|
assert visual.name == "Test Workflow"
|
|
assert len(visual.nodes) == 3
|