Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 14s
security-audit / pip-audit (CVE dépendances) (push) Successful in 12s
security-audit / Scan secrets (grep) (push) Successful in 9s
tests / Lint (ruff + black) (push) Successful in 15s
tests / Tests sécurité (critique) (push) Has been cancelled
tests / Tests unitaires (sans GPU) (push) Has been cancelled
Vrais bugs corrigés :
- core/execution/target_resolver.py : suppression de 5 lignes de dead code
après return (vestige de refacto incomplète référençant des params
jamais assignés à self : similarity_threshold, use_spatial_fallback)
- agent_v0/agent_v1/core/executor.py:2180 : variable `prefill` référencée
mais jamais définie. Initialisation explicite ajoutée en amont
(conditionnée sur _is_thinking_popup, cohérent avec l'append du message)
Fichier supprimé :
- core/security/input_validator_new.py : contenu corrompu (texte inversé,
artefact de copier-coller), jamais importé nulle part, 550 erreurs ruff
à lui seul
Workflow CI :
- Exclusions ajoutées pour dossiers legacy connus cassés :
- agent_v0/deploy/windows_client/ (clone obsolète)
- tests/property/ (cf. MEMORY.md — imports cassés)
- tests/integration/test_visual_rpa_checkpoint.py (VisualMetadata
inexistant, déjà documenté)
Résultat : "ruff All checks passed!" sur core/ agent_v0/ tests/
(avec E9,F63,F7,F82 — syntax + undefined critiques).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
611 lines
24 KiB
Python
611 lines
24 KiB
Python
# tests/unit/test_env_setup.py
|
||
"""
|
||
Tests unitaires pour la phase de setup environnement (pré-replay).
|
||
|
||
Vérifie que les fonctions d'extraction d'apps et de génération
|
||
d'actions de setup 100% visuelles fonctionnent correctement.
|
||
"""
|
||
import pytest
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# Ajouter le répertoire racine au path pour l'import
|
||
ROOT = Path(__file__).parent.parent.parent
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from agent_v0.server_v1.api_stream import (
|
||
_extract_required_apps_from_events,
|
||
_extract_required_apps_from_workflow,
|
||
_resolve_launch_command,
|
||
_infer_app_from_window_titles,
|
||
_generate_setup_actions,
|
||
_get_visual_search_info,
|
||
_APP_LAUNCH_COMMANDS,
|
||
_APP_VISUAL_SEARCH,
|
||
_SETUP_IGNORE_APPS,
|
||
)
|
||
|
||
|
||
# =========================================================================
|
||
# Tests pour _resolve_launch_command
|
||
# =========================================================================
|
||
|
||
class TestResolveLaunchCommand:
|
||
"""Tests pour la résolution des commandes de lancement."""
|
||
|
||
def test_known_app(self):
|
||
"""Les apps connues retournent la bonne commande."""
|
||
assert _resolve_launch_command("Notepad.exe") == "notepad"
|
||
assert _resolve_launch_command("notepad.exe") == "notepad"
|
||
|
||
def test_known_app_case_insensitive(self):
|
||
"""Le mapping est insensible à la casse."""
|
||
assert _resolve_launch_command("NOTEPAD.EXE") == "notepad"
|
||
assert _resolve_launch_command("Chrome.exe") == "chrome"
|
||
|
||
def test_unknown_app_strips_exe(self):
|
||
"""Les apps inconnues utilisent le nom sans .exe."""
|
||
assert _resolve_launch_command("MonApp.exe") == "MonApp"
|
||
assert _resolve_launch_command("customtool.exe") == "customtool"
|
||
|
||
def test_no_exe_extension(self):
|
||
"""Les noms sans .exe sont retournés tels quels."""
|
||
assert _resolve_launch_command("notepad") == "notepad"
|
||
|
||
def test_all_mapped_apps(self):
|
||
"""Toutes les apps du mapping sont résolvables."""
|
||
for app_name, expected_cmd in _APP_LAUNCH_COMMANDS.items():
|
||
assert _resolve_launch_command(app_name) == expected_cmd
|
||
|
||
|
||
# =========================================================================
|
||
# Tests pour _get_visual_search_info
|
||
# =========================================================================
|
||
|
||
class TestGetVisualSearchInfo:
|
||
"""Tests pour la résolution des infos de recherche visuelle."""
|
||
|
||
def test_known_app_notepad(self):
|
||
"""Notepad retourne les infos visuelles françaises."""
|
||
info = _get_visual_search_info("Notepad.exe")
|
||
assert info["search_text"] == "Bloc-notes"
|
||
assert info["display_name"] == "Bloc-notes"
|
||
assert "Bloc-notes" in info["vlm_description"]
|
||
|
||
def test_known_app_calc(self):
|
||
"""Calculatrice retourne les infos visuelles françaises."""
|
||
info = _get_visual_search_info("calc.exe")
|
||
assert info["search_text"] == "Calculatrice"
|
||
|
||
def test_known_app_word(self):
|
||
"""Word retourne les infos visuelles."""
|
||
info = _get_visual_search_info("winword.exe")
|
||
assert info["search_text"] == "Word"
|
||
assert info["display_name"] == "Microsoft Word"
|
||
|
||
def test_case_insensitive(self):
|
||
"""Le mapping est insensible à la casse."""
|
||
info = _get_visual_search_info("NOTEPAD.EXE")
|
||
assert info["search_text"] == "Bloc-notes"
|
||
|
||
def test_unknown_app_fallback(self):
|
||
"""Une app inconnue utilise le nom sans .exe comme fallback."""
|
||
info = _get_visual_search_info("MonApp.exe")
|
||
assert info["search_text"] == "MonApp"
|
||
assert info["display_name"] == "MonApp"
|
||
assert "MonApp" in info["vlm_description"]
|
||
|
||
def test_unknown_app_no_exe(self):
|
||
"""Une app sans .exe utilise le nom tel quel."""
|
||
info = _get_visual_search_info("myapp")
|
||
assert info["search_text"] == "myapp"
|
||
|
||
def test_all_visual_apps_have_required_keys(self):
|
||
"""Toutes les apps du mapping visuel ont les clés requises."""
|
||
for app_name, info in _APP_VISUAL_SEARCH.items():
|
||
assert "search_text" in info, f"{app_name} manque search_text"
|
||
assert "display_name" in info, f"{app_name} manque display_name"
|
||
assert "vlm_description" in info, f"{app_name} manque vlm_description"
|
||
|
||
|
||
# =========================================================================
|
||
# Tests pour _infer_app_from_window_titles
|
||
# =========================================================================
|
||
|
||
class TestInferAppFromWindowTitles:
|
||
"""Tests pour l'inférence d'app depuis les titres de fenêtres."""
|
||
|
||
def test_notepad_french(self):
|
||
"""Détecte Notepad depuis un titre français."""
|
||
app, cmd, title = _infer_app_from_window_titles(["Sans titre – Bloc-notes"])
|
||
assert app == "Notepad.exe"
|
||
assert cmd == "notepad"
|
||
assert title == "Sans titre – Bloc-notes"
|
||
|
||
def test_notepad_english(self):
|
||
"""Détecte Notepad depuis un titre anglais."""
|
||
app, cmd, title = _infer_app_from_window_titles(["Untitled - Notepad"])
|
||
assert app == "Notepad.exe"
|
||
assert cmd == "notepad"
|
||
|
||
def test_word(self):
|
||
"""Détecte Word."""
|
||
app, cmd, _ = _infer_app_from_window_titles(["Document1 - Word"])
|
||
assert app == "winword.exe"
|
||
assert cmd == "winword"
|
||
|
||
def test_excel(self):
|
||
"""Détecte Excel."""
|
||
app, cmd, _ = _infer_app_from_window_titles(["Classeur1 - Excel"])
|
||
assert app == "excel.exe"
|
||
assert cmd == "excel"
|
||
|
||
def test_chrome(self):
|
||
"""Détecte Chrome."""
|
||
app, cmd, _ = _infer_app_from_window_titles(["Google - Chrome"])
|
||
assert app == "chrome.exe"
|
||
assert cmd == "chrome"
|
||
|
||
def test_explorer_ignored(self):
|
||
"""Explorer est ignoré (app système)."""
|
||
app, cmd, _ = _infer_app_from_window_titles(["Explorateur de fichiers"])
|
||
assert app == ""
|
||
assert cmd == ""
|
||
|
||
def test_unknown_title(self):
|
||
"""Un titre inconnu retourne des chaînes vides."""
|
||
app, cmd, _ = _infer_app_from_window_titles(["Ma Super App Custom"])
|
||
assert app == ""
|
||
assert cmd == ""
|
||
|
||
def test_empty_list(self):
|
||
"""Une liste vide retourne des chaînes vides."""
|
||
app, cmd, _ = _infer_app_from_window_titles([])
|
||
assert app == ""
|
||
assert cmd == ""
|
||
|
||
def test_first_match_wins(self):
|
||
"""Le premier titre reconnu est utilisé."""
|
||
app, cmd, title = _infer_app_from_window_titles([
|
||
"Rechercher", # Pas reconnu
|
||
"*test – Bloc-notes", # Notepad
|
||
"Document1 - Excel", # Excel (pas utilisé car Notepad est trouvé avant)
|
||
])
|
||
assert app == "Notepad.exe"
|
||
assert title == "*test – Bloc-notes"
|
||
|
||
def test_returns_matched_title(self):
|
||
"""Le titre matché est celui de l'app, pas le premier de la liste."""
|
||
app, cmd, title = _infer_app_from_window_titles([
|
||
"Rechercher", # Pas reconnu
|
||
"Sans titre – Bloc-notes", # Notepad → ce titre
|
||
])
|
||
assert title == "Sans titre – Bloc-notes"
|
||
|
||
|
||
# =========================================================================
|
||
# Tests pour _extract_required_apps_from_events
|
||
# =========================================================================
|
||
|
||
class TestExtractRequiredAppsFromEvents:
|
||
"""Tests pour l'extraction d'apps depuis les événements bruts."""
|
||
|
||
def _make_events(self, focus_changes):
|
||
"""Helper : créer des événements bruts à partir de changements de focus."""
|
||
events = []
|
||
for from_info, to_info in focus_changes:
|
||
events.append({
|
||
"session_id": "test_sess",
|
||
"event": {
|
||
"type": "window_focus_change",
|
||
"from": from_info,
|
||
"to": to_info,
|
||
},
|
||
})
|
||
return events
|
||
|
||
def test_notepad_session(self):
|
||
"""Détecte Notepad comme app principale."""
|
||
events = self._make_events([
|
||
(None, {"app_name": "explorer.exe", "title": "Explorateur"}),
|
||
({"app_name": "explorer.exe"}, {"app_name": "SearchHost.exe", "title": "Rechercher"}),
|
||
({"app_name": "SearchHost.exe"}, {"app_name": "Notepad.exe", "title": "Bloc-notes"}),
|
||
({"app_name": "Notepad.exe"}, {"app_name": "Notepad.exe", "title": "Sans titre – Bloc-notes"}),
|
||
({"app_name": "Notepad.exe"}, {"app_name": "Notepad.exe", "title": "*test – Bloc-notes"}),
|
||
])
|
||
|
||
result = _extract_required_apps_from_events(events)
|
||
assert result["primary_app"] == "Notepad.exe"
|
||
assert result["primary_launch_cmd"] == "notepad"
|
||
# Le premier app hors ignorées est Notepad
|
||
assert result["first_window_title"] == "Bloc-notes"
|
||
|
||
def test_empty_events(self):
|
||
"""Pas d'événements → dict vide."""
|
||
assert _extract_required_apps_from_events([]) == {}
|
||
|
||
def test_only_system_apps(self):
|
||
"""Que des apps système → dict vide."""
|
||
events = self._make_events([
|
||
(None, {"app_name": "explorer.exe", "title": "Bureau"}),
|
||
(None, {"app_name": "SearchHost.exe", "title": "Rechercher"}),
|
||
])
|
||
assert _extract_required_apps_from_events(events) == {}
|
||
|
||
def test_multiple_apps_picks_most_frequent(self):
|
||
"""L'app la plus fréquente (hors système) est choisie."""
|
||
events = self._make_events([
|
||
(None, {"app_name": "Notepad.exe", "title": "Bloc-notes"}),
|
||
(None, {"app_name": "calc.exe", "title": "Calculatrice"}),
|
||
(None, {"app_name": "calc.exe", "title": "Calculatrice"}),
|
||
(None, {"app_name": "calc.exe", "title": "Calculatrice"}),
|
||
])
|
||
result = _extract_required_apps_from_events(events)
|
||
assert result["primary_app"] == "calc.exe"
|
||
assert result["primary_launch_cmd"] == "calc"
|
||
|
||
|
||
# =========================================================================
|
||
# Tests pour _extract_required_apps_from_workflow
|
||
# =========================================================================
|
||
|
||
class TestExtractRequiredAppsFromWorkflow:
|
||
"""Tests pour l'extraction d'apps depuis un workflow structuré."""
|
||
|
||
def test_workflow_dict_with_notepad(self):
|
||
"""Détecte Notepad depuis un workflow dict."""
|
||
workflow = {
|
||
"nodes": [
|
||
{
|
||
"node_id": "node_000",
|
||
"template": {
|
||
"window": {
|
||
"title_pattern": "Sans titre – Bloc-notes",
|
||
"title_contains": "Bloc-notes",
|
||
},
|
||
},
|
||
},
|
||
],
|
||
"edges": [],
|
||
"metadata": {
|
||
"source_session_id": "sess_test",
|
||
"machine_id": "DESKTOP-TEST",
|
||
},
|
||
}
|
||
result = _extract_required_apps_from_workflow(workflow)
|
||
assert result["primary_app"] == "Notepad.exe"
|
||
assert result["primary_launch_cmd"] == "notepad"
|
||
|
||
def test_workflow_no_recognizable_titles(self):
|
||
"""Workflow sans titres reconnaissables → dict vide."""
|
||
workflow = {
|
||
"nodes": [
|
||
{
|
||
"node_id": "node_000",
|
||
"template": {
|
||
"window": {"title_pattern": "Rechercher"},
|
||
},
|
||
},
|
||
],
|
||
"edges": [],
|
||
"metadata": {},
|
||
}
|
||
result = _extract_required_apps_from_workflow(workflow)
|
||
assert result == {}
|
||
|
||
def test_empty_workflow(self):
|
||
"""Workflow vide → dict vide."""
|
||
assert _extract_required_apps_from_workflow({"nodes": [], "edges": []}) == {}
|
||
assert _extract_required_apps_from_workflow({}) == {}
|
||
|
||
|
||
# =========================================================================
|
||
# Tests pour _generate_setup_actions — 100% visuel
|
||
# =========================================================================
|
||
|
||
class TestGenerateSetupActions:
|
||
"""Tests pour la génération des actions de setup 100% visuelles."""
|
||
|
||
def test_notepad_setup_visual(self):
|
||
"""Génère les bonnes actions visuelles pour lancer Notepad."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "Sans titre – Bloc-notes",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
|
||
# 9 actions : click_start, wait, click_search, wait, type, wait, click_result, wait, verify
|
||
assert len(actions) == 9
|
||
|
||
# Étape 1 : clic visuel sur le bouton Démarrer
|
||
assert actions[0]["type"] == "click"
|
||
assert actions[0]["visual_mode"] is True
|
||
assert actions[0]["target_spec"]["by_role"] == "start_button"
|
||
assert actions[0]["target_spec"]["by_text"] == "Démarrer"
|
||
|
||
# Étape 2 : attente menu Démarrer
|
||
assert actions[1]["type"] == "wait"
|
||
assert actions[1]["duration_ms"] == 1000
|
||
|
||
# Étape 3 : clic visuel sur la barre de recherche
|
||
assert actions[2]["type"] == "click"
|
||
assert actions[2]["visual_mode"] is True
|
||
assert actions[2]["target_spec"]["by_role"] == "search_box"
|
||
|
||
# Étape 4 : attente barre de recherche active
|
||
assert actions[3]["type"] == "wait"
|
||
assert actions[3]["duration_ms"] == 500
|
||
|
||
# Étape 5 : taper le nom visuel français
|
||
assert actions[4]["type"] == "type"
|
||
assert actions[4]["text"] == "Bloc-notes"
|
||
|
||
# Étape 6 : attente résultats
|
||
assert actions[5]["type"] == "wait"
|
||
assert actions[5]["duration_ms"] == 1200
|
||
|
||
# Étape 7 : clic visuel sur le résultat
|
||
assert actions[6]["type"] == "click"
|
||
assert actions[6]["visual_mode"] is True
|
||
assert actions[6]["target_spec"]["by_text"] == "Bloc-notes"
|
||
assert actions[6]["target_spec"]["by_role"] == "app_icon"
|
||
|
||
# Étape 8 : attente lancement (app légère = 2000ms)
|
||
assert actions[7]["type"] == "wait"
|
||
assert actions[7]["duration_ms"] == 2000
|
||
|
||
# Étape 9 : vérification visuelle
|
||
assert actions[8]["type"] == "verify_screen"
|
||
assert actions[8]["_expected_title"] == "Sans titre – Bloc-notes"
|
||
|
||
# Toutes les actions sont marquées comme phase setup
|
||
for action in actions:
|
||
assert action.get("_setup_phase") is True
|
||
|
||
def test_no_key_combo_in_setup(self):
|
||
"""AUCUNE action key_combo ne doit être générée dans le setup."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "Bloc-notes",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
key_combos = [a for a in actions if a["type"] == "key_combo"]
|
||
assert key_combos == [], (
|
||
"Le setup 100% visuel ne doit JAMAIS contenir de key_combo. "
|
||
f"Trouvé : {key_combos}"
|
||
)
|
||
|
||
def test_all_clicks_are_visual(self):
|
||
"""Tous les clics du setup doivent avoir visual_mode=True et un target_spec."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "Bloc-notes",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
clicks = [a for a in actions if a["type"] == "click"]
|
||
assert len(clicks) >= 3 # Démarrer, recherche, résultat
|
||
|
||
for click in clicks:
|
||
assert click.get("visual_mode") is True, (
|
||
f"Clic {click.get('action_id')} n'a pas visual_mode=True"
|
||
)
|
||
assert click.get("target_spec"), (
|
||
f"Clic {click.get('action_id')} n'a pas de target_spec"
|
||
)
|
||
# Chaque target_spec doit avoir by_text et by_role
|
||
spec = click["target_spec"]
|
||
assert "by_text" in spec, f"target_spec sans by_text : {spec}"
|
||
assert "by_role" in spec, f"target_spec sans by_role : {spec}"
|
||
assert "vlm_description" in spec, f"target_spec sans vlm_description : {spec}"
|
||
|
||
def test_clicks_have_fallback_coordinates(self):
|
||
"""Tous les clics visuels ont des coordonnées de fallback (x_pct, y_pct)."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "Bloc-notes",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
clicks = [a for a in actions if a["type"] == "click"]
|
||
|
||
for click in clicks:
|
||
assert "x_pct" in click, f"Clic {click['action_id']} sans x_pct"
|
||
assert "y_pct" in click, f"Clic {click['action_id']} sans y_pct"
|
||
assert isinstance(click["x_pct"], (int, float))
|
||
assert isinstance(click["y_pct"], (int, float))
|
||
|
||
def test_heavy_app_longer_wait(self):
|
||
"""Les apps lourdes ont un temps d'attente plus long."""
|
||
app_info = {
|
||
"primary_app": "winword.exe",
|
||
"primary_launch_cmd": "winword",
|
||
"first_window_title": "Document1 - Word",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
wait_action = [a for a in actions if a.get("_setup_step") == "wait_app_launch"][0]
|
||
assert wait_action["duration_ms"] == 3000
|
||
|
||
def test_light_app_shorter_wait(self):
|
||
"""Les apps légères ont un temps d'attente standard."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "Bloc-notes",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
wait_action = [a for a in actions if a.get("_setup_step") == "wait_app_launch"][0]
|
||
assert wait_action["duration_ms"] == 2000
|
||
|
||
def test_word_uses_visual_search_text(self):
|
||
"""Word utilise 'Word' comme texte de recherche visuelle, pas 'winword'."""
|
||
app_info = {
|
||
"primary_app": "winword.exe",
|
||
"primary_launch_cmd": "winword",
|
||
"first_window_title": "Document1 - Word",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
|
||
# Le type doit utiliser le texte visuel, pas la commande shell
|
||
type_action = [a for a in actions if a["type"] == "type"][0]
|
||
assert type_action["text"] == "Word"
|
||
|
||
# Le clic sur le résultat doit utiliser le display_name
|
||
click_result = [a for a in actions if a.get("_setup_step") == "click_app_result"][0]
|
||
assert click_result["target_spec"]["by_text"] == "Microsoft Word"
|
||
|
||
def test_verify_screen_present_with_title(self):
|
||
"""Un verify_screen est ajouté quand un titre de fenêtre est connu."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "Sans titre – Bloc-notes",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
verify = [a for a in actions if a.get("type") == "verify_screen"]
|
||
assert len(verify) == 1
|
||
assert verify[0]["_expected_title"] == "Sans titre – Bloc-notes"
|
||
|
||
def test_no_verify_without_title(self):
|
||
"""Pas de verify_screen si aucun titre de fenêtre n'est connu."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
verify = [a for a in actions if a.get("type") == "verify_screen"]
|
||
assert len(verify) == 0
|
||
|
||
def test_empty_app_info(self):
|
||
"""Dict vide → pas d'actions."""
|
||
assert _generate_setup_actions({}) == []
|
||
|
||
def test_system_app_ignored(self):
|
||
"""Les apps système sont ignorées."""
|
||
app_info = {
|
||
"primary_app": "explorer.exe",
|
||
"primary_launch_cmd": "explorer",
|
||
"first_window_title": "Explorateur",
|
||
}
|
||
assert _generate_setup_actions(app_info) == []
|
||
|
||
def test_no_launch_cmd(self):
|
||
"""Pas de commande de lancement → pas d'actions."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "",
|
||
"first_window_title": "Bloc-notes",
|
||
}
|
||
assert _generate_setup_actions(app_info) == []
|
||
|
||
def test_action_ids_unique(self):
|
||
"""Tous les action_id sont uniques."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "Bloc-notes",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
ids = [a["action_id"] for a in actions]
|
||
assert len(ids) == len(set(ids))
|
||
|
||
def test_custom_prefix(self):
|
||
"""Le préfixe personnalisé est utilisé dans les action_id."""
|
||
app_info = {
|
||
"primary_app": "Notepad.exe",
|
||
"primary_launch_cmd": "notepad",
|
||
"first_window_title": "Bloc-notes",
|
||
}
|
||
actions = _generate_setup_actions(app_info, setup_id_prefix="mysetup")
|
||
for action in actions:
|
||
assert "mysetup" in action["action_id"]
|
||
|
||
def test_unknown_app_uses_fallback_visual_info(self):
|
||
"""Une app inconnue utilise le nom de l'exécutable comme texte de recherche."""
|
||
app_info = {
|
||
"primary_app": "MonAppMedical.exe",
|
||
"primary_launch_cmd": "MonAppMedical",
|
||
"first_window_title": "Mon App",
|
||
}
|
||
actions = _generate_setup_actions(app_info)
|
||
|
||
# Le type doit utiliser le nom sans .exe
|
||
type_action = [a for a in actions if a["type"] == "type"][0]
|
||
assert type_action["text"] == "MonAppMedical"
|
||
|
||
|
||
# =========================================================================
|
||
# Tests d'intégration : pipeline complet events → setup visuel
|
||
# =========================================================================
|
||
|
||
class TestSetupPipeline:
|
||
"""Tests du pipeline complet : extraction + génération visuelle."""
|
||
|
||
def test_full_pipeline_from_events(self):
|
||
"""Pipeline complet depuis des événements bruts de type Notepad."""
|
||
events = [
|
||
{"event": {"type": "window_focus_change", "from": None,
|
||
"to": {"app_name": "explorer.exe", "title": "Bureau"}}},
|
||
{"event": {"type": "window_focus_change",
|
||
"from": {"app_name": "explorer.exe"},
|
||
"to": {"app_name": "Notepad.exe", "title": "Sans titre – Bloc-notes"}}},
|
||
{"event": {"type": "window_focus_change",
|
||
"from": {"app_name": "Notepad.exe"},
|
||
"to": {"app_name": "Notepad.exe", "title": "*test – Bloc-notes"}}},
|
||
]
|
||
|
||
app_info = _extract_required_apps_from_events(events)
|
||
assert app_info["primary_app"] == "Notepad.exe"
|
||
|
||
actions = _generate_setup_actions(app_info)
|
||
assert len(actions) >= 8 # Au minimum 8 actions visuelles (sans verify si pas de titre)
|
||
|
||
# Vérifier l'ordre logique 100% visuel
|
||
types = [a["type"] for a in actions]
|
||
assert types[0] == "click" # Clic Démarrer
|
||
assert types[1] == "wait" # Attente menu
|
||
assert types[2] == "click" # Clic barre de recherche
|
||
assert types[3] == "wait" # Attente barre active
|
||
assert types[4] == "type" # Taper le nom
|
||
assert types[5] == "wait" # Attente résultats
|
||
assert types[6] == "click" # Clic sur le résultat
|
||
assert types[7] == "wait" # Attente lancement
|
||
|
||
# AUCUN key_combo dans le pipeline
|
||
assert "key_combo" not in types, "Le pipeline ne doit contenir aucun key_combo"
|
||
|
||
# Le texte tapé est le nom visuel français
|
||
assert actions[4]["text"] == "Bloc-notes"
|
||
|
||
def test_full_pipeline_from_workflow(self):
|
||
"""Pipeline complet depuis un workflow structuré."""
|
||
workflow = {
|
||
"nodes": [
|
||
{"node_id": "n0", "template": {
|
||
"window": {"title_pattern": "Sans titre – Bloc-notes"},
|
||
}},
|
||
{"node_id": "n1", "template": {
|
||
"window": {"title_pattern": "*test – Bloc-notes"},
|
||
}},
|
||
],
|
||
"edges": [],
|
||
"metadata": {},
|
||
}
|
||
|
||
app_info = _extract_required_apps_from_workflow(workflow)
|
||
assert app_info["primary_app"] == "Notepad.exe"
|
||
|
||
actions = _generate_setup_actions(app_info)
|
||
assert len(actions) >= 8
|
||
|
||
# Le texte tapé doit être le nom visuel, pas la commande shell
|
||
type_action = [a for a in actions if a["type"] == "type"][0]
|
||
assert type_action["text"] == "Bloc-notes"
|
||
|
||
# Aucun key_combo
|
||
key_combos = [a for a in actions if a["type"] == "key_combo"]
|
||
assert key_combos == []
|