Files
rpa_vision_v3/tests/unit/test_navigate_handler_e2e.py
Dom f9a0531325
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m52s
tests / Tests unitaires (sans GPU) (push) Failing after 1m58s
tests / Tests sécurité (critique) (push) Has been skipped
feat(navigation): brique login visuel OCR-ancre + action navigate au replay
- core/navigation/ : visual_verifier (presence=OCR, role=VLM ancre sur tokens),
  grounding (OCR-anchor first, VLM fallback, cache coords valide par la vue),
  visual_login (verify_before/after, DETTE-023), action_resolver (pont runtime)
- api_stream/replay_engine : dispatch action navigate server-side,
  never-fail -> needs_review, import depuis core.navigation (boot 5005 garanti)
- 131 tests verts (wiring boot, e2e handler, unit modules)

Chantier Qwen 01-02/07/2026, revue croisee Claude (plan deploy v2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 10:31:44 +02:00

152 lines
5.9 KiB
Python

"""End-to-end mocked test for navigate action handler — 3 edge-case scenarios.
Tests the _handle_navigate_action handler with mocked OCR/VLM, verifying:
- Nominal: all resolved, coords populated in variables
- OCR miss + VLM fail: no phantom coords, all_resolved=False
- No screenshot: error="no_screenshot", False return
NOTE: The handler uses lazy imports inside its body. Mock targets must be
at the source module (core.navigation.action_resolver.navigate_login) rather
than the package-level re-export (core.navigation.navigate_login).
"""
import pytest
from unittest.mock import patch, MagicMock
from core.navigation.action_resolver import NavigateCoords, NavigateResult
from core.navigation import _handle_navigate_action
def _patch_all_deps(navigate_login_result=None, navigate_login_side_effect=None):
"""Return stacked patches for handler's lazy imports + navigate_login."""
nl_mock = MagicMock(return_value=navigate_login_result) if navigate_login_result else None
if navigate_login_side_effect:
nl_mock = MagicMock(side_effect=navigate_login_side_effect)
return (
patch("core.llm.extract_grid_from_image", return_value=[]),
patch("core.extraction.vlm_client.make_vllm_client", return_value=MagicMock()),
patch("core.navigation.action_resolver.make_ocr_detailed_from_grid",
return_value=MagicMock(return_value=[])),
patch("core.navigation.action_resolver.navigate_login", nl_mock),
)
class TestNominalCase:
"""All fields grounded → coords populated, all_resolved=True."""
def test_nominal_coords_populated(self):
mock_result = NavigateResult(
login_coords=NavigateCoords(x_pct=0.15, y_pct=0.07, method="ocr_anchor"),
password_coords=NavigateCoords(x_pct=0.15, y_pct=0.25, method="ocr_anchor"),
submit_coords=NavigateCoords(x_pct=0.50, y_pct=0.35, method="ocr_anchor"),
all_resolved=True,
)
action = {"parameters": {"action": "login"}}
replay_state = {
"last_screenshot_path": "/tmp/login_screen.png",
"screen_width": 1920,
"screen_height": 1080,
}
p1, p2, p3, p4 = _patch_all_deps(navigate_login_result=mock_result)
with p1, p2, p3, p4:
result = _handle_navigate_action(action, replay_state, "test-session")
assert result is True
vars_ = replay_state["variables"]
assert "navigate_login_coords" in vars_
assert vars_["navigate_login_coords"]["x_pct"] == 0.15
assert "navigate_password_coords" in vars_
assert "navigate_submit_coords" in vars_
assert vars_["navigate_result"]["all_resolved"] is True
class TestOcrMissVlmFail:
"""OCR misses target + VLM grounder also fails → no phantom coords."""
def test_no_phantom_coords_on_failure(self):
mock_result = NavigateResult(
login_coords=None,
password_coords=None,
submit_coords=None,
all_resolved=False,
error="grounding failed — no login form elements found",
)
action = {"parameters": {"action": "login"}}
replay_state = {
"last_screenshot_path": "/tmp/no_login_form.png",
"screen_width": 1920,
"screen_height": 1080,
}
p1, p2, p3, p4 = _patch_all_deps(navigate_login_result=mock_result)
with p1, p2, p3, p4:
result = _handle_navigate_action(action, replay_state, "test-session")
assert result is False
vars_ = replay_state["variables"]
# No coords keys should be present (coords are None → not stored)
assert "navigate_login_coords" not in vars_
assert "navigate_password_coords" not in vars_
assert "navigate_submit_coords" not in vars_
# Error must be non-empty
assert vars_["navigate_result"]["all_resolved"] is False
assert "grounding failed" in vars_["navigate_result"]["error"]
class TestNoScreenshot:
"""No screenshot in replay_state → error="no_screenshot", False."""
def test_no_screenshot_error(self):
action = {"parameters": {"action": "login"}}
replay_state = {} # No screenshot at all
result = _handle_navigate_action(action, replay_state, "test-session")
assert result is False
vars_ = replay_state["variables"]
assert vars_["navigate_login_coords"]["error"] == "no_screenshot"
def test_empty_screenshot_path(self):
action = {"parameters": {"action": "login"}}
replay_state = {"last_screenshot_path": ""}
result = _handle_navigate_action(action, replay_state, "test-session")
assert result is False
vars_ = replay_state["variables"]
assert vars_["navigate_login_coords"]["error"] == "no_screenshot"
class TestNeverFailReplay:
"""Handler must never raise — even on malformed input, returns False."""
def test_missing_parameters(self):
action = {} # No "parameters" key
replay_state = {"last_screenshot_path": "/tmp/x.png"}
mock_result = NavigateResult(all_resolved=False, error="no params")
p1, p2, p3, p4 = _patch_all_deps(navigate_login_result=mock_result)
with p1, p2, p3, p4:
result = _handle_navigate_action(action, replay_state, "test-session")
assert result is False
def test_exception_in_inner_call(self):
action = {"parameters": {"action": "login"}}
replay_state = {
"last_screenshot_path": "/tmp/login.png",
"screen_width": 1920,
"screen_height": 1080,
}
p1, p2, p3, p4 = _patch_all_deps(navigate_login_side_effect=RuntimeError("boom"))
with p1, p2, p3, p4:
result = _handle_navigate_action(action, replay_state, "test-session")
assert result is False
vars_ = replay_state["variables"]
assert vars_["navigate_result"]["all_resolved"] is False
assert "boom" in vars_["navigate_result"]["error"]