Files
rpa_vision_v3/tests/unit/test_action_resolver.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

206 lines
7.6 KiB
Python

"""Tests for core/navigation/action_resolver.py — coordinate conversion + OCR adapters."""
import json
import pytest
from core.navigation.action_resolver import (
NavigateCoords,
NavigateResult,
grounded_to_coords,
make_ocr_simple_from_detailed,
navigate_login,
)
from core.navigation.grounding import (
CoordsCache,
GroundedElement,
OcrTokenInfo,
OcrDetailedClient,
)
from core.navigation.visual_verifier import VlmClient
# ── Mock factories ─────────────────────────────────────────────────────
def mock_ocr_detailed_client_factory(tokens: list):
def client(image_path: str) -> list:
return tokens
return client
def mock_vlm_client_factory(response_json: dict):
def client(image_path: str, prompt: str) -> str:
return json.dumps(response_json)
return client
# ── grounded_to_coords tests ───────────────────────────────────────────
class TestGroundedToCoords:
def test_basic_conversion(self):
el = GroundedElement(
role="bouton", text="Connexion",
bbox=(200, 50, 400, 100), center=(300, 75),
confidence=0.9, method="ocr_anchor",
)
coords = grounded_to_coords(el, 1920, 1080)
assert coords.x_pct == pytest.approx(300 / 1920, abs=0.01)
assert coords.y_pct == pytest.approx(75 / 1080, abs=0.01)
assert coords.method == "ocr_anchor"
assert coords.bbox_pct is not None
def test_to_dict(self):
coords = NavigateCoords(x_pct=0.15, y_pct=0.07, method="ocr_anchor")
d = coords.to_dict()
assert d["x_pct"] == 0.15
assert d["y_pct"] == 0.07
assert d["method"] == "ocr_anchor"
def test_to_dict_with_bbox(self):
coords = NavigateCoords(
x_pct=0.15, y_pct=0.07,
bbox_pct=(0.10, 0.05, 0.20, 0.09),
method="vlm_grounder",
)
d = coords.to_dict()
assert "bbox_pct" in d
assert len(d["bbox_pct"]) == 4
# ── make_ocr_simple_from_detailed tests ────────────────────────────────
class TestMakeOcrSimpleFromDetailed:
def test_conversion(self):
tokens = [
OcrTokenInfo(text="Login", bbox=(100, 50, 250, 90)),
OcrTokenInfo(text="Password", bbox=(100, 100, 250, 140)),
]
detailed = mock_ocr_detailed_client_factory(tokens)
simple = make_ocr_simple_from_detailed(detailed)
result = simple("/tmp/test.png")
assert result == ["Login", "Password"]
def test_empty_tokens(self):
detailed = mock_ocr_detailed_client_factory([])
simple = make_ocr_simple_from_detailed(detailed)
result = simple("/tmp/test.png")
assert result == []
# ── navigate_login tests ───────────────────────────────────────────────
class TestNavigateLogin:
def test_full_success(self):
"""All fields grounded → NavigateResult with coords."""
ocr = mock_ocr_detailed_client_factory([
OcrTokenInfo(text="Login", bbox=(100, 50, 250, 90), confidence=0.95),
OcrTokenInfo(text="Mot de passe", bbox=(100, 100, 250, 140), confidence=0.95),
OcrTokenInfo(text="Connexion", bbox=(100, 150, 250, 190), confidence=0.95),
])
vlm = mock_vlm_client_factory({
"confirmed": [
{"index": 1, "role_confirmed": True, "actual_role": "champ", "confidence": 0.9},
{"index": 2, "role_confirmed": True, "actual_role": "champ", "confidence": 0.9},
{"index": 3, "role_confirmed": True, "actual_role": "bouton", "confidence": 0.9},
],
"overall_confidence": 0.9,
})
result = navigate_login(
"/tmp/login.png",
ocr_client=ocr, vlm_client=vlm,
skip_pre_verify=True,
)
assert result.all_resolved == True
assert result.login_coords is not None
assert result.password_coords is not None
assert result.submit_coords is not None
assert result.submit_coords.x_pct > 0
assert result.submit_coords.y_pct > 0
def test_no_clients_error(self):
"""Missing OCR/VLM clients → error."""
result = navigate_login("/tmp/login.png", ocr_client=None, vlm_client=None)
assert result.all_resolved == False
assert "required" in result.error
def test_pre_verify_fail(self):
"""Pre-verify fails → early abort."""
ocr = mock_ocr_detailed_client_factory([
OcrTokenInfo(text="Accueil", bbox=(0, 0, 100, 40)),
])
vlm = mock_vlm_client_factory({})
result = navigate_login(
"/tmp/page.png",
ocr_client=ocr, vlm_client=vlm,
skip_pre_verify=False,
)
assert result.all_resolved == False
assert result.pre_verify is not None
assert result.pre_verify.match == False
def test_skip_pre_verify(self):
"""Skip pre-verify → proceed to grounding even if form incomplete."""
ocr = mock_ocr_detailed_client_factory([
OcrTokenInfo(text="Login", bbox=(100, 50, 250, 90)),
OcrTokenInfo(text="Mot de passe", bbox=(100, 100, 250, 140)),
OcrTokenInfo(text="Connexion", bbox=(100, 150, 250, 190)),
])
vlm = mock_vlm_client_factory({})
result = navigate_login(
"/tmp/login.png",
ocr_client=ocr, vlm_client=vlm,
skip_pre_verify=True,
)
assert result.pre_verify is None # skipped
assert result.all_resolved == True
# ── NavigateResult dataclass tests ─────────────────────────────────────
class TestNavigateResult:
def test_default(self):
result = NavigateResult()
assert result.all_resolved == False
assert result.login_coords is None
assert result.error == ""
def test_with_coords(self):
result = NavigateResult(
login_coords=NavigateCoords(x_pct=0.15, y_pct=0.07, method="ocr_anchor"),
all_resolved=True,
)
assert result.login_coords.x_pct == 0.15
# ── Import validation ──────────────────────────────────────────────────
class TestImportValidation:
def test_action_resolver_imports(self):
"""Verify action_resolver module imports cleanly."""
from core.navigation.action_resolver import (
NavigateCoords,
NavigateResult,
grounded_to_coords,
make_ocr_detailed_from_grid,
make_ocr_simple_from_detailed,
navigate_login,
)
assert NavigateCoords is not None
assert NavigateResult is not None
def test_navigation_package_handler(self):
"""Verify _handle_navigate_action is importable from package."""
from core.navigation import _handle_navigate_action
assert callable(_handle_navigate_action)
def test_navigation_package_exports(self):
"""Verify package __all__ includes navigate exports."""
import core.navigation as nav
assert "navigate_login" in nav.__all__
assert "NavigateResult" in nav.__all__
assert "_handle_navigate_action" in nav.__all__