Skip to content

[build-tools] Allow running Maestro tests with maestro-runner - #4187

Merged
sjchmiela merged 7 commits into
mainfrom
stanley/maestro-runner-tests
Aug 19, 2026
Merged

sjchmiela merged 7 commits into
mainfrom
stanley/maestro-runner-tests

Conversation

@sjchmiela

@sjchmiela sjchmiela commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Why

maestro-runner promises to be faster, more reliable alternative to the official Maestro CLI.

How

Added support for EAS_MAESTRO_BACKEND and/or input to eas/maestro_tests which opts in to using maestro-runner over maestro command.

maestro-runner doesn't support DADB, but we don't error if both are enabled.

maestro-runner has slightly different screenshots and JUnit format -- both are handled. It also exposes a nicer JSON file that lets us parse failed flows without having to parse JUnit file.

This adds relatively big chunk of code, but it's all guarded by backend === maestro-runner so maybe it's not so bad? I considered splitting logic into MaestroCliUtils and MaestroRunnerUtils and expose similar interfaces from them, but I feel it was too complicated.

Test Plan

@sjchmiela sjchmiela added the no changelog PR that doesn't require a changelog entry label Aug 13, 2026
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.17647% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.73%. Comparing base (7cec953) to head (446c8ac).

Files with missing lines Patch % Lines
...ld-tools/src/steps/functions/maestroScreenshots.ts 84.85% 10 Missing ⚠️
...es/build-tools/src/steps/functions/maestroTests.ts 94.80% 5 Missing ⚠️
...d-tools/src/steps/functions/maestroResultParser.ts 92.86% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4187      +/-   ##
==========================================
+ Coverage   63.63%   63.73%   +0.10%     
==========================================
  Files        1030     1030              
  Lines       47282    47448     +166     
  Branches     9926     9977      +51     
==========================================
+ Hits        30085    30237     +152     
- Misses      17096    17110      +14     
  Partials      101      101              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from 5f3a29b to 6d821dd Compare August 13, 2026 20:38
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch 2 times, most recently from f2d4996 to a594330 Compare August 13, 2026 20:53
@sjchmiela sjchmiela changed the title [build-tools] Run Maestro tests with maestro-runner [build-tools] Allow running Maestro tests with maestro-runner Aug 13, 2026
Base automatically changed from stanley/maestro-runner-install to main August 14, 2026 07:28
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch 2 times, most recently from 61db12a to f2afd26 Compare August 14, 2026 07:59
@sjchmiela
sjchmiela changed the base branch from main to stanley/maestro-runner-xcode-compat August 14, 2026 08:01
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from f2afd26 to de87b9c Compare August 14, 2026 11:12
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from de87b9c to 65414f2 Compare August 14, 2026 11:24
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from 65414f2 to 39e37ff Compare August 14, 2026 11:31
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from 39e37ff to 40892df Compare August 14, 2026 11:44
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from 40892df to ff2750d Compare August 14, 2026 11:48
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from ff2750d to b20c996 Compare August 14, 2026 11:51
@sjchmiela sjchmiela changed the title [build-tools] Allow running Maestro tests with maestro-runner [build-tools] Allow running Maestro tests with maestro-runner Aug 14, 2026
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch 4 times, most recently from 10d0c3f to 069a3c1 Compare August 14, 2026 12:46
@sjchmiela
sjchmiela marked this pull request as ready for review August 14, 2026 12:55
@sjchmiela
sjchmiela requested a review from hSATAC August 14, 2026 12:55
Comment on lines +196 to +199
const backend = resolveMaestroBackend({
input: inputs.backend.value,
env,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveMaestroBackend can throw before the required outputs are published. If the input or EAS_MAESTRO_BACKEND is invalid, the downstream if: always() upload step may fail to interpolate those outputs and obscure the original error.

This seems to be exactly what the “Outputs are published BEFORE any throw below” comment is meant to prevent. Nothing before the output assignments uses backend, so could we move this call below them?

Non-blocking—just moving the call should be enough.

Comment on lines +575 to +587
case 'maestro-runner': {
const results = await Promise.all(
reportDirectories.map(directory => parseMaestroRunnerReport(directory))
);
flowResults = results.some(result => result === null)
? null
: results.flatMap(result => result?.flows ?? []);
break;
}
}
if (flowResults === null) {
logger.warn('Failed to classify failure screenshots; skipping upload.');
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runner screenshot classification is currently all-or-nothing: if any attempt’s report.json cannot be parsed, all previously harvested screenshots are skipped.

For example, an earlier attempt could fail normally and collect screenshots, then the final retry crashes before writing report.json, causing all of them to be discarded. The Maestro path degrades and still uploads, so the comment above about fully failed runs still uploading screenshots does not hold for the runner path.

Could we use the reports that can be parsed, or fall back to uploading everything? The 30-screenshot cap should keep that bounded.

Non-blocking, but I think it’s worth addressing in this PR.

Comment on lines +154 to +171
let report: {
flows?: { name?: string; status?: string; dataFile?: string }[];
};
try {
report = JSON.parse(await fs.readFile(path.join(args.reportDirectory, 'report.json'), 'utf8'));
} catch (err: any) {
args.logger.info(
{ err },
`Skipping maestro-runner screenshot harvest: cannot read ${args.reportDirectory}.`
);
return [];
}

const shots: HarvestedScreenshot[] = [];
for (const flow of report.flows ?? []) {
if (flow.status !== 'failed' || !flow.name || !flow.dataFile) {
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think the “Never throws” guarantee currently holds because report.flows is accessed outside the try.

Valid JSON with an unexpected shape, such as null, { "flows": 5 }, or { "flows": [null] }, can throw here. Since the caller does not catch it, a screenshot issue could fail the step and obscure the test result.

Could we move the loop into the try or validate the shape before iterating?

Non-blocking, small fix.

hSATAC commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

On the “maybe it’s not so bad?” point: I think the current structure is fine for now. maestro-runner is still being validated, so adding it behind backend guards while keeping the existing Maestro path unchanged seems like the lowest-cost approach. I wouldn’t ask to restructure this PR.

If we decide to support both backends long-term, I’d prefer to keep the overall flow shared and resolve the backend-specific runtime once before the loop, instead of adding backend checks throughout. The divergence is still small, but there are already some signs of it: the runner path drops all screenshots if any report is unusable while the Maestro path degrades, and the retry logic has an unreachable branch just for TypeScript narrowing.

Not something we need to do in this PR, but I’d prefer to refactor before the two paths diverge further.

@hSATAC hSATAC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more non-blocking comment.

@@ -82,6 +88,17 @@ function parseJUnitContent(content: string): JUnitTestCaseResult[] {
: (tc.error?.['#text'] ?? null)
: null;
const errorMessage: string | null = failureText ?? errorText ?? null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From junit.go’s failure element construction, the <failure> message attribute contains the actual error from entry.Error, while resolveFailure puts the command label/type in the body.

This currently reads #text, so the runner’s errorMessage may be something like tapOn instead of the actual element not found: ... error, which will also be exposed through the API and website.

Could we prefer @_message and fall back to #text? Official Maestro only uses the body, so its behavior should remain unchanged.

Non-blocking.

Base automatically changed from stanley/maestro-runner-xcode-compat to main August 18, 2026 09:31
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from 3a0293f to f48b2eb Compare August 18, 2026 09:42
@sjchmiela
sjchmiela requested a lite review from Copilot August 18, 2026 10:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the eas/maestro_tests build-step function in @expo/build-tools to support running Maestro tests via an alternate backend (maestro-runner) selected by EAS_MAESTRO_BACKEND and/or a step input. It updates the execution, retry subsetting, and artifact/screenshot parsing paths to accommodate maestro-runner’s output formats while keeping existing Maestro behavior intact.

Changes:

  • Added backend selection (maestro vs maestro-runner) and wired it into spawning, retry logic, and logs.
  • Added maestro-runner-specific parsing for report.json (failed flows + flow results) and screenshot harvesting.
  • Expanded Jest coverage for backend selection, retry behavior, and the new parsers/harvesters.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/build-tools/src/steps/functions/maestroTests.ts Adds backend selection, runner-specific execution/output handling, and integrates runner parsing/harvesting.
packages/build-tools/src/steps/functions/maestroScreenshots.ts Implements maestro-runner screenshot harvesting from report.json bundles.
packages/build-tools/src/steps/functions/maestroResultParser.ts Extends JUnit parsing for runner differences and adds report.json parsing helpers.
packages/build-tools/src/steps/functions/tests/maestroTests.test.ts Adds tests covering backend selection, runner invocation, retry subsetting, and runner artifact handling.
packages/build-tools/src/steps/functions/tests/maestroScreenshots.test.ts Adds tests for harvesting runner failure screenshots from report bundles.
packages/build-tools/src/steps/functions/tests/maestroResultParser.test.ts Adds tests for report.json parsing + runner JUnit file-property handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/build-tools/src/steps/functions/maestroScreenshots.ts
Comment thread packages/build-tools/src/steps/functions/maestroTests.ts
Comment thread packages/build-tools/src/steps/functions/maestroResultParser.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/build-tools/src/steps/functions/maestroTests.ts:238

  • backend resolution currently allows an explicit empty string (e.g. a workflow interpolation that resolves to "") to be treated as “not provided”, which can silently fall back to EAS_MAESTRO_BACKEND/default instead of failing validation. This makes misconfiguration harder to detect and can unexpectedly change which binary is invoked.
      const backend = resolveMaestroBackend({
        input: inputs.backend.value,
        env,
      });

@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from fdb1df6 to f070ae5 Compare August 19, 2026 12:02
sjchmiela and others added 7 commits August 19, 2026 17:42
- Resolve the maestro backend after publishing outputs so an invalid backend
  input can't throw before `if: always()` upload steps can interpolate them.
- Keep runner failure screenshots from attempts that reported when a later
  attempt's report.json is unparseable, instead of discarding all of them.
- Guard the runner screenshot report shape so a malformed report.json can't
  throw out of the harvest that promises never to throw.
- Prefer the JUnit `<failure>`/`<error>` message attribute over the body so the
  runner's real error surfaces instead of the command label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hand-rolled report shape check and the flow-detail `as` cast with
zod schemas: a recursive command schema (zod v4 getter) drives the flow-detail
parse, and a tolerant report schema yields no flows on an unexpected shape
instead of throwing. Add regression cases for malformed report.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es to Sentry

The zod schemas are expected to always match maestro-runner's output, so a
validation failure is an anomaly worth surfacing: capture it in Sentry (still
without throwing) for both report.json and the per-flow detail file. Drop the
verbose schema comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reject an exact `..` in resolvePathInsideDirectory, which previously resolved
  to the parent directory and slipped past the containment guard.
- Clear the deterministic maestro-runner output directory before each attempt
  so a crash before it writes fresh output can't resurrect stale results.
- Exclude skipped JUnit testcases (<skipped/>) instead of counting them as
  passed, matching the report.json path which already drops skipped flows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sjchmiela
sjchmiela force-pushed the stanley/maestro-runner-tests branch from f070ae5 to 446c8ac Compare August 19, 2026 15:42
@sjchmiela sjchmiela added the ai-review Commits pushed to PRs with this label be automatically reviewed. label Aug 19, 2026
@github-actions

Copy link
Copy Markdown

⏩ The changelog entry check has been skipped since the "no changelog" label is present.

@github-actions

Copy link
Copy Markdown

🤖 AI code review

Decision: Ready for human review (with comments)

Overall PR risk: Medium. The change modifies an existing, shared build step (maestroTests.ts in build-tools) by adding a new opt-in maestro-runner backend path, so the surface touched is a production build step even though the new code is guarded by a backend check. No cross-cutting or security reviewer flagged broader integration risk. The one confirmed finding is a correctness warning: an infra-level error while copying the maestro-runner JUnit report is swallowed as a warning instead of failing the step, which could let a build report success while missing JUnit data for that attempt. This is a bounded, single-path issue with a direct fix, not a systemic risk.

🟡 Warning (1)

  • maestro-runner JUnit copy turns infra errors into warningspackages/build-tools/src/steps/functions/maestroTests.ts:405 (correctness) · id:a57f0e03f4b8
    Confidence: High — the catch block at the flagged line has no error-code check and always logs a warning, then continues.
    Impact if shipped: Medium — an infra-level failure while copying the maestro-runner JUnit report gets silently dropped, so the build can finish with no usable JUnit or final report and no infra-fault signal.
    Suggested remediation: Reuse isFilesystemError(err) in this catch block. Throw SystemError('Failed to collect the maestro-runner JUnit report', { cause: err }) when it matches, matching the mergeJUnitReports handling. Keep the log-and-continue behavior only for other (data-issue) errors.

    Evidence and reasoning

    packages/build-tools/src/steps/functions/maestroTests.ts defines isFilesystemError (lines 59-67). This function tells an infra fault (ENOSPC/EACCES/EROFS/EIO/EPERM) apart from a data issue (ENOENT). The code uses this helper at line 520 to turn a mergeJUnitReports filesystem fault into a SystemError.

    The new maestro-runner JUnit-collection block does not use this helper. The line await fs.copyFile(path.join(runnerOutputDirectory, 'junit-report.xml'), outputPath); sits inside a bare catch (err: any). This catch always calls logger.warn and returns, no matter the error code.

    A real infra fault here, for example the build worker's disk fills up, gets treated the same as the expected case. In the expected case, maestro-runner crashed before it wrote a report. The infra fault never becomes a SystemError and never fails the step on its own. lastAttemptExitCode is set independently by the spawn call. So a run where the maestro-runner process exits 0 but the JUnit copy fails this way reports the step as successful. It silently misses that attempt's JUnit data, which feeds final_report_path and the screenshot upload logic.


This review is advisory — it never blocks a merge and never auto-approves.

@sjchmiela
sjchmiela merged commit 1c08266 into main Aug 19, 2026
13 checks passed
@sjchmiela
sjchmiela deleted the stanley/maestro-runner-tests branch August 19, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Commits pushed to PRs with this label be automatically reviewed. no changelog PR that doesn't require a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants