Skip to content

fix(core): follow BS App Automate tmp dir relocation for Maestro screenshots#2353

Open
prklm10 wants to merge 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Open

fix(core): follow BS App Automate tmp dir relocation for Maestro screenshots#2353
prklm10 wants to merge 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir

Conversation

@prklm10

@prklm10 prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories from /tmp/{sessionId} to /usr/local/.browserstack/app-automate-tmp/{sessionId}, breaking the Maestro screenshot relay's hardcoded /tmp globs and scope roots — screenshot lookups started 404'ing.
  • All BrowserStack-mode paths (glob patterns, manual-walker fallback, and the scopeRoot used by the realpath containment check) now route through a new appAutomateTmpDir() helper.
  • The helper reads a new PERCY_APP_AUTOMATE_TMP_DIR env var, defaulting to /usr/local/.browserstack/app-automate-tmp, so infra can relocate the root again without a CLI release. Trailing separators are trimmed and backslashes normalized for the glob.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs updated to the new default root (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, and filePath containment re-scoping to the overridden root (default-root files 404).
  • Full packages/core/test/api.test.js run locally: 126/127 pass; the single failure (when the server is disabled does not start a server with #start(), ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue, unrelated to this change.

🤖 Generated with Claude Code

…enshots

BrowserStack hosts moved the App Automate session directories from
/tmp/{sessionId} to /usr/local/.browserstack/app-automate-tmp/{sessionId},
breaking the Maestro screenshot relay's hardcoded /tmp globs and scope
roots (screenshot lookups 404'd).

Route every BS-mode path through a new appAutomateTmpDir() helper that
reads PERCY_APP_AUTOMATE_TMP_DIR and defaults to
/usr/local/.browserstack/app-automate-tmp, so infra can relocate the
root again without a CLI release. Self-hosted mode is untouched
(PERCY_MAESTRO_SCREENSHOT_DIR).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10 prklm10 added the 🐛 bug Something isn't working label Jul 27, 2026
@prklm10
prklm10 marked this pull request as ready for review July 27, 2026 05:06
@prklm10
prklm10 requested a review from a team as a code owner July 27, 2026 05:06

@prklm10 prklm10 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude Code Review (automated) — 5 inline finding(s). Full report in the PR comment below. Verdict: Passed.

// BrowserStack-mode only — self-hosted scoping stays on
// PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/usr/local/.browserstack/app-automate-tmp';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Medium] Rollout-skew risk: no legacy /tmp fallback

If any BS host fleet segment (Linux/Android vs macOS/iOS are separate fleets) still writes to /tmp/{sid}, this CLI version globs only the new root and hard-404s — and non-relocated hosts by definition run older injection that won't set PERCY_APP_AUTOMATE_TMP_DIR=/tmp.

Suggestion: Confirm the relocation is fleet-wide for both Android and iOS hosts before release (stragglers can inject PERCY_APP_AUTOMATE_TMP_DIR=/tmp), or add a legacy fallback that retries the /tmp pattern (with matching scopeRoot) when the new-root glob is empty and the env var is unset.

Reviewer: stack:code-reviewer

// PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/usr/local/.browserstack/app-automate-tmp';
return dir.replace(/[/\\]+$/, '');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Low] No shape validation on PERCY_APP_AUTOMATE_TMP_DIR

Asymmetric with PERCY_MAESTRO_SCREENSHOT_DIR, which gets absolute-path + existing-dir checks with actionable 400s. A relative value here yields a cwd-relative glob surfacing only as an opaque 404; / trims to '', silently making the filesystem root the tmp root. Not a security issue (env is host-injected; containment fails closed), but host misconfiguration will be painful to debug.

Suggestion: After trimming, fall back to the default (optionally with a warn log) when the value is empty or not absolute.

Reviewer: stack:code-reviewer

scopeRoot = platform === 'ios'
? `/tmp/${sessionId}`
: `/tmp/${sessionId}_test_suite`;
? `${appAutomateTmpDir()}/${sessionId}`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Low] scopeRoot and glob root resolved by separate env reads

The containment boundary (scopeRoot) and the glob search root come from independent appAutomateTmpDir() calls; a mid-request env change would make them diverge. Divergence fails closed (404), so this is coherence-only.

Suggestion: Optionally resolve the root once in the handler and derive the glob root from the already-passed scopeRoot, as the self-hosted branch does.

Reviewer: stack:code-reviewer

// Real-fs root (matched by the top-level $bypass) — fast-glob caches its
// fs bindings on first import, so a memfs-only root created in a later
// test is invisible to it; real-fs fixtures sidestep the staleness.
describe('PERCY_APP_AUTOMATE_TMP_DIR override', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Low] Override tests cover the Android branch only

The iOS glob and scopeRoot expressions are never exercised under an override (low risk — same tmpRoot expression as Android).

Suggestion: Optionally add one iOS spec mirroring the Android override test, with a fixture under ${CUSTOM_ROOT}/${SID}/emu_maestro_debug_abc/....

Reviewer: stack:code-reviewer

expect(payload.tiles[0].content).toBe(Buffer.from('PNGBYTES-CUSTOM-ROOT').toString('base64'));
});

it('tolerates a trailing slash on the override', async () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Low] Trailing-slash spec doesn't strictly pin the trim behavior

fast-glob normalizes // in patterns and realpath collapses duplicate separators, so this spec would likely pass even without the replace(/[/\\]+$/, '') trim — it documents intent more than it guards the code.

Suggestion: Optionally add a direct unit assertion on the exported helper, e.g. expect(appAutomateTmpDir()).toBe(CUSTOM_ROOT) with the env set to ${CUSTOM_ROOT}///.

Reviewer: stack:code-reviewer

@prklm10

prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Claude Code PR Review

PR: #2353Head: f28af57Reviewers: stack:code-reviewer

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories from /tmp/{sessionId} to /usr/local/.browserstack/app-automate-tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) now route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (default /usr/local/.browserstack/app-automate-tmp); self-hosted mode is unchanged.

Review Table

Priority Category Check Status Notes
High Security No hardcoded secrets or credentials Pass Only a path constant; env var is host-injected, never request-controlled
High Security Authentication/authorization checks present N/A Local relay endpoint; no auth surface changed
High Security Input validation and sanitization Pass SAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
High Security No IDOR — resource ownership validated Pass Cross-session containment re-verified under the overridden root by a new test
High Security No SQL injection (parameterized queries) N/A No database code
High Correctness Logic is correct, handles edge cases Pass Trailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
High Correctness Error handling is explicit, no swallowed exceptions Pass Missing screenshots still surface as actionable 404s with the searched pattern
High Correctness No race conditions or concurrency issues Pass scopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
Medium Testing New code has corresponding tests Pass 3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
Medium Testing Error paths and edge cases tested Pass Containment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
Medium Testing Existing tests still pass (no regressions) Pass Full api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
Medium Performance No N+1 queries or unbounded data fetching N/A No data-fetching changes
Medium Performance Long-running tasks use background jobs N/A Not applicable
Medium Quality Follows existing codebase patterns Pass Mirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
Medium Quality Changes are focused (single concern) Pass 3 files, all scoped to the tmp-root relocation
Low Quality Meaningful names, no dead code Pass appAutomateTmpDir() is descriptive; no dead code introduced
Low Quality Comments explain why, not what Pass Comments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
Low Quality No unnecessary dependencies added Pass No dependency changes

Findings

  • File: packages/core/src/maestro-screenshot-file.js:13

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Rollout-skew risk: no legacy /tmp fallback. If any BS host fleet segment (Linux/Android vs macOS/iOS are different fleets) still writes to /tmp/{sid}, this CLI version globs only the new root and hard-404s; non-relocated hosts by definition run older injection that won't set PERCY_APP_AUTOMATE_TMP_DIR=/tmp.

  • Suggestion: Confirm the relocation is fleet-wide for both Android and iOS hosts before release (and note the PERCY_APP_AUTOMATE_TMP_DIR=/tmp escape hatch for stragglers), or add a legacy fallback that retries the /tmp pattern (with matching scopeRoot) when the new-root glob is empty and the env var is unset.

  • File: packages/core/src/maestro-screenshot-file.js:14

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: No shape validation on PERCY_APP_AUTOMATE_TMP_DIR, asymmetric with PERCY_MAESTRO_SCREENSHOT_DIR (which gets absolute-path + existing-dir checks with actionable 400s). A relative value yields a cwd-relative glob surfacing only as an opaque 404; / trims to '', silently making the filesystem root the tmp root. Not a security issue (env is host-injected; containment fails closed), but host misconfiguration will be painful to debug.

  • Suggestion: Validate in appAutomateTmpDir(): after trimming, fall back to the default (optionally with a warn log) when the value is empty or not absolute.

  • File: packages/core/src/maestro-screenshot.js:130

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: scopeRoot (containment boundary) and the glob search root are computed by independent appAutomateTmpDir() env reads; a mid-request env change would make them diverge. Divergence fails closed (404), so this is coherence-only.

  • Suggestion: Optionally resolve the root once in the handler and derive the glob root from the already-passed scopeRoot, as the self-hosted branch does.

  • File: packages/core/test/api.test.js:1838

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override tests cover the Android branch only; the iOS glob and scopeRoot expressions are never exercised under an override (low risk — same tmpRoot expression as Android).

  • Suggestion: Optionally add one iOS spec mirroring the Android override test, with a fixture under ${CUSTOM_ROOT}/${SID}/emu_maestro_debug_abc/....

  • File: packages/core/test/api.test.js:1868

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The trailing-slash spec would likely pass even without the trim (fast-glob normalizes // and realpath collapses duplicate separators) — it documents intent more than it guards the code.

  • Suggestion: Optionally add a direct unit assertion on the exported helper, e.g. expect(appAutomateTmpDir()).toBe(CUSTOM_ROOT) with the env set to ${CUSTOM_ROOT}///.

Informational (not counted against the verdict): the fixed real-fs fixture name percy-bs-tmp-real-root in os.tmpdir() could collide across concurrent suite runs on one machine (consistent with the pre-existing percy-self-hosted-real convention); one unrelated pre-existing flake (page discovery … captures requests from workers) was observed during the local full-suite run and is not attributable to this change.


Verdict: PASS — approved by stack:code-reviewer; 1 Medium deployment-verification question (fleet-wide relocation confirmation) and 4 non-blocking polish items.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant