snapshot: WIP 5j replay reliability (B1 watchdog + dialog handlers + grounding drift)
Snapshot avant correction du blocage relance Léa (3 incidents 24h: SSH refusé, polls morts ×2). Point de rollback stable. Contenu: - agent_v1/core/executor.py: 5 patchs dialog handling (saveas drift, close_tab hotkey fallback, confirm_save Unicode apostrophe, foreground dialog recontextualization, runtime_dialog in-loop) + helpers normalize_window_hint, requires_post_verify_window_transition - agent_v1/core/grounding.py: garde drift template fix (fallback_x/y plumbed) - server_v1/replay_watchdog.py (NEW): orphan watchdog B1, scan 10s timeout 30s - server_v1/api_stream.py: dispatched_action plumbing, watchdog lifespan, metrics endpoint - server_v1/replay_engine.py: _schedule_retry préserve original_action + dispatched_action - stream_processor.py: gardes _infer_tab_switch_target (no false switch_tab on save_as dialog open) + _attach_expected_window_before - tests/integration: test_replay_watchdog.py (8 cas), test_stream_processor.py - tests/unit: test_executor_verify_window_guard.py (start_button, close_tab, runtime_dialog, post_verify, transition fallbacks) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
184
tests/unit/test_agent_finalize_replay_contract.py
Normal file
184
tests/unit/test_agent_finalize_replay_contract.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""Tests ciblés sur l'intégration agent du contrat finalize enrichi."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
_ROOT = str(Path(__file__).resolve().parents[2])
|
||||
if _ROOT not in sys.path:
|
||||
sys.path.insert(0, _ROOT)
|
||||
|
||||
|
||||
class _ImmediateThread:
|
||||
def __init__(self, target=None, args=(), kwargs=None, daemon=None):
|
||||
self._target = target
|
||||
self._args = args
|
||||
self._kwargs = kwargs or {}
|
||||
|
||||
def start(self):
|
||||
if self._target is not None:
|
||||
self._target(*self._args, **self._kwargs)
|
||||
|
||||
|
||||
class _DummyServerClient:
|
||||
_stream_base = "http://server.test:5005"
|
||||
|
||||
def __init__(self):
|
||||
self.on_connection_change = None
|
||||
|
||||
def set_on_connection_change(self, callback):
|
||||
self.on_connection_change = callback
|
||||
|
||||
def _auth_headers(self):
|
||||
return {"Authorization": "Bearer test-token"}
|
||||
|
||||
|
||||
def _install_pystray_stub():
|
||||
pystray_stub = types.ModuleType("pystray")
|
||||
|
||||
class _DummyMenu:
|
||||
SEPARATOR = object()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
class _DummyIcon:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def run(self):
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
return None
|
||||
|
||||
def update_menu(self):
|
||||
return None
|
||||
|
||||
pystray_stub.MenuItem = lambda *args, **kwargs: (args, kwargs)
|
||||
pystray_stub.Menu = _DummyMenu
|
||||
pystray_stub.Icon = _DummyIcon
|
||||
sys.modules["pystray"] = pystray_stub
|
||||
|
||||
|
||||
def _build_tray():
|
||||
_install_pystray_stub()
|
||||
|
||||
from agent_v0.agent_v1.ui.smart_tray import SmartTrayV1
|
||||
|
||||
tray = SmartTrayV1(
|
||||
on_start_callback=lambda _name: None,
|
||||
on_stop_callback=lambda: None,
|
||||
server_client=_DummyServerClient(),
|
||||
)
|
||||
tray._notifier = MagicMock()
|
||||
return tray
|
||||
|
||||
|
||||
def test_offer_finalize_replay_requires_user_consent():
|
||||
_install_pystray_stub()
|
||||
from agent_v0.agent_v1.ui import smart_tray as smart_tray_mod
|
||||
|
||||
tray = _build_tray()
|
||||
tray._launch_replay_request = MagicMock()
|
||||
|
||||
with patch.object(smart_tray_mod.threading, "Thread", _ImmediateThread), \
|
||||
patch.object(smart_tray_mod, "_ask_consent", return_value=False):
|
||||
tray.offer_finalize_replay(
|
||||
{
|
||||
"endpoint": "/api/v1/traces/stream/replay-session",
|
||||
"session_id": "sess_offer_001",
|
||||
"machine_id": "pc-offer",
|
||||
},
|
||||
"Bloc-notes",
|
||||
)
|
||||
|
||||
tray._notifier.notify.assert_called_once()
|
||||
tray._launch_replay_request.assert_not_called()
|
||||
|
||||
|
||||
def test_launch_replay_request_calls_replay_session_endpoint():
|
||||
_install_pystray_stub()
|
||||
from agent_v0.agent_v1.ui import smart_tray as smart_tray_mod
|
||||
|
||||
tray = _build_tray()
|
||||
|
||||
with patch.object(smart_tray_mod.threading, "Thread", _ImmediateThread), \
|
||||
patch("requests.post") as mock_post:
|
||||
mock_post.return_value = MagicMock(ok=True)
|
||||
tray._launch_replay_request(
|
||||
{
|
||||
"endpoint": "/api/v1/traces/stream/replay-session",
|
||||
"session_id": "sess_offer_002",
|
||||
"machine_id": "pc-replay",
|
||||
},
|
||||
"Bloc-notes",
|
||||
)
|
||||
|
||||
mock_post.assert_called_once()
|
||||
_, kwargs = mock_post.call_args
|
||||
assert kwargs["params"] == {
|
||||
"session_id": "sess_offer_002",
|
||||
"machine_id": "pc-replay",
|
||||
}
|
||||
assert kwargs["headers"] == {"Authorization": "Bearer test-token"}
|
||||
assert kwargs["allow_redirects"] is False
|
||||
|
||||
|
||||
def test_agent_finalize_result_delegates_to_tray_offer():
|
||||
from agent_v0.agent_v1.finalize_contract import dispatch_finalize_result
|
||||
|
||||
ui = MagicMock()
|
||||
|
||||
dispatch_finalize_result(
|
||||
ui,
|
||||
{
|
||||
"replay_ready": True,
|
||||
"replay_request": {
|
||||
"endpoint": "/api/v1/traces/stream/replay-session",
|
||||
"session_id": "sess_offer_003",
|
||||
"machine_id": "pc-main",
|
||||
},
|
||||
},
|
||||
"Saisie dossier",
|
||||
)
|
||||
|
||||
ui.offer_finalize_replay.assert_called_once_with(
|
||||
{
|
||||
"endpoint": "/api/v1/traces/stream/replay-session",
|
||||
"session_id": "sess_offer_003",
|
||||
"machine_id": "pc-main",
|
||||
},
|
||||
"Saisie dossier",
|
||||
)
|
||||
|
||||
|
||||
def test_agent_finalize_result_ignores_already_started_replay():
|
||||
from agent_v0.agent_v1.finalize_contract import dispatch_finalize_result
|
||||
|
||||
ui = MagicMock()
|
||||
|
||||
dispatch_finalize_result(
|
||||
ui,
|
||||
{
|
||||
"replay_ready": True,
|
||||
"replay_request": {
|
||||
"endpoint": "/api/v1/traces/stream/replay-session",
|
||||
"session_id": "sess_offer_004",
|
||||
"machine_id": "pc-main",
|
||||
},
|
||||
"replay_launch": {
|
||||
"status": "started",
|
||||
"replay": {"replay_id": "replay_sess_1234"},
|
||||
},
|
||||
},
|
||||
"Saisie dossier",
|
||||
)
|
||||
|
||||
ui.offer_finalize_replay.assert_not_called()
|
||||
Reference in New Issue
Block a user