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>
104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
"""Tests pour les contrôles HTTP de replay paused (resume/abort).
|
|
|
|
Ces appels sont le fallback du chemin SocketIO `lea:replay_resume`
|
|
/ `lea:replay_abort` quand le bus feedback est déconnecté au moment
|
|
où l'utilisateur clique dans la bulle paused (cf.
|
|
`docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).parent.parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from agent_v0.lea_ui.server_client import LeaServerClient # noqa: E402
|
|
|
|
|
|
# Préfixe partagé pour comparer les URLs sans coller à la valeur de
|
|
# RPA_STREAMING_URL côté env d'exécution des tests.
|
|
RESUME_PATH = "/traces/stream/replay/replay_xyz/resume"
|
|
CANCEL_PATH = "/traces/stream/replay/replay_xyz/cancel"
|
|
|
|
|
|
@pytest.fixture
|
|
def client(monkeypatch):
|
|
monkeypatch.setenv("RPA_API_TOKEN", "tok-test-1234")
|
|
c = LeaServerClient()
|
|
return c
|
|
|
|
|
|
# =========================================================================
|
|
# resume_replay
|
|
# =========================================================================
|
|
|
|
|
|
class TestResumeReplay:
|
|
def test_returns_true_when_server_accepts(self, client):
|
|
resp = MagicMock(ok=True)
|
|
with patch("requests.post", return_value=resp) as post:
|
|
assert client.resume_replay("replay_xyz") is True
|
|
assert post.call_count == 1
|
|
|
|
def test_returns_false_when_server_rejects(self, client):
|
|
resp = MagicMock(ok=False)
|
|
with patch("requests.post", return_value=resp):
|
|
assert client.resume_replay("replay_xyz") is False
|
|
|
|
def test_returns_false_on_empty_replay_id(self, client):
|
|
with patch("requests.post") as post:
|
|
assert client.resume_replay("") is False
|
|
post.assert_not_called()
|
|
|
|
def test_returns_false_on_exception(self, client):
|
|
with patch("requests.post", side_effect=ConnectionError("network down")):
|
|
assert client.resume_replay("replay_xyz") is False
|
|
|
|
def test_posts_to_resume_endpoint_with_auth_header(self, client):
|
|
resp = MagicMock(ok=True)
|
|
with patch("requests.post", return_value=resp) as post:
|
|
client.resume_replay("replay_xyz")
|
|
call = post.call_args
|
|
url = call.args[0] if call.args else call.kwargs.get("url", "")
|
|
assert url.endswith(RESUME_PATH)
|
|
headers = call.kwargs.get("headers", {})
|
|
assert headers.get("Authorization") == "Bearer tok-test-1234"
|
|
|
|
|
|
# =========================================================================
|
|
# abort_replay
|
|
# =========================================================================
|
|
|
|
|
|
class TestAbortReplay:
|
|
def test_returns_true_when_server_accepts(self, client):
|
|
resp = MagicMock(ok=True)
|
|
with patch("requests.post", return_value=resp):
|
|
assert client.abort_replay("replay_xyz") is True
|
|
|
|
def test_returns_false_when_server_rejects(self, client):
|
|
resp = MagicMock(ok=False)
|
|
with patch("requests.post", return_value=resp):
|
|
assert client.abort_replay("replay_xyz") is False
|
|
|
|
def test_returns_false_on_empty_replay_id(self, client):
|
|
with patch("requests.post") as post:
|
|
assert client.abort_replay("") is False
|
|
post.assert_not_called()
|
|
|
|
def test_returns_false_on_exception(self, client):
|
|
with patch("requests.post", side_effect=TimeoutError("timeout")):
|
|
assert client.abort_replay("replay_xyz") is False
|
|
|
|
def test_posts_to_cancel_endpoint(self, client):
|
|
resp = MagicMock(ok=True)
|
|
with patch("requests.post", return_value=resp) as post:
|
|
client.abort_replay("replay_xyz")
|
|
url = post.call_args.args[0]
|
|
assert url.endswith(CANCEL_PATH)
|