"""Native semantics with deterministic clocks and no devices or paid models.""" import asyncio from copy import deepcopy from unittest.mock import AsyncMock from uuid import UUID import pytest from artemis.mcp import native_executor from artemis.mcp.native_executor import execute_native SELECTOR = {"resource_id": "control"} CONTAINER = {"resource_id": "list"} TARGET = {"text": "destination"} TOKEN = "a" * 64 def node(**overrides): return { "token": TOKEN, "content_signature": "contents-a", "visible": True, "enabled": True, "checkable": True, "checked": False, "selected": False, "password": False, "text": "exact text", "actions": ["click", "set_progress", "set_text", "scroll_forward"], "range": {"min": 0, "max": 10, "current": 3, "type": 1}, **overrides, } def observed(**overrides): return {"success": True, "execution": "observed", "code": "OK", "node": node(**overrides)} def missing(code="NOT_FOUND"): return {"success": False, "execution": "not_started", "code": code, "error": code} def completed(**extra): return {"success": True, "execution": "completed", "code": "OK", **extra} class Clock: def __init__(self): self.now = 0.0 self.sleeps = [] def monotonic(self): return self.now async def sleep(self, seconds): assert seconds > 0 self.sleeps.append(seconds) self.now += seconds @pytest.fixture def clock(monkeypatch): fake = Clock() monkeypatch.setattr(native_executor, "monotonic", fake.monotonic) monkeypatch.setattr(native_executor, "sleep", fake.sleep) return fake async def run(name, args, native): return await execute_native(name, args, 400, 800, native) @pytest.mark.asyncio @pytest.mark.parametrize( "condition,fields", [ ({"state": "unchecked"}, {"checked": False}), ({"state": "text", "expected_text": " exact "}, {"text": " exact "}), ({"state": "content_desc", "expected_text": "播放"}, {"content_desc": "播放"}), ({"state": "value", "expected_value": 3.0}, {}), ], ) async def test_ensure_state_already_satisfied_never_toggles(condition, fields): native = AsyncMock(return_value=observed(**fields)) result = await run( "control_action", {"selector": SELECTOR, "action": "click", "ensure_state": condition}, native, ) assert result["success"] and result["skipped"] assert result["execution"] == "observed" assert [call.args[0] for call in native.await_args_list] == ["inspect_control"] @pytest.mark.asyncio async def test_ensure_state_missing_native_boolean_refuses_input(): native = AsyncMock(return_value=observed(checked=None)) result = await run( "control_action", {"selector": SELECTOR, "action": "click", "ensure_state": {"state": "unchecked"}}, native, ) assert not result["success"] and result["execution"] == "not_started" native.assert_awaited_once() @pytest.mark.asyncio async def test_ensure_state_writes_once_then_reads_actual_state(): native = AsyncMock(side_effect=[observed(checked=True), completed(), observed(checked=False)]) result = await run( "control_action", {"selector": SELECTOR, "action": "click", "ensure_state": {"state": "unchecked"}}, native, ) assert result["success"] and result["verification"] == "condition_satisfied" assert [call.args[0] for call in native.await_args_list] == [ "inspect_control", "control_action", "inspect_control", ] args = native.await_args_list[1].args[1] assert "ensure_state" not in args and "verify_timeout_ms" not in args assert args["expected_token"] == TOKEN @pytest.mark.asyncio async def test_failed_postcondition_preserves_completed_input_without_retoggling(clock): async def native(command, args, **kwargs): return completed() if command == "control_action" else observed(checked=True) native = AsyncMock(side_effect=native) result = await run( "control_action", { "selector": SELECTOR, "action": "click", "ensure_state": {"state": "unchecked"}, "verify_timeout_ms": 400, }, native, ) assert not result["success"] and result["code"] == "TIMEOUT" assert result["execution"] == "completed" assert result["native_receipt"]["success"] assert sum(c.args[0] == "control_action" for c in native.await_args_list) == 1 @pytest.mark.asyncio @pytest.mark.parametrize("name", ["pinch", "touch_gesture"]) async def test_all_gesture_geometry_is_validated_before_a_single_native_dispatch(name): args = ( { "center": [200, 400], "start_span": 40, "end_span": 80, "rotation_degrees": 45, "duration_ms": 900, "coordinate_space": "pixel", "target_description": "observed control", } if name == "pinch" else { "frames": [ {"at_ms": 0, "points": [[100, 100]]}, {"at_ms": 300, "points": [[120, 160]]}, {"at_ms": 900, "points": [[140, 220]]}, ], "coordinate_space": "pixel", "target_description": "observed control", } ) native = AsyncMock(return_value=completed(native_marker="original receipt")) result = await run(name, args, native) assert result["success"] and result["execution"] == "completed" assert result["verification"] == "pending_observation" assert result["native_receipt"]["native_marker"] == "original receipt" native.assert_awaited_once() command, sent = native.await_args.args assert command == "touch_gesture" assert UUID(sent["operation_id"]) assert sent["frames"] == result["plan"]["frames"] assert sent["screen_size"] == [400, 800] assert native.await_args.kwargs == {"timeout": 5.9} invalid = deepcopy(args) if name == "pinch": invalid["center"] = [0, 0] else: invalid["frames"][-1]["points"] = [[401, 220]] never = AsyncMock() refused = await run(name, invalid, never) assert not refused["success"] and refused["execution"] == "not_started" never.assert_not_awaited() @pytest.mark.asyncio async def test_touch_timeout_never_replays_the_input(): native = AsyncMock(side_effect=TimeoutError("reply lost")) result = await run( "touch_gesture", { "frames": [{"at_ms": 0, "points": [[10, 10]]}, {"at_ms": 20, "points": [[20, 30]]}], "target_description": "observed control", "require_visible": "required control", }, native, ) assert not result["success"] and result["execution"] == "unknown" assert result["code"] == "TIMEOUT" native.assert_awaited_once() assert native.await_args.args[1]["require_visible"] == "required control" @pytest.mark.asyncio async def test_read_control_keeps_native_evidence_and_omits_null_selector_fields(): receipt = observed() native = AsyncMock(return_value=receipt) result = await run("read_control", {"selector": {**SELECTOR, "text": None}}, native) native.assert_awaited_once_with("inspect_control", {"selector": SELECTOR}, timeout=None) assert result["node"] == receipt["node"] assert result["native_receipt"] == receipt assert result["execution"] == "observed" and result["action"] == "read_control" @pytest.mark.asyncio async def test_control_action_uses_fresh_token_preserves_whitespace_and_does_not_verify_business_value(): before = observed() after = node(text="different accepted text") receipt = completed(node_before=before["node"], node_after=after) native = AsyncMock(side_effect=[before, receipt]) result = await run( "control_action", {"selector": SELECTOR, "action": "set_text", "text": " exact\n"}, native ) assert native.await_args_list[1].args == ( "control_action", {"selector": SELECTOR, "action": "set_text", "text": " exact\n", "expected_token": TOKEN}, ) assert native.await_count == 2 assert result["success"] and result["verification"] == "pending_observation" assert result["node_before"] == before["node"] assert result["node_after"] == after assert result["native_receipt"] == receipt @pytest.mark.asyncio @pytest.mark.parametrize( "snapshot,request_extra,code", [ (node(), {"expected_token": "b" * 64}, "STALE"), (node(actions=["long_click"]), {}, "UNSUPPORTED"), (node(enabled=False), {}, "DISABLED"), (node(token=None), {}, "INVALID_RESPONSE"), ], ) async def test_control_action_refuses_stale_unsupported_or_disabled_without_input( snapshot, request_extra, code ): native = AsyncMock(return_value={**observed(), "node": snapshot}) result = await run( "control_action", {"selector": SELECTOR, "action": "click", **request_extra}, native ) assert not result["success"] and result["code"] == code assert result["execution"] == "not_started" assert result["node_before"] == snapshot native.assert_awaited_once() @pytest.mark.asyncio @pytest.mark.parametrize( "native_range,value", [ ({"min": 0, "max": 10, "current": 3}, 11), ({"min": 0, "max": 10, "current": float("nan")}, 3), ({"min": 0, "max": 10, "current": 3, "type": 0}, 3.5), ], ) async def test_set_progress_refuses_invalid_live_range_before_input(native_range, value): native = AsyncMock(return_value=observed(range=native_range)) result = await run( "control_action", {"selector": SELECTOR, "action": "set_progress", "value": value}, native ) assert not result["success"] and result["execution"] == "not_started" native.assert_awaited_once() @pytest.mark.asyncio async def test_control_transport_failure_is_unknown_and_never_replayed(): native = AsyncMock(side_effect=[observed(), OSError("lost reply")]) result = await run("control_action", {"selector": SELECTOR, "action": "click"}, native) assert result["code"] == "NATIVE_ERROR" and result["execution"] == "unknown" assert not result["success"] and native.await_count == 2 @pytest.mark.asyncio @pytest.mark.parametrize( "receipt", [ "legacy unstructured success", None, {"success": True, "execution": [], "code": "OK"}, {"success": True, "execution": "unknown", "code": "OK"}, {"success": True, "execution": "completed"}, ], ) async def test_malformed_write_receipts_are_unknown_and_never_replayed(receipt): native = AsyncMock(side_effect=[observed(), receipt]) result = await run("control_action", {"selector": SELECTOR, "action": "click"}, native) assert not result["success"] and result["execution"] == "unknown" assert result["code"] == "INVALID_RESPONSE" and native.await_count == 2 assert result["native_receipt"] == receipt @pytest.mark.asyncio async def test_wait_observes_delayed_condition_without_operating_or_sleeping_after_success(clock): native = AsyncMock(side_effect=[missing(), observed(visible=False), observed()]) result = await run( "wait_until", {"selector": SELECTOR, "state": "visible", "timeout_ms": 1000, "poll_interval_ms": 100}, native, ) assert result["success"] and result["execution"] == "observed" assert result["attempts"] == 3 and clock.now == pytest.approx(0.2) assert [call.args[0] for call in native.await_args_list] == ["inspect_control"] * 3 assert [call.kwargs["timeout"] for call in native.await_args_list] == pytest.approx( [1, 0.9, 0.8] ) @pytest.mark.asyncio @pytest.mark.parametrize("changed_field", ["token", "content_signature"]) async def test_stability_restarts_on_identity_or_descendant_redraw(clock, changed_field): change = {changed_field: "changed"} native = AsyncMock( side_effect=[observed(), observed(**change), observed(**change), observed(**change)] ) result = await run( "wait_until", { "selector": SELECTOR, "state": "stable", "stable_ms": 190, "timeout_ms": 1000, "poll_interval_ms": 100, }, native, ) assert result["success"] and result["attempts"] == 4 assert clock.now == pytest.approx(0.3) @pytest.mark.asyncio async def test_zero_stability_window_still_requires_two_actual_reads(clock): native = AsyncMock(return_value=observed()) result = await run( "wait_until", {"selector": SELECTOR, "state": "stable", "stable_ms": 0}, native ) assert result["success"] and result["attempts"] == 2 assert len(clock.sleeps) == 1 @pytest.mark.asyncio @pytest.mark.parametrize("receipt", [missing(), observed(visible=False)]) async def test_absent_accepts_only_not_found_or_unique_explicitly_invisible(clock, receipt): native = AsyncMock(return_value=receipt) result = await run("wait_until", {"selector": SELECTOR, "state": "absent"}, native) assert result["success"] and result["execution"] == "observed" native.assert_awaited_once() @pytest.mark.asyncio @pytest.mark.parametrize( "error", ["AMBIGUOUS", "UNSUPPORTED", "LIMIT_EXCEEDED", "INVALID_RESPONSE"] ) async def test_absent_rejects_nontransient_inspection_errors_immediately(clock, error): native = AsyncMock(return_value=missing(error)) result = await run("wait_until", {"selector": SELECTOR, "state": "absent"}, native) assert not result["success"] and result["code"] == error native.assert_awaited_once() assert not clock.sleeps @pytest.mark.asyncio @pytest.mark.parametrize("error", ["UNAVAILABLE", "READ_FAILED"]) async def test_unreadable_windows_time_out_without_proving_absence(clock, error): native = AsyncMock(return_value=missing(error)) result = await run( "wait_until", {"selector": SELECTOR, "state": "absent", "timeout_ms": 300, "poll_interval_ms": 100}, native, ) assert not result["success"] and result["code"] == "TIMEOUT" assert result["native_receipt"]["code"] == error assert result["attempts"] == 3 and clock.now == pytest.approx(0.3) assert [sample["code"] for sample in result["observations"]] == [error] * 3 assert [call.args[0] for call in native.await_args_list] == ["inspect_control"] * 3 @pytest.mark.asyncio @pytest.mark.parametrize("error", ["UNAVAILABLE", "READ_FAILED"]) @pytest.mark.parametrize("state,receipt", [("visible", observed()), ("absent", missing())]) async def test_wait_recovers_only_after_a_reliable_observation(clock, error, state, receipt): native = AsyncMock(side_effect=[missing(error), receipt]) result = await run( "wait_until", {"selector": SELECTOR, "state": state, "timeout_ms": 500, "poll_interval_ms": 100}, native, ) assert result["success"] and result["execution"] == "observed" assert result["attempts"] == 2 and clock.now == pytest.approx(0.1) assert result["observations"][0]["code"] == error assert result["native_receipt"] == receipt assert [call.args[0] for call in native.await_args_list] == ["inspect_control"] * 2 @pytest.mark.asyncio @pytest.mark.parametrize("error", ["UNAVAILABLE", "READ_FAILED"]) async def test_transient_read_failure_restarts_the_full_stability_window(clock, error): native = AsyncMock(side_effect=[observed(), missing(error), observed(), observed(), observed()]) result = await run( "wait_until", { "selector": SELECTOR, "state": "stable", "stable_ms": 190, "timeout_ms": 1000, "poll_interval_ms": 100, }, native, ) assert result["success"] and result["attempts"] == 5 assert clock.now == pytest.approx(0.4) assert result["observations"][1]["code"] == error @pytest.mark.asyncio async def test_absent_transport_failure_is_not_absence(clock): native = AsyncMock(side_effect=OSError("unavailable")) result = await run("wait_until", {"selector": SELECTOR, "state": "absent"}, native) assert not result["success"] and result["code"] == "NATIVE_ERROR" native.assert_awaited_once() @pytest.mark.asyncio @pytest.mark.parametrize("receipt", [{**observed(), "candidate_count": 2}, observed(visible=None)]) async def test_absent_rejects_nonunique_or_unknown_visibility_observation(clock, receipt): native = AsyncMock(return_value=receipt) result = await run("wait_until", {"selector": SELECTOR, "state": "absent"}, native) assert not result["success"] assert result["code"] in {"AMBIGUOUS", "INVALID_STATE"} native.assert_awaited_once() @pytest.mark.asyncio @pytest.mark.parametrize( "state,args,snapshot,success", [ ("text", {"expected_text": " exact text "}, node(), False), ("text", {"expected_text": "exact text"}, node(), True), ("value", {"expected_value": 3.1, "tolerance": 0.2}, node(), True), ( "value", {"expected_value": 3}, node(range={"min": 0, "max": float("inf"), "current": 3}), False, ), ("unchecked", {}, node(checkable=False, checked=False), False), ("checked", {}, node(checked=True), True), ("disabled", {}, node(enabled=False), True), ("unselected", {}, node(selected=False), True), ], ) async def test_wait_predicates_use_meaningful_exact_native_values( clock, state, args, snapshot, success ): native = AsyncMock(return_value={**observed(), "node": snapshot}) result = await run( "wait_until", {"selector": SELECTOR, "state": state, "timeout_ms": 400, "stable_ms": 0, **args}, native, ) assert result["success"] is success assert all(call.args[0] == "inspect_control" for call in native.await_args_list) @pytest.mark.asyncio async def test_wait_deadline_is_bounded_and_preserves_observations(clock): native = AsyncMock(return_value=observed(visible=False)) result = await run( "wait_until", {"selector": SELECTOR, "state": "visible", "timeout_ms": 350, "poll_interval_ms": 100}, native, ) assert not result["success"] and result["code"] == "TIMEOUT" assert clock.now == pytest.approx(0.35) and native.await_count == 4 assert len(result["observations"]) == 4 @pytest.mark.asyncio async def test_wait_rejects_condition_returned_after_monotonic_deadline(clock): async def native(command, args, *, timeout=None): clock.now += 0.6 return observed() result = await run( "wait_until", {"selector": SELECTOR, "state": "visible", "timeout_ms": 500}, native ) assert not result["success"] and result["code"] == "TIMEOUT" assert result["attempts"] == 1 @pytest.mark.asyncio async def test_wait_stops_a_nonreturning_native_read_at_its_deadline(clock): calls = [] async def native(command, args, *, timeout=None): calls.append(command) await asyncio.Event().wait() result = await run( "wait_until", {"selector": SELECTOR, "state": "visible", "timeout_ms": 20, "stable_ms": 0}, native, ) assert not result["success"] and result["code"] == "TIMEOUT" assert calls == ["inspect_control"] @pytest.mark.asyncio async def test_long_wait_bounds_evidence_memory_while_retaining_first_and_last_reads(clock): native = AsyncMock(return_value=observed(visible=False)) result = await run( "wait_until", {"selector": SELECTOR, "state": "visible", "timeout_ms": 4000, "poll_interval_ms": 50}, native, ) assert result["attempts"] > 64 and len(result["observations"]) == 64 assert result["observations"][0]["at_ms"] == 0 assert result["observations"][-1]["at_ms"] > 3000 class ScrollNative: def __init__(self, clock, *, delayed_target=None, progress=True, receipt=None): self.clock = clock self.delayed_target = delayed_target self.progress = progress self.receipt = receipt or completed() self.calls = [] self.writes = 0 async def __call__(self, command, args, *, timeout=None): self.calls.append((command, deepcopy(args), timeout, self.clock.now)) if command == "control_action": self.writes += 1 return self.receipt if args["selector"] == TARGET: # An unscoped query would spuriously find a same-text external node. if args.get("scope") != CONTAINER: return observed(text="external false positive") if ( self.writes and self.delayed_target is not None and self.clock.now >= self.delayed_target ): return observed(text="destination") return missing() assert args == {"selector": CONTAINER} changed = self.writes and self.progress and self.clock.now >= 0.1 return observed( token=("b" if self.writes else "a") * 64, content_signature=f"page-{self.writes}" if changed else "contents-a", ) def scroll_request(**extra): return {"container": CONTAINER, "target": TARGET, "settle_ms": 300, "timeout_ms": 2000, **extra} @pytest.mark.asyncio async def test_scroll_waits_for_delayed_scoped_target_after_content_changes(clock): native = ScrollNative(clock, delayed_target=0.2) result = await run("scroll_to", scroll_request(), native) assert result["success"] and result["execution"] == "completed" assert native.writes == 1 and clock.now >= 0.4 - 1e-9 assert result["node"]["text"] == "destination" assert result["scroll_results"][0]["native_receipt"]["success"] queries = [ args for command, args, _, _ in native.calls if command == "inspect_control" and args["selector"] == TARGET ] assert queries and all(query["scope"] == CONTAINER for query in queries) @pytest.mark.asyncio async def test_scroll_token_change_without_content_change_is_no_progress(clock): native = ScrollNative(clock, progress=False) result = await run("scroll_to", scroll_request(), native) assert not result["success"] and result["code"] == "NO_PROGRESS" assert result["execution"] == "completed" and native.writes == 1 assert clock.now == pytest.approx(0.5) assert result["scroll_results"][0]["native_receipt"]["success"] @pytest.mark.asyncio async def test_scroll_preserves_each_receipt_and_refreshes_token_with_hard_input_limit(clock): native = ScrollNative(clock) result = await run("scroll_to", scroll_request(max_scrolls=2), native) assert result["code"] == "MAX_SCROLLS" and not result["success"] assert native.writes == 2 and len(result["scroll_results"]) == 2 writes = [(args, at) for command, args, _, at in native.calls if command == "control_action"] assert writes[0][0]["expected_token"] == TOKEN assert writes[1][0]["expected_token"] == "b" * 64 assert writes[1][1] >= 0.3 @pytest.mark.asyncio @pytest.mark.parametrize("execution", ["unknown", "cancelled"]) async def test_scroll_unknown_or_cancelled_input_is_never_repeated(clock, execution): receipt = {"success": False, "execution": execution, "code": "TIMEOUT", "native_marker": "kept"} native = ScrollNative(clock, receipt=receipt) result = await run("scroll_to", scroll_request(), native) assert not result["success"] and result["execution"] == execution assert result["scroll_results"][0]["native_receipt"] == receipt assert native.writes == 1 and not clock.sleeps @pytest.mark.asyncio async def test_scroll_boundary_rejection_is_no_progress_without_retry(clock): native = ScrollNative(clock, receipt=missing("ACTION_REJECTED")) result = await run("scroll_to", scroll_request(), native) assert result["code"] == "NO_PROGRESS" and native.writes == 1 assert result["scroll_results"][0]["native_receipt"]["code"] == "ACTION_REJECTED" @pytest.mark.asyncio @pytest.mark.parametrize("which", ["container", "target"]) async def test_scroll_refuses_ambiguous_container_or_scoped_target_before_input(clock, which): calls = [] async def native(command, args, *, timeout=None): calls.append(command) assert command == "inspect_control" if args["selector"] == (CONTAINER if which == "container" else TARGET): return missing("AMBIGUOUS") return observed() result = await run("scroll_to", scroll_request(), native) assert not result["success"] and result["code"] == "AMBIGUOUS" assert "control_action" not in calls @pytest.mark.asyncio @pytest.mark.parametrize( "write_receipt,execution", [ (completed(native_marker="late accepted input"), "completed"), ( { "success": False, "execution": "unknown", "code": "TIMEOUT", "native_marker": "late uncertain receipt", }, "unknown", ), ], ) async def test_scroll_drains_dispatched_input_past_search_deadline_without_more_work( clock, write_receipt, execution ): calls = [] events = [] pending_input = None async def deliver_input(): # Device input survives a lost HTTP waiter; cancelling its await cannot undo it. await asyncio.sleep(0.12) clock.now = 0.12 events.append("input_delivered") return write_receipt async def native(command, args, *, timeout=None): nonlocal pending_input calls.append((command, deepcopy(args), timeout)) if command == "inspect_control": return missing() if args["selector"] == TARGET else observed() pending_input = asyncio.create_task(deliver_input()) return await asyncio.wait_for(asyncio.shield(pending_input), timeout=timeout) try: result = await run( "scroll_to", scroll_request(timeout_ms=50, settle_ms=50, max_scrolls=3), native ) events.append("tool_returned") finally: if pending_input is not None: await pending_input assert events == ["input_delivered", "tool_returned"] assert not result["success"] and result["code"] == "TIMEOUT" assert result["execution"] == execution assert result["scroll_results"][0]["native_receipt"] == write_receipt assert [command for command, _, _ in calls] == [ "inspect_control", "inspect_control", "control_action", ] assert calls[-1][2] == 5.0 assert not clock.sleeps @pytest.mark.asyncio async def test_scroll_timeout_caps_total_search_even_when_content_changes(clock): native = ScrollNative(clock) result = await run("scroll_to", scroll_request(timeout_ms=350, settle_ms=300), native) assert result["code"] == "TIMEOUT" and not result["success"] assert clock.now == pytest.approx(0.35) and native.writes <= 2 assert len(result["scroll_results"]) == native.writes @pytest.mark.asyncio async def test_scroll_requires_visible_target_not_merely_an_existing_scoped_node(clock): backend = ScrollNative(clock, delayed_target=0.2) async def native(command, args, *, timeout=None): result = await backend(command, args, timeout=timeout) return ( observed(text="destination", visible=False) if result.get("code") == "NOT_FOUND" else result ) result = await run("scroll_to", scroll_request(), native) assert result["success"] and backend.writes == 1 assert clock.now >= 0.4 - 1e-9 @pytest.mark.asyncio async def test_scroll_does_not_send_next_input_while_content_is_animating(clock): writes = [] async def native(command, args, *, timeout=None): if command == "control_action": writes.append(clock.now) return completed() if args["selector"] == TARGET: return missing() page = min(int(clock.now * 10 + 1e-6), 6) if writes else 0 return observed(content_signature=f"animation-{page}") result = await run("scroll_to", scroll_request(max_scrolls=2), native) assert result["code"] == "NO_PROGRESS" and len(writes) == 2 assert writes[1] >= 0.9 - 1e-9 @pytest.mark.asyncio async def test_scroll_transient_visible_target_during_animation_is_not_success(clock): writes = [] async def native(command, args, *, timeout=None): if command == "control_action": writes.append(clock.now) return completed() if args["selector"] == TARGET: return observed() if writes and 0.1 <= clock.now < 0.2 else missing() return observed(content_signature="after" if writes and clock.now >= 0.25 else "before") result = await run("scroll_to", scroll_request(max_scrolls=1), native) assert result["code"] == "MAX_SCROLLS" and not result["success"] assert len(writes) == 1 and clock.now >= 0.55 - 1e-9 @pytest.mark.asyncio async def test_scroll_tracks_target_outside_bounded_container_signature_sample(clock): writes = [] async def native(command, args, *, timeout=None): if command == "control_action": writes.append(clock.now) return completed() if args["selector"] == TARGET: return observed() if writes and 0.4 - 1e-9 <= clock.now < 0.45 - 1e-9 else missing() return observed(content_signature="after" if writes and clock.now >= 0.1 else "before") result = await run("scroll_to", scroll_request(max_scrolls=1), native) assert result["code"] == "MAX_SCROLLS" and not result["success"] assert len(writes) == 1 and clock.now >= 0.75 - 1e-9 @pytest.mark.asyncio async def test_scroll_read_failure_after_accepted_input_preserves_receipt_without_more_input(clock): backend = ScrollNative(clock) async def native(command, args, *, timeout=None): if command == "inspect_control" and backend.writes: raise OSError("read unavailable after accepted input") return await backend(command, args, timeout=timeout) result = await run("scroll_to", scroll_request(), native) assert not result["success"] and result["execution"] == "completed" assert result["code"] == "NATIVE_ERROR" and backend.writes == 1 assert result["scroll_results"][0]["native_receipt"]["success"] @pytest.mark.asyncio async def test_cancelled_scroll_settling_never_sends_another_input(clock, monkeypatch): backend = ScrollNative(clock) async def cancelled_sleep(seconds): raise asyncio.CancelledError monkeypatch.setattr(native_executor, "sleep", cancelled_sleep) with pytest.raises(asyncio.CancelledError): await run("scroll_to", scroll_request(), backend) assert backend.writes == 1 @pytest.mark.asyncio @pytest.mark.parametrize("name", ["control_action", "wait_until", "scroll_to", "touch_gesture"]) async def test_cancellation_propagates_without_replaying_native_work(clock, name): requests = { "control_action": {"selector": SELECTOR, "action": "click"}, "wait_until": {"selector": SELECTOR, "state": "visible"}, "scroll_to": scroll_request(), "touch_gesture": { "frames": [{"at_ms": 0, "points": [[1, 1]]}, {"at_ms": 20, "points": [[2, 3]]}], "target_description": "observed control", }, } inputs = [] async def native(command, args, *, timeout=None): if command == "inspect_control": if name == "wait_until": raise asyncio.CancelledError return missing() if args["selector"] == TARGET else observed() inputs.append(command) raise asyncio.CancelledError with pytest.raises(asyncio.CancelledError): await run(name, requests[name], native) assert inputs == ( [] if name == "wait_until" else ["touch_gesture" if name == "touch_gesture" else "control_action"] )