153 lines
6.2 KiB
Python
153 lines
6.2 KiB
Python
"""Non-régression — trim du préambule redondant pour /replay-session.
|
||
|
||
Bug fixé le 2026-05-20 (cf. ``docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md``
|
||
et ``CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md``) : sur la session source
|
||
``sess_20260520T102916_066851``, le premier event raw rejoué après le
|
||
setup auto Windows était un clic intra-Notepad sur la barre d'onglets
|
||
qui basculait de ``http...txt – Bloc-notes`` vers ``Sans titre – Bloc-notes``.
|
||
Comme le setup amène déjà Notepad dans ``Sans titre``, ce clic ne
|
||
modifiait rien à l'écran → `retry_threshold`.
|
||
|
||
Ce test reproduit la chaîne complète d'``api_stream.replay-session``
|
||
côté serveur (sans HTTP) sur une fixture synthétique correspondante,
|
||
et vérifie que la première action utile post-setup est bien la
|
||
saisie de texte ``test`` — pas un clic de bascule d'onglet ``Sans titre``.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).parent.parent.parent
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
import pytest
|
||
|
||
from agent_v0.server_v1.replay_engine import ( # noqa: E402
|
||
_extract_required_apps_from_events,
|
||
_generate_setup_actions,
|
||
_trim_redundant_setup_events,
|
||
)
|
||
from agent_v0.server_v1.stream_processor import ( # noqa: E402
|
||
build_replay_from_raw_events,
|
||
)
|
||
|
||
|
||
def _make_session_events() -> list:
|
||
"""Reproduit le pattern de ``sess_20260520T102916_066851`` :
|
||
Démarrer → Rechercher → Notepad ouvre un fichier .txt → l'utilisateur
|
||
clique sur l'onglet ``Sans titre`` → tape ``test`` → Ctrl+S.
|
||
|
||
L'enregistrement initial passe par un titre non-neutre puis bascule
|
||
sur un titre neutre — c'est le scénario qui piégeait le trim."""
|
||
return [
|
||
# Démarrer
|
||
{"event": {
|
||
"type": "window_focus_change",
|
||
"to": {"app_name": "explorer.exe", "title": "Explorateur"},
|
||
}},
|
||
{"event": {
|
||
"type": "mouse_click", "pos": [50, 1430], "timestamp": 1.0,
|
||
"window": {"app_name": "explorer.exe", "title": "Explorateur"},
|
||
}},
|
||
# SearchHost
|
||
{"event": {
|
||
"type": "window_focus_change",
|
||
"to": {"app_name": "SearchHost.exe", "title": "Rechercher"},
|
||
}},
|
||
{"event": {
|
||
"type": "text_input", "text": "bloc", "timestamp": 2.0,
|
||
"window": {"app_name": "SearchHost.exe", "title": "Rechercher"},
|
||
}},
|
||
{"event": {
|
||
"type": "mouse_click", "pos": [681, 448], "timestamp": 2.5,
|
||
"window": {"app_name": "SearchHost.exe", "title": "Rechercher"},
|
||
}},
|
||
# Notepad ouvre un fichier .txt existant (non-neutre)
|
||
{"event": {
|
||
"type": "window_focus_change",
|
||
"to": {
|
||
"app_name": "Notepad.exe",
|
||
"title": "http192.168.1.408765dossier.htmlid=.txt – Bloc-notes",
|
||
},
|
||
}},
|
||
# Clic dans la barre d'onglets (y=40) → bascule vers Sans titre
|
||
{"event": {
|
||
"type": "mouse_click", "pos": [1191, 40], "timestamp": 4.0,
|
||
"window": {
|
||
"app_name": "Notepad.exe",
|
||
"title": "http192.168.1.408765dossier.htmlid=.txt – Bloc-notes",
|
||
},
|
||
"window_capture": {"click_relative": [1191, 40]},
|
||
}},
|
||
{"event": {
|
||
"type": "window_focus_change",
|
||
"to": {"app_name": "Notepad.exe", "title": "Sans titre – Bloc-notes"},
|
||
}},
|
||
# Saisie réelle de l'utilisateur — c'est la première action utile
|
||
{"event": {
|
||
"type": "text_input", "text": "test", "timestamp": 5.0,
|
||
"window": {"app_name": "Notepad.exe",
|
||
"title": "Sans titre – Bloc-notes"},
|
||
}},
|
||
]
|
||
|
||
|
||
def test_replay_session_pipeline_skips_redundant_tab_switch(tmp_path):
|
||
"""Pipeline complet replay-session : setup auto + trim + build doit
|
||
produire un replay dont la première action post-setup est la saisie
|
||
``test``, pas le clic de bascule d'onglet ``Sans titre``.
|
||
"""
|
||
raw_events = _make_session_events()
|
||
app_info = _extract_required_apps_from_events(raw_events)
|
||
|
||
# 1) Setup auto reconnaît Notepad et génère ses actions
|
||
assert app_info.get("primary_app") == "Notepad.exe"
|
||
assert app_info.get("has_neutral_window_title") is True
|
||
setup_actions = _generate_setup_actions(app_info, setup_id_prefix="setup_sess")
|
||
assert setup_actions, "le setup auto doit injecter des actions Notepad"
|
||
setup_steps = [a.get("_setup_step", "") for a in setup_actions]
|
||
assert "open_run_dialog" in setup_steps
|
||
assert "ensure_fresh_document" in setup_steps
|
||
|
||
# 2) Trim : le clic intra-Notepad redondant doit disparaître
|
||
trimmed = _trim_redundant_setup_events(raw_events, app_info)
|
||
click_titles = [
|
||
(ev.get("event") or ev).get("window", {}).get("title", "")
|
||
for ev in trimmed
|
||
if (ev.get("event") or ev).get("type") == "mouse_click"
|
||
]
|
||
assert not any(
|
||
"http192.168.1.40" in t for t in click_titles
|
||
), "le clic intra-Notepad redondant doit être coupé par le trim"
|
||
|
||
# 3) Build replay propre : la première action utile post-trim est
|
||
# la saisie 'test' — pas un click "Sans titre" issu de
|
||
# _infer_tab_switch_target.
|
||
actions = build_replay_from_raw_events(
|
||
trimmed, session_id="sess_synthetic", session_dir=str(tmp_path),
|
||
)
|
||
actionable = [a for a in actions if a.get("type") in ("click", "type", "key_combo")]
|
||
assert actionable, "le replay doit contenir au moins une action utile"
|
||
|
||
first = actionable[0]
|
||
assert first.get("type") == "type", (
|
||
f"première action utile doit être 'type', pas '{first.get('type')}' "
|
||
f"(target_spec={first.get('target_spec')})"
|
||
)
|
||
assert first.get("text") == "test"
|
||
|
||
# Sanity : aucune action click ne doit cibler "Sans titre" (= la
|
||
# bascule d'onglet inférée par _infer_tab_switch_target) dans le
|
||
# replay nettoyé.
|
||
sans_titre_clicks = [
|
||
a for a in actions
|
||
if a.get("type") == "click"
|
||
and a.get("target_spec", {}).get("by_text", "").strip().lower() == "sans titre"
|
||
]
|
||
assert not sans_titre_clicks, (
|
||
"le replay ne doit plus contenir de click ciblant 'Sans titre' "
|
||
f"(trouvés : {sans_titre_clicks})"
|
||
)
|