Files
rpa_vision_v3/tests/unit/test_workflow_components.py
Dom 6d34b3cb68
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m44s
tests / Tests unitaires (sans GPU) (push) Failing after 1m49s
tests / Tests sécurité (critique) (push) Has been skipped
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>
2026-06-08 16:33:58 +02:00

403 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Tests unitaires pour les composants de workflow
Teste:
- VariableManager : Gestion des variables
- SemanticMatcher : Matching sémantique
"""
import pytest
import json
import tempfile
from pathlib import Path
from core.workflow import (
VariableManager,
VariableDefinition,
SemanticMatcher,
WorkflowMatch,
create_variable_manager_from_workflow
)
# =============================================================================
# Tests VariableManager
# =============================================================================
class TestVariableManager:
"""Tests pour VariableManager."""
def test_define_variable(self):
"""Test définition de variable."""
vm = VariableManager()
vm.define_variable("client", "Nom du client", required=True)
definitions = vm.get_definitions()
assert "client" in definitions
assert definitions["client"].required is True
def test_set_and_get_variable(self):
"""Test set/get de variable."""
vm = VariableManager()
vm.set_variable("name", "John")
assert vm.get_variable("name") == "John"
assert vm.get_variable("unknown") is None
assert vm.get_variable("unknown", "default") == "default"
def test_substitute_simple(self):
"""Test substitution simple."""
vm = VariableManager()
vm.set_variable("client", "Acme")
result = vm.substitute("Facturer {{client}}")
assert result == "Facturer Acme"
def test_substitute_with_default(self):
"""Test substitution avec valeur par défaut."""
vm = VariableManager()
result = vm.substitute("Montant: {{montant|0}} euros")
assert result == "Montant: 0 euros"
def test_substitute_missing_variable(self):
"""Test substitution avec variable manquante."""
vm = VariableManager()
result = vm.substitute("Client: {{client}}")
# Variable non définie reste telle quelle
assert "{{client}}" in result
def test_substitute_dict(self):
"""Test substitution dans un dictionnaire."""
vm = VariableManager()
vm.set_variable("client", "Acme")
vm.set_variable("montant", "1000")
data = {
"name": "Facture {{client}}",
"amount": "{{montant}}",
"nested": {
"description": "Pour {{client}}"
}
}
result = vm.substitute_dict(data)
assert result["name"] == "Facture Acme"
assert result["amount"] == "1000"
assert result["nested"]["description"] == "Pour Acme"
def test_extract_variables(self):
"""Test extraction de variables."""
vm = VariableManager()
text = "Facturer {{client}} pour {{montant}} euros"
variables = vm.extract_variables(text)
assert "client" in variables
assert "montant" in variables
assert len(variables) == 2
def test_validation_required(self):
"""Test validation des variables requises."""
vm = VariableManager()
vm.define_variable("client", required=True)
vm.define_variable("optional", required=False)
# Sans valeur pour client
errors = vm.validate()
assert len(errors) == 1
assert "client" in errors[0]
# Avec valeur pour client
vm.set_variable("client", "Acme")
errors = vm.validate()
assert len(errors) == 0
def test_validation_with_default(self):
"""Test validation avec valeur par défaut."""
vm = VariableManager()
vm.define_variable("montant", required=True, default_value="0")
# La valeur par défaut satisfait la validation
errors = vm.validate()
assert len(errors) == 0
def test_type_conversion(self):
"""Test conversion de type."""
vm = VariableManager()
vm.define_variable("count", var_type="integer")
vm.define_variable("active", var_type="boolean")
vm.set_variable("count", "42")
vm.set_variable("active", "true")
assert vm.get_variable("count") == 42
assert vm.get_variable("active") is True
def test_serialization(self):
"""Test sérialisation/désérialisation."""
vm = VariableManager()
vm.define_variable("client", "Nom", required=True)
vm.set_variable("client", "Acme")
# Sérialiser
data = vm.to_dict()
# Désérialiser
vm2 = VariableManager.from_dict(data)
assert vm2.get_variable("client") == "Acme"
assert "client" in vm2.get_definitions()
# =============================================================================
# Tests SemanticMatcher
# =============================================================================
class TestSemanticMatcher:
"""Tests pour SemanticMatcher."""
@pytest.fixture
def temp_workflows_dir(self):
"""Créer un répertoire temporaire avec des workflows de test."""
with tempfile.TemporaryDirectory() as tmpdir:
workflows_dir = Path(tmpdir)
# Créer workflow de facturation
facturation = {
"name": "Facturation Client",
"description": "Créer une facture pour un client",
"tags": ["facturer", "facture", "client", "invoice"],
"param_patterns": [
"(?:client|customer)\\s+(?P<client>[A-Za-z0-9_\\-]+)"
]
}
with open(workflows_dir / "facturation.json", "w") as f:
json.dump(facturation, f)
# Créer workflow d'export
export = {
"name": "Export Rapport",
"description": "Exporter un rapport",
"tags": ["export", "rapport", "pdf", "excel"],
"param_patterns": [
"(?:format|en)\\s+(?P<format>pdf|excel)"
]
}
with open(workflows_dir / "export.json", "w") as f:
json.dump(export, f)
yield workflows_dir
def test_load_workflows(self, temp_workflows_dir):
"""Test chargement des workflows."""
matcher = SemanticMatcher(str(temp_workflows_dir))
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."""
matcher = SemanticMatcher(str(temp_workflows_dir))
match = matcher.find_workflow("Facturation Client")
assert match is not None
assert match.workflow_name == "Facturation Client"
assert match.confidence > 0.5
def test_find_workflow_by_tag(self, temp_workflows_dir):
"""Test matching par tag."""
matcher = SemanticMatcher(str(temp_workflows_dir))
match = matcher.find_workflow("facturer quelque chose")
assert match is not None
assert "Facturation" in match.workflow_name
def test_find_workflow_by_keywords(self, temp_workflows_dir):
"""Test matching par mots-clés."""
matcher = SemanticMatcher(str(temp_workflows_dir))
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."""
matcher = SemanticMatcher(str(temp_workflows_dir))
match = matcher.find_workflow("facturer client Acme")
assert match is not None
assert "client" in match.extracted_params
assert match.extracted_params["client"].lower() == "acme"
def test_extract_range_params(self, temp_workflows_dir):
"""Test extraction de paramètres de plage."""
matcher = SemanticMatcher(str(temp_workflows_dir))
match = matcher.find_workflow("facturer de A à Z")
assert match is not None
assert "start" in match.extracted_params
assert "end" in match.extracted_params
def test_find_multiple_workflows(self, temp_workflows_dir):
"""Test recherche de plusieurs workflows."""
matcher = SemanticMatcher(str(temp_workflows_dir))
matches = matcher.find_workflows("rapport", limit=5)
assert len(matches) >= 1
assert any("Export" in m.workflow_name for m in matches)
def test_min_confidence_filter(self, temp_workflows_dir):
"""Test filtre de confiance minimale."""
matcher = SemanticMatcher(str(temp_workflows_dir))
# Avec confiance élevée, moins de résultats
matches_high = matcher.find_workflows("xyz random", min_confidence=0.8)
matches_low = matcher.find_workflows("xyz random", min_confidence=0.1)
assert len(matches_high) <= len(matches_low)
def test_suggest_commands(self, temp_workflows_dir):
"""Test suggestions de commandes."""
matcher = SemanticMatcher(str(temp_workflows_dir))
suggestions = matcher.suggest_commands("Fact")
assert len(suggestions) > 0
assert any("Facturation" in s for s in suggestions)
def test_get_workflow_help(self, temp_workflows_dir):
"""Test aide pour un workflow."""
matcher = SemanticMatcher(str(temp_workflows_dir))
help_text = matcher.get_workflow_help("facturation")
assert "Facturation Client" in help_text
assert "Tags" in help_text
# =============================================================================
# Tests d'intégration
# =============================================================================
class TestWorkflowIntegration:
"""Tests d'intégration VariableManager + SemanticMatcher."""
@pytest.fixture
def temp_workflows_dir(self):
"""Créer un répertoire temporaire avec un workflow paramétré."""
with tempfile.TemporaryDirectory() as tmpdir:
workflows_dir = Path(tmpdir)
workflow = {
"name": "Facturation",
"description": "Facturer {{client}} pour {{montant}} euros",
"tags": ["facturer"],
"variables": [
{"name": "client", "required": True},
{"name": "montant", "required": False, "default_value": "0"}
],
"edges": [
{
"action": {
"type": "text_input",
"parameters": {"text": "{{client}}"}
}
}
]
}
with open(workflows_dir / "facturation.json", "w") as f:
json.dump(workflow, f)
yield workflows_dir
def test_full_workflow_execution(self, temp_workflows_dir):
"""Test exécution complète avec variables."""
# 1. Trouver le workflow
matcher = SemanticMatcher(str(temp_workflows_dir))
match = matcher.find_workflow("facturer client Acme")
assert match is not None
# 2. Charger le workflow
with open(match.workflow_path) as f:
workflow_data = json.load(f)
# 3. Créer le VariableManager
vm = create_variable_manager_from_workflow(workflow_data)
# 4. Injecter les paramètres extraits
vm.set_variables(match.extracted_params)
# 5. Substituer les variables
result = vm.substitute_dict(workflow_data)
# Vérifier la substitution (le matcher normalise en minuscules)
assert "acme" in result["description"].lower()
assert result["edges"][0]["action"]["parameters"]["text"].lower() == "acme"
if __name__ == "__main__":
pytest.main([__file__, "-v"])