You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Maestro flows that used repeat.while were rejected with "repeat.while is not supported; use repeat.times". This adds support that follows the Maestro repeat reference:
Parser:repeat now needs times, while, or both. while accepts platform, visible, notVisible, and true.
Execution: the condition is checked before each iteration. The loop stops when the condition is false or when times is reached, whichever comes first.
Visibility checks use the same lookup timeout as runFlow.when.
true expressions run JavaScript through the same evaluator as evalScript. Results count as false using the same rule as assertTrue. So "false", "0", "", "null", and "undefined" from string output values are false.
Changes the expression makes to output.* carry into later steps.
Security:while.true is refused for flows received over the remote daemon connection, the same as evalScript.
Conformance and docs: added an authored repeat-while flow and regenerated the layer-1 fixtures. Updated the support matrix and replay-e2e.md.
19 files changed. No scope expansion.
Test plan
Ran pnpm check:affected --run on e4dc41a24 and it passed.
The first run had two unrelated timeouts (interaction-response-shape, apple-platform-output-guard). Both passed when run alone and on the rerun.
New unit tests cover:
parsing, including the errors for a missing or empty while
a JavaScript loop
a visibility loop
stopping at the times bound
refusal for untrusted remote flows
true/false handling of script results
pnpm maestro:conformance passes.
Not tested on a device yet. To check manually, run the YAML above with agent-device replay on an iOS simulator or Android emulator.
Parse and execute repeat.while with platform, visible, notVisible, and JavaScript true conditions, optionally bounded by times. Script results use Maestro string truthiness, and while.true is refused for untrusted remote flows like evalScript.
The reviewed commit e4dc41a has one code defect and one missing validation, so it is not ready to merge yet. The one check that ran is green, and there are no conflicts.
The same Maestro true: condition has two evaluators. runFlow.when.true goes through evaluateMaestroBooleanExpression (engine-flow.ts:164), which knows maestro.platform, quoted strings and plain literals. repeat.while.true goes through scriptConditionMatches (replay-plan-step-execution.ts:291), which runs node:vm with no maestro object. So while: {true: "${maestro.platform == 'ios'}"} fails with "maestro is not defined", and a bare literal like true: yes throws a ReferenceError. The same condition passes under runFlow.when, and upstream Maestro accepts both. The rule should be that every Maestro true: condition is evaluated by one owner with the same bindings. Please add the maestro binding and the literal handling to that one owner, or route both commands through one evaluator. Then add tests for repeat.while.true with maestro.platform and with a non-${} literal.
The PR says it is not tested on a device. repeat.while.visible and notVisible call observe() on a live snapshot each iteration (replay-plan-step-execution.ts:245). Nothing yet shows that the loop ends when a real element appears, or that the cached observation is not reused across iterations. Please run agent-device test <flow> --maestro on an iOS simulator or Android emulator with a repeat: while: notVisible: X loop whose body makes X appear after N taps. Paste the output showing N body iterations and the loop exit. Also paste a times-bounded while.true run that shows the final output counter value.
Could MaestroRepeatCondition and parseMaestroRepeatCondition go away? The type is a Pick of all four MaestroRunFlowCondition fields, so it is the same type. parseMaestroRunFlowCondition already threads name, so it could take the field name and serve both commands. One matching function could then serve runFlow.when and repeat.while. That would also remove the second UNAUTHORIZED message and the special case in resolveCommand. First, we would need to decide whether true: is a restricted expression or full JS for every command, and record it in the support matrix.
On the open review threads, r4179477864 and its duplicate r4179481837 still stand (selectors resolved once). The 0n truthiness thread r4179477876 and the Copilot starvation thread r4179481812 still stand, as do the parser copy r4179477880, the missing empty-string test case r4179477883, the duplicated UNAUTHORIZED text r4179477886 and the boolean normalization thread r4179481825. Two threads do not apply, so please resolve them. r4179477867: running the script before the platform check matches upstream. r4179477871: an unbounded loop without times is upstream behavior, and cancellation is checked each iteration.
I did not run the tests or pnpm maestro:conformance. I also did not read upstream Maestro's source, so the upstream points above come from memory.
Before merge, repeat.while.true must use the same bindings and rules as runFlow.when.true, and the live simulator or emulator run must be attached.
…ition owner
Both conditions share one parser, one IR type, and one true: evaluator. Booleans, maestro.platform comparisons, and literal text after ${VAR} lookups need no JavaScript; other expressions run in node:vm with a maestro.platform binding and are refused for untrusted remote flows through a shared trust check. Condition selectors are resolved at every check, and repeat yields to the event loop each iteration so cancellation timers can fire.
Thanks for the update. At e3f1ecf, one code issue remains and no device run is attached yet, so it is not ready. CI is green. There are no conflicts.
Since the earlier review (#3214 (comment)), the loop now yields with setImmediate and checks cancellation on every iteration, and a cancellation test covers it. That part is fixed.
One problem remains. repeat.while.visible and notVisible call port.observe() on a live snapshot before every iteration (https://github.com/callstack/agent-device/blob/e3f1ecf/packages/maestro/src/internal/replay-plan-step-execution.ts#L245). The only proof is a unit test whose mocked observe always returns matched: true. The PR body still says "Not tested on a device yet", and I see no run attached since the last review. So nothing shows that a real loop exits once the element appears, or that the observation is not served stale across iterations. Please run agent-device test <flow> --maestro on an iOS simulator or Android emulator with repeat: while: notVisible: X, where the body makes X appear after N taps. Paste the output showing exactly N body iterations, then the loop exit. Please also paste a times-bounded while.true: ${output.counter < 3} run that prints the final counter value.
The Copilot thread on engine.test.ts still applies (#3214 (comment)). The file grew from 877 to 1076 lines, which crosses the 1,000 line limit in docs/agents/testing.md. Please move the repeat-while cases into a sibling test module. Three cubic-dev-ai threads do not apply, so please resolve them. The unbounded while without times matches upstream Maestro, and the loop now yields and checks cancellation (#3214 (comment)). The ${output.counter} < 3 case becomes literal text after interpolation, which is the documented interpolate-then-truthy behavior (#3214 (comment)). Nothing read the interpolated runFlow label before or after this change (#3214 (comment)). I recalled the upstream behavior from memory and did not check upstream source.
I did not run tests or pnpm maestro:conformance. I judged regressions by reading the pre-change code paths. I also did not check whether the static runFlow planner accepts plain literals such as yes. The runtime fallback decides those cases either way.
Before merge, please attach the live simulator or emulator run and split engine.test.ts back under 1,000 lines.
This introduces JavaScript evaluation outside evalScript, but accepted ADR 0015 still states that ${...} is lookup-only outside evalScript, that evalScript is the sole JavaScript-evaluating command, and that trustedScripts gates only evalScript (docs/adr/0015-direct-maestro-engine.md:193-207). Update that decision (and the trustedScripts API comment) to record condition true: evaluation and its trust boundary; otherwise the architecture and security documentation contradict the implementation.
Device run on the iOS simulator (iPhone 18 Pro Max, iOS 27.0) at ecdc3986a (engine source unchanged since e3f1ecf95):
agent-device test repeat-while-not-visible.yaml repeat-while-true-counter.yaml --maestro --platform ios
✓ repeat while notVisible exits once the element appears 26.8s
✓ times-bounded repeat whiletrue stops at counter 3 2.06s
Test summary: 2 passed (2) in 28.8s
1. repeat: while: notVisible: X, where X appears after N=3 taps. The app under test is Safari showing a local page. Its Tap me button updates Taps: <n>, and on the 3rd tap it renders Target reached. The loop has a times: 10 ceiling, so a stale observation would show up as over-tapping or a timeout.
step 1 line 4 openLink ok=true 14058ms
step 2 line 5 assertVisible ok=true 387ms
step 3 line 6 evalScript ok=true 6ms
step 4 line 7 repeat ok=true 11806ms
step 5 line 14 assertVisible ok=true 216ms # Target reached
step 6 line 15 assertVisible ok=true 222ms # Taps: 3, counted by the page
step 7 line 16 evalScript ok=true 4ms
step 8 line 17 assertTrue ok=true 3ms # loop body ran exactly 3 times
The body ran exactly 3 times. The page itself shows Taps: 3, and the flow's own count output.taps == 3 agrees. The loop then exited once Target reached appeared. A 4th iteration would have produced Taps: 4 and failed step 6.
2. times-bounded while.true: ${output.counter < 3}. To show the final counter value on the device, the flow opens the same page with ?counter=${output.counter}. The page renders Final counter: <value>.
step 1 line 4 evalScript ok=true 34ms
step 2 line 5 repeat ok=true 5ms
step 3 line 11 openLink ok=true 1719ms
step 4 line 12 assertVisible ok=true 257ms # Final counter: 3
Android: I couldn't run this on the available emulator (API 37). The bundled snapshot helper crashes there before any flow step: hiddenapi: Accessing hidden field Instrumentation->mUiAutomation ... denied, then IllegalStateException: Cannot call disconnect() while connecting UiAutomation. That is an existing helper problem on API 37 and is unrelated to this PR. It happens on a plain agent-device snapshot too.
I found no problems in ecdc398, and the fixes from the earlier review at e3f1ecf are in. The new push only moves the repeat-while tests out of engine.test.ts into replay-plan-step-execution.test.ts, so engine.test.ts is back to 858 lines. I did not run the moved tests locally. I judged from the delta diff that they are identical. I also did not check the Android device route because of the API 37 helper crash you report. The changed engine code is platform-neutral. The device-run output is your own paste, and I did not reproduce it.
CI is green: the one check reported so far passes, and this push changes only tests. Cubic has not reviewed this head yet. No conflicts are known. Before merge, wait for the remaining checks and for Cubic's re-review of ecdc398.
On the other threads: the file-size thread (#3214 (comment)) is fixed at this head, so please resolve it. The unbounded while without times (#3214 (comment)) matches upstream Maestro. The loop yields and checks cancellation, and the cancellation test covers it, so please resolve it. The ${output.counter} < 3 interpolation thread (#3214 (comment)) describes the documented interpolate-then-truthy behavior, so please resolve it. The runFlow label thread (#3214 (comment)) is benign because nothing reads that label before or after this change, so please resolve it.
* origin/main: (77 commits)
fix(apple-runner): fence prep spawns behind a start-owned admission (callstack#3239)
0.21.22
test(apple): own the simctl settings plan tests in simctl-settings.test.ts (callstack#3244)
fix(limrun): report the session device id in iOS settings refusals (callstack#3243)
0.21.21
feat(remote): add a host-allocated macos-app lease backend (callstack#3236)
test(android): bound the screenshot write wait by wall time, not event-loop turns (callstack#3250)
feat(recording): cap the touch overlay frame rate at the caller's --fps (callstack#3241)
fix(ad-script): let .ad scripts carry scroll --until and wait capture flags (callstack#3197) (callstack#3234)
feat(provider-webdriver): keyboard enter, dismiss, and status over WebDriver (callstack#3233)
feat(selectors): match role= against snapshot kind with a node-scoped alias window (callstack#3232)
fix(provider-webdriver): read field values, placeholders, secure fields, and checked state from page source (callstack#3231)
feat(replay): accept --test-ime on test and replay so flow-owned Android opens opt into the test IME (callstack#3235)
refactor(daemon): route daemon-level diagnostics through one scope helper (callstack#3242)
docs(adr): correct ADR 0031 pointer event delivery evidence (callstack#3245)
fix(ios): stop a tap's post-gesture lookup from recording an XCTest failure (callstack#3060) (callstack#3237)
fix(recording): render the touch overlay at most 30 fps and inside the record request (callstack#3219)
fix(daemon): keep an idle daemon alive only for retained leases (callstack#3227)
fix(provider-webdriver): send an empty JSON object on bodyless POSTs (callstack#3230)
Feat/maestro repeat while (callstack#3214)
...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ready-for-humanValid work that needs human implementation, judgment, or maintainer merge
3 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Maestro flows that used
repeat.whilewere rejected with "repeat.while is not supported; use repeat.times". This adds support that follows the Maestrorepeatreference:repeatnow needstimes,while, or both.whileacceptsplatform,visible,notVisible, andtrue.timesis reached, whichever comes first.runFlow.when.trueexpressions run JavaScript through the same evaluator asevalScript. Results count as false using the same rule asassertTrue. So"false","0","","null", and"undefined"from string output values are false.output.*carry into later steps.while.trueis refused for flows received over the remote daemon connection, the same asevalScript.repeat-whileflow and regenerated the layer-1 fixtures. Updated the support matrix andreplay-e2e.md.19 files changed. No scope expansion.
Test plan
pnpm check:affected --runone4dc41a24and it passed.interaction-response-shape,apple-platform-output-guard). Both passed when run alone and on the rerun.whiletimesboundpnpm maestro:conformancepasses.agent-device replayon an iOS simulator or Android emulator.