fix(core): let BS hosts inject a complete Maestro scope root - #2357
fix(core): let BS hosts inject a complete Maestro scope root#2357rishigupta1599 wants to merge 1 commit into
Conversation
PR #2353 assumed the App Automate tmp-dir relocation only moved the root while preserving the {root}/{sessionId}{_test_suite} layout beneath it. That holds on Android, where the suite moved to <aa-tmp>/<device>/test-suites/<sid>_test_suite/logs/*/screenshots — so relocating the root via PERCY_APP_AUTOMATE_TMP_DIR composes correctly. It does not hold on iOS. realmobile's relocation moved Maestro debug output to <aa-tmp>/<device>/logs/maestro_debug_*, which drops the sessionId path segment entirely (the CLI appends it, so no tmp-root value can absorb <device>/logs) and drops the <device>_ prefix that the *_maestro_debug_* glob keys on. Screenshot lookups there 404 regardless of what PERCY_APP_AUTOMATE_TMP_DIR is set to. Add PERCY_MAESTRO_BS_SCOPE_ROOT for layouts the convention can no longer express. When set, the value IS the scope root: the relay globs {root}/**/{name}.png with no layout assumption, the manual-walker fallback recurses the same root, and the realpath containment check anchors on it — the security boundary is relocated, never widened. Non-absolute values are ignored so a malformed injection falls back to the composed convention rather than producing a cwd-relative root. Android and self-hosted (PERCY_MAESTRO_SCREENSHOT_DIR) are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
aryanku-dev
left a comment
There was a problem hiding this comment.
Claude Code Review (automated) — 5 inline finding(s). Full report in the PR comment below. Verdict: Passed.
| expect(payload.tiles[0].content).toBe(Buffer.from('PNGBYTES-ANDROID').toString('base64')); | ||
| }); | ||
|
|
||
| it('re-anchors filePath containment on the overridden root', async () => { |
There was a problem hiding this comment.
[Medium] No symlink-escape test for the overridden scope root
The self-hosted arm already has 404s when a globbed file resolves outside the root (symlink escape); this override block has no analogue. The containment code is shared, so present-day risk is low — but a future edit that shortcuts realpath for the "host-trusted" root would go undetected, and re-anchored containment is the property this PR explicitly claims.
Suggestion: Add a spec here that places a symlink inside REALMOBILE_DIR pointing outside SCOPE_ROOT, then asserts a 404 matching /resolved outside session dir/.
Reviewer: stack-code-reviewer
| scopeRoot = overrideRoot; | ||
| recursiveScope = true; | ||
| percy.log.debug(`maestro screenshot scope root overridden: ${scopeRoot}`); | ||
| } else { |
There was a problem hiding this comment.
[Low] A rejected override falls through silently
When PERCY_MAESTRO_BS_SCOPE_ROOT is set but rejected by bsScopeRootOverride() (non-absolute, or reducing to /), this branch composes the convention root with no log line. The accepted path logs at debug; the rejected path is silent, so a host-side typo surfaces only as 404s indistinguishable from an ordinary missing file.
Suggestion: Emit percy.log.warn here when process.env.PERCY_MAESTRO_BS_SCOPE_ROOT is truthy but the helper returned null, mirroring the existing debug log.
Reviewer: stack-code-reviewer
| let raw = process.env.PERCY_MAESTRO_BS_SCOPE_ROOT; | ||
| if (!raw) return null; | ||
| let dir = raw.replace(/[/\\]+$/, ''); | ||
| return path.isAbsolute(dir) ? dir : null; |
There was a problem hiding this comment.
[Low] Rejection of root / is incidental to the trim regex, not an explicit guard
'/' is rejected only because the trailing-separator strip turns it into '', which then fails path.isAbsolute. The behaviour is pinned by a test, but the safety property — an override can never widen the root to the whole filesystem — is incidental in the code, so a future refactor of the trim could silently reintroduce it.
Suggestion: Record the intent at the return, e.g. // '/' trims to '' and is rejected here — intentional; prevents an override that would recurse the whole filesystem.
Reviewer: stack-code-reviewer
| })).toBeRejectedWithError(/Screenshot not found/); | ||
| }); | ||
|
|
||
| it('404s when the overridden root does not exist', async () => { |
There was a problem hiding this comment.
[Low] No multi-match mtime tie-break test for the override glob
recursiveScope routes the override through the same files.length > 1 mtime-descending sort, but no spec exercises it. This matters more here than for the convention glob precisely because the layout constraint was removed: a same-named PNG from a second flow anywhere under the session root can now match.
Suggestion: Add a spec with two ${SS_NAME}.png fixtures at different depths and mtimes under SCOPE_ROOT, asserting the newer one is chosen.
Reviewer: stack-code-reviewer
| // customer config, so a stale/missing root should surface as the same 404 | ||
| // the containment check emits rather than a 400 aimed at the customer. | ||
| let overrideRoot = bsScopeRootOverride(); | ||
| if (overrideRoot) { |
There was a problem hiding this comment.
[Low] Override applies to Android too — worth documenting on the host side
Design note, not a defect. The override is applied regardless of platform, so an Android session under it also loses the _test_suite/logs/*/screenshots structural guard. That is intentional and tested (applies to android too), but the PR description frames the feature as iOS-only, and nothing in code prevents a host from setting the var too broadly and quietly loosening Android's guard.
Suggestion: State in the BS-host runbook that this is set per-invocation for iOS sessions, not host-wide, unless Android's layout has also changed.
Reviewer: stack-code-reviewer
Claude Code PR ReviewPR: #2357 • Head: 0b084ed • Reviewers: stack-code-reviewer SummaryAdds Review Table
Findings1.
2.
3.
4.
5.
Verdict: PASS — the security boundary is genuinely relocated rather than widened, and the reasoning is well documented; the open items are test-coverage and observability gaps, none gating. Please confirm |
Summary
Follow-up to #2353. That PR assumed the App Automate tmp-dir relocation only moved the root while preserving the
{root}/{sessionId}{_test_suite}layout beneath it. That holds on Android but not on iOS.PERCY_APP_AUTOMATE_TMP_DIRenough?<aa-tmp>/<device>/test-suites/<sid>_test_suite/logs/*/screenshots/{TMP}/{sid}_test_suite/logs/*/screenshots/<aa-tmp>/<device>/logs/maestro_debug_*/{TMP}/{sid}/*_maestro_debug_*/The iOS layout drops the
sessionIdpath segment entirely — the CLI appends it, so no tmp-root value can absorb<device>/logs— and drops the<device>_prefix that the*_maestro_debug_*glob keys on. Lookups there 404 no matter whatPERCY_APP_AUTOMATE_TMP_DIRis set to.What this adds
PERCY_MAESTRO_BS_SCOPE_ROOT, for layouts the convention can no longer express. When set, the value is the scope root:{root}/**/{name}.pngwith no layout assumptionThe security boundary is relocated, never widened — an out-of-root
filePathstill 404s. Non-absolute values are ignored so a malformed injection falls back to the composed convention rather than producing a cwd-relative root. Android (PERCY_APP_AUTOMATE_TMP_DIR) and self-hosted (PERCY_MAESTRO_SCREENSHOT_DIR) are untouched.No existence pre-check on the override, unlike self-hosted: this is host config, not customer config, so a stale root should surface as the same 404 the containment check emits rather than a 400 aimed at the customer.
Host side
browserstack/realmobile— injectsPERCY_MAESTRO_BS_SCOPE_ROOT(needs this PR released)browserstack/mobile— injectsPERCY_APP_AUTOMATE_TMP_DIR, fixed by fix(core): follow BS App Automate tmp dir relocation for Maestro screenshots #2353 aloneBoth host PRs pin 1.32.5 today; realmobile needs a second bump once this ships.
Test plan
8 new specs in
packages/core/test/api.test.js: locating a screenshot the platform convention can't reach, android under the same override, override winning overPERCY_APP_AUTOMATE_TMP_DIR, trailing-slash tolerance, non-absolute fallback,filePathcontainment re-anchored on the override (in-root resolves, out-of-root 404s), missing root 404, plus direct trim/null assertions on the exported helper.Local run caveat, stated plainly: every real-fs-fixture spec in
api.test.jsfails on my machine withENOTEMPTYfrom theafterEachrmSync, including blocks this PR doesn't touch. Measured baseline on clean master: 15 failures, allENOTEMPTY. With this PR: 23 — a delta of exactly the 8 new specs, same environmental cause, no new failure mode. Those blocks need CI to actually be exercised.Because of that, I verified the logic directly against the real filesystem using the exact relocated layouts — 7/7: helper trim/absolute semantics, #2353-alone still 404ing on the realmobile layout, explicit root locating the file, out-of-root
filePathrejected, in-root accepted; plus a separate check that the Android glob still composes correctly fromPERCY_APP_AUTOMATE_TMP_DIR.ESLint clean on all three changed files. (
yarn lintfor the whole package fails environmentally here — 54 identical "No Babel config file detected" errors across every test file, untouched ones included.)Ref: PER-10040, AAP-18965
🤖 Generated with Claude Code