"""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__