fix(mobile): bound each input by its budget
Merged
TH
13d ago+293+293+012345678901234567890123456789-50-50-01234567890123456789
What and why
- The mobile engine bounded each agent-device call only by
operation.signal. The harness aborts that signal with the attempt, never attimeoutMs, so an input that outlivedactionTimeoutheld the step until the test timeout. - The
OperationContext.signaldocs said it also aborts "when the operation budget is spent". It doesn't for locator operations. The web engine enforcestimeoutMsitself (packages/web/src/operation-budget.ts). The doc comment now says so. InputBudget.send(packages/mobile/src/support.ts) wraps each device command that sends input. That covers node actions, the screen swipe, point actions, typing, keys,back, and the keyboard dismissal with its gesture fallbacks.- No budget left: nothing is sent, and the result is
OPERATION_TIMEOUT. - Sent but no answer in time:
ACTION_MAY_HAVE_COMMITTED, which the runner never repeats. Web'sinputCutOffdoes the same. - Little budget left: the input is still sent and gets 250 ms to answer.
- A hold longer than the budget left: a long press, or a timed swipe, that holds at least as long as the budget left is refused before it is sent. It fails as
NOT_ACTIONABLE, naming the hold and the time left. - Several commands in one input: a focus tap then a keystroke is sent one command at a time, so no keystroke follows a tap that used the budget up.
- Abandoned commands: a timed-out command stays tracked as in flight until it settles, so the next attempt still waits for it.
- No budget left: nothing is sent, and the result is
- Reads stay unbounded, as on main. That covers observe, locate, and the relocation before a tap. Bounding them broke this PR's first CI run: a locator poll's last iOS snapshot was cut off at the deadline and read as
ACTION_FAILEDinstead ofLOCATOR_NOT_FOUND. docs/mobile.mdx› Troubleshooting says what an action that outlives its timeout does.- Hung reads still hold the step until the test timeout, as on main: #1099.
- No runtime change to
e2e, so mobile's peer floor stays.
Verified
Ran it locally: yes
- Setup: iOS simulator (iPhone 18 Pro Max, iOS 27), a scratch project on the built packages,
com.apple.Preferences,actionTimeout: 4_000. - Long press:
getByTestId('com.apple.settings.general').longPress({ duration: 8_000 }).
| main | this branch |
|---|---|
step passes after 8.7 s (agent-device longpress ok 8685ms) | 73279167: refused before sending (no longpress request): ACTION_FAILED: longPress holds for 8000 ms, but only 616 ms of its timeout are left; raise the timeout or shorten the hold |
Before the hold refusal, f1befc86 sent it and failed at 4.0 s: ACTION_FAILED: longPress timed out before the device answered; its input may have been dispatched.
- Missing locator:
getByText('No such row').tap({ timeout: 3_000 })reportsLOCATOR_NOT_FOUNDon the branch, which is the CI regression fixed. - Unit tests:
- every input entry, hung, times out as
ACTION_MAY_HAVE_COMMITTED; - a slow relocation that spends the budget sends no press;
- a focus tap that answers past the budget sends no keystroke;
- a timed-out tap keeps the next attempt waiting until it settles;
- a long press longer than its timeout is refused unsent;
- each of these fails when its mechanism is removed.
- every input entry, hung, times out as
- CI:
ios deterministicandandroid deterministic(the mobile benchmark's deterministic suites) pass. - Not verified on a device: the agent-origin path. No mobile agent suite runs on a fork PR. Near a step deadline it behaves like web: a tap left with a small budget gets 250 ms to answer.
- Review: five fresh-context review rounds, cubic, and okwasniewski. Every finding was fixed or answered.
Checklist
- Changeset added (
pnpm changeset) for any change to a published package, or the change is testbed, docs, or CI only. - Breaking change: title carries
!, the changeset body names what breaks and what replaces it, and the deprecation warning landed a window earlier (stability policy in CONTRIBUTING.md). - Docs updated in the same PR: the
docs/**/*.mdxpage for the behavior, andskills/e2e/if the skill describes it. - Contract change: emitted
.d.tsreviewed,tests/types/sdk-types.tsupdated; wire change edits the schema and both fixtures. - Engine contract change (
e2e/engine): changesets fore2e,@e2e-dev/web, and@e2e-dev/mobile.
The e2e/engine change is the OperationContext.signal doc comment only. The behavior it describes is unchanged.
🤖 Generated with Claude Code