Hi. Thanks for your great runner! This is the issue I faced today, and decided to submit. It's documented by claude but is valid and was really hard to debug!
Description
runScript in maestro-runner mutates a snapshot of output, not the persistent bag. Object/array property mutations like output.list.push(x) are silently discarded across runScript calls — and even within the same script, reading back the value returns the unmutated snapshot. Scalar reassignments (output.foo = "x") DO persist.
This breaks any flow that builds up state across multiple runScript invocations using the documented output.<field> API. The same flows work in upstream maestro because Maestro's JS bridge exposes a live output reference.
Steps to Reproduce
Note on the IIFE wrapping below: maestro-runner reuses the same JS context across runScript calls — top-level const word = … in a script that runs more than once throws SyntaxError: Identifier 'word' has already been declared on the second invocation (vanilla maestro gives each call a fresh scope, so this works there). I wrap each script in an IIFE (function () { … })() to keep const locally scoped and avoid the collision while reproducing the actual output bug. The wrapping is unrelated to the persistence issue and is needed regardless.
-
Create three scripts:
// init.js
(function () {
output.list = []
output.list.push("a")
console.log("[init] output.list =", JSON.stringify(output.list))
})()
// push.js
(function () {
output.list.push("b")
console.log("[push] output.list =", JSON.stringify(output.list))
})()
// check.js
(function () {
console.log("[check] output =", JSON.stringify(output))
})()
-
Flow:
appId: <any>
---
- runScript: init.js
- runScript: push.js
- runScript: check.js
-
Run with maestro-runner -p ios --device <udid> --app-file <app> test probe.yaml (also reproduces on Android with appropriate args).
Expected Behavior
[init] output.list = ["a"]
[push] output.list = ["a","b"]
[check] output = {"list":["a","b"]}
(Matches upstream maestro behavior.)
Actual Behavior
[init] output.list = []
[push] output.list = []
[check] output = {"list":[]}
The .push() mutation is invisible even one line later within the same script. The only way to persist a non-scalar is to reassign the whole value:
(function () {
const list = output.list
list.push("a")
output.list = list // reassign — only this persists
})()
Environment
- OS: macOS 26.4 (Darwin 25.4.0, arm64)
- maestro-runner version: 1.1.13 (commit 7addd21)
- Executor: Native (UIAutomator2 / WDA)
- Device/Simulator: iPhone 17e (iOS 26.4 Simulator) — also reproduced on Pixel 9 API 35 emulator
Flow File
appId: org.example.app
---
- runScript: init.js
- runScript: push.js
- runScript: check.js
Error Output
No error — silent data loss. Downstream tapOn: ${output.list[0]} resolves to empty string, which Maestro treats as a tap with empty selector and the test "passes" while doing nothing meaningful.
Additional Context
- Real-world impact: a wallet test reads 24 mnemonic words via
copyTextFrom + runScript push, then runScript answer.js computes a verification answer from output.words. Under maestro-runner, output.words ends empty, so output.answerWords is empty, and the three tapOn: ${output.answerWords[N]} taps fire with empty selectors. Test reports passing taps but the app screen never advances.
- Two distinct semantics issues are entangled here: (a) shared JS context across
runScript calls (causes const collisions, fixable with IIFE), (b) output reads return frozen/cloned snapshots so in-place mutations are invisible (cannot be worked around inside the script — must reassign the whole value).
- Workaround in scripts: wrap in IIFE to dodge (a), and always reassign the whole value (
output.list = list after mutating) to dodge (b).
Hi. Thanks for your great runner! This is the issue I faced today, and decided to submit. It's documented by claude but is valid and was really hard to debug!
Description
runScriptin maestro-runner mutates a snapshot ofoutput, not the persistent bag. Object/array property mutations likeoutput.list.push(x)are silently discarded acrossrunScriptcalls — and even within the same script, reading back the value returns the unmutated snapshot. Scalar reassignments (output.foo = "x") DO persist.This breaks any flow that builds up state across multiple
runScriptinvocations using the documentedoutput.<field>API. The same flows work in upstreammaestrobecause Maestro's JS bridge exposes a liveoutputreference.Steps to Reproduce
Create three scripts:
Flow:
Run with
maestro-runner -p ios --device <udid> --app-file <app> test probe.yaml(also reproduces on Android with appropriate args).Expected Behavior
(Matches upstream
maestrobehavior.)Actual Behavior
The
.push()mutation is invisible even one line later within the same script. The only way to persist a non-scalar is to reassign the whole value:Environment
Flow File
Error Output
No error — silent data loss. Downstream
tapOn: ${output.list[0]}resolves to empty string, which Maestro treats as a tap with empty selector and the test "passes" while doing nothing meaningful.Additional Context
copyTextFrom+runScriptpush, thenrunScriptanswer.js computes a verification answer fromoutput.words. Under maestro-runner,output.wordsends empty, sooutput.answerWordsis empty, and the threetapOn: ${output.answerWords[N]}taps fire with empty selectors. Test reports passing taps but the app screen never advances.runScriptcalls (causesconstcollisions, fixable with IIFE), (b)outputreads return frozen/cloned snapshots so in-place mutations are invisible (cannot be worked around inside the script — must reassign the whole value).output.list = listafter mutating) to dodge (b).