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