feat(android): add state conditions and measured gesture feedback

Add optional ensure_state, gesture region and final value feedback shared by Flash and Pro. Preserve completed inputs when verification fails and stop repeated ineffective strategies. Regenerate pinned tool schemas for the new optional arguments.
This commit is contained in:
chenmingxuan committed 2026-11-02 23:20:34 +08:00
1 parent 6a69a595ef
commit 4ea7939d9c
20 files changed
+2779 -33

No files matched your search

+75
View File
@@ -71,6 +71,81 @@ 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):