chore: sauvegarde complète avant factorisation executor
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 12s
security-audit / pip-audit (CVE dépendances) (push) Successful in 10s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 12s
security-audit / pip-audit (CVE dépendances) (push) Successful in 10s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped
Point de sauvegarde incluant les fichiers non committés des sessions précédentes (systemd, docs, agents, GPU manager). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
610
tests/unit/test_env_setup.py
Normal file
610
tests/unit/test_env_setup.py
Normal file
@@ -0,0 +1,610 @@
|
||||
# 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 == []
|
||||
Reference in New Issue
Block a user