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

Merged
prklm10 merged 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Jul 27, 2026
Merged

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

Conversation

@prklm10

@prklm10prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories away from /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 — injected by the host with the relocated root — and falls back to the legacy /tmp convention when unset, so no internal infra path is hardcoded in the CLI and hosts with older injection keep working during rollout. Trailing separators are trimmed, backslashes normalized for the glob, and non-absolute values fall back to /tmp.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing against the default root.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, non-absolute fallback to /tmp, direct trim/fallback assertions on the exported helper, and filePath containment re-scoping to the overridden root.
  • Full packages/core/test/api.test.js run locally: all maestro specs pass; the single unrelated failure (when the server is disabled…, ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue.

🤖 Generated with Claude Code

@prklm10prklm10 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 ownerJuly 27, 2026 05:06

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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 the relocated App Automate session root: 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 the relocated root); self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassOnly a path constant; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
HighSecurityNo IDOR — resource ownership validatedPassCross-session containment re-verified under the overridden root by a new test
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
HighCorrectnessError handling is explicit, no swallowed exceptionsPassMissing screenshots still surface as actionable 404s with the searched pattern
HighCorrectnessNo race conditions or concurrency issuesPassscopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
MediumTestingNew code has corresponding testsPass3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
MediumTestingError paths and edge cases testedPassContainment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
MediumTestingExisting tests still pass (no regressions)PassFull api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassappAutomateTmpDir() is descriptive; no dead code introduced
LowQualityComments explain why, not whatPassComments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
LowQualityNo unnecessary dependencies addedPassNo 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.

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

// producing a cwd-relative glob. BrowserStack-mode only — self-hosted scoping
// stays on PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = (process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/tmp').replace(/[/\\]+$/, '');

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Silent fallback hides host misconfiguration (+ no whitespace trim)

A set-but-invalid value — non-absolute, trimming to empty, or carrying incidental whitespace from shell injection (whitespace fails isAbsolute too) — is silently discarded; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

Suggestion: two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected, e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>.

Reviewer: stack:code-reviewer

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Tmp root still read independently at three sites per request

scopeRoot here, the glob root, and the manual walker each call appAutomateTmpDir() separately. Divergence between reads fails closed (404, never an escape), so this is coherence-only — carried over from the prior round.

Suggestion: in the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS), which also unifies the two branches' slash normalization. Fine as a fast-follow.

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
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Override coverage is Android-glob-only end-to-end

The iOS override glob shares the helper (low risk, and helper semantics are now pinned platform-independently by the direct assertions below) but is never exercised under a custom root.

Suggestion: optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Reviewer: stack:code-reviewer

@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) route through a new appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) and falls back to the legacy /tmp convention when unset or non-absolute; self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; grep-verified no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this head
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Prior-round concerns: rollout-skew (legacy fallback) and env shape validation — resolved with test pins; trailing-slash trim — resolved via direct helper assertions; separate env reads and iOS override coverage — carried forward as Low (Findings 2–3).

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace from shell injection — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening in the helper: add .trim() before the separator trim, and emit a debug log when a set value is rejected (e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>).

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is still read from the env independently at three sites per request (scopeRoot here; glob root and manual walker in maestro-screenshot-file.js). Divergence between reads fails closed (404, never an escape), so this is coherence-only — but it is the same structural concern raised last round.

  • Suggestion: In the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS) — this also unifies the two branches' slash normalization. Fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (risk is low, and helper semantics are now pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational (not counted against the verdict): on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp (drive-relative there) — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer; both prior blocking concerns resolved with test pins, containment security preserved under relocated roots; remaining findings are Low polish (a two-line helper hardening is recommended pre-merge, the rest can follow up).

…_TMP_DIR
BrowserStack hosts can relocate the App Automate session directories away
from /tmp/{sessionId}, which broke the Maestro screenshot relay's hardcoded
/tmp globs and scope roots. Route every BrowserStack-mode path (glob
patterns, manual-walker fallback, containment scope roots) through a new
appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR — injected
by the host with the relocated root, so no infra path is baked into the
public CLI — and falls back to the legacy /tmp convention when the var is
unset or not absolute, keeping hosts with older injection working during
rollout. Self-hosted mode (PERCY_MAESTRO_SCREENSHOT_DIR) is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10
prklm10force-pushed the fix/maestro-app-automate-tmp-dir branch from d07e89a to fb9afa0CompareJuly 27, 2026 12:40
@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) with a legacy /tmp fallback when unset or non-absolute; self-hosted mode is unchanged.

Note: Head fb9afa0 is a history-only squash — its tree and PR diff are byte-identical to the previously reviewed head d07e89a (verified via git diff --quiet and a byte-compare of the gh pr diff snapshots). The stack:code-reviewer findings below are from that review of this exact diff. The three inline comments from review #4786463906 anchor to the same unchanged lines and remain valid, so they are not re-posted.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this exact tree
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, single squashed commit, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is read from the env independently at three sites per request (scopeRoot, glob root, manual walker). Divergence fails closed (404, never an escape) — coherence-only.

  • Suggestion: Derive the glob from the already-passed scopeRoot in the BS branch of locateScreenshot; fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (low risk, semantics pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational: on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer on this exact diff; remaining findings are Low polish already tracked as inline comments on the PR.

@prklm10
prklm10 merged commit 512e238 into masterJul 27, 2026
46 of 47 checks passed
@prklm10
prklm10 deleted the fix/maestro-app-automate-tmp-dir branch July 27, 2026 12:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@prklm10@ninadbstack
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
prklm10 merged 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Jul 27, 2026
Merged

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

Conversation

@prklm10

@prklm10prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories away from /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 — injected by the host with the relocated root — and falls back to the legacy /tmp convention when unset, so no internal infra path is hardcoded in the CLI and hosts with older injection keep working during rollout. Trailing separators are trimmed, backslashes normalized for the glob, and non-absolute values fall back to /tmp.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing against the default root.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, non-absolute fallback to /tmp, direct trim/fallback assertions on the exported helper, and filePath containment re-scoping to the overridden root.
  • Full packages/core/test/api.test.js run locally: all maestro specs pass; the single unrelated failure (when the server is disabled…, ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue.

🤖 Generated with Claude Code

@prklm10prklm10 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 ownerJuly 27, 2026 05:06

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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 the relocated App Automate session root: 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 the relocated root); self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassOnly a path constant; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
HighSecurityNo IDOR — resource ownership validatedPassCross-session containment re-verified under the overridden root by a new test
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
HighCorrectnessError handling is explicit, no swallowed exceptionsPassMissing screenshots still surface as actionable 404s with the searched pattern
HighCorrectnessNo race conditions or concurrency issuesPassscopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
MediumTestingNew code has corresponding testsPass3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
MediumTestingError paths and edge cases testedPassContainment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
MediumTestingExisting tests still pass (no regressions)PassFull api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassappAutomateTmpDir() is descriptive; no dead code introduced
LowQualityComments explain why, not whatPassComments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
LowQualityNo unnecessary dependencies addedPassNo 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.

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

// producing a cwd-relative glob. BrowserStack-mode only — self-hosted scoping
// stays on PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = (process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/tmp').replace(/[/\\]+$/, '');

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Silent fallback hides host misconfiguration (+ no whitespace trim)

A set-but-invalid value — non-absolute, trimming to empty, or carrying incidental whitespace from shell injection (whitespace fails isAbsolute too) — is silently discarded; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

Suggestion: two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected, e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>.

Reviewer: stack:code-reviewer

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Tmp root still read independently at three sites per request

scopeRoot here, the glob root, and the manual walker each call appAutomateTmpDir() separately. Divergence between reads fails closed (404, never an escape), so this is coherence-only — carried over from the prior round.

Suggestion: in the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS), which also unifies the two branches' slash normalization. Fine as a fast-follow.

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
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Override coverage is Android-glob-only end-to-end

The iOS override glob shares the helper (low risk, and helper semantics are now pinned platform-independently by the direct assertions below) but is never exercised under a custom root.

Suggestion: optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Reviewer: stack:code-reviewer

@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) route through a new appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) and falls back to the legacy /tmp convention when unset or non-absolute; self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; grep-verified no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this head
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Prior-round concerns: rollout-skew (legacy fallback) and env shape validation — resolved with test pins; trailing-slash trim — resolved via direct helper assertions; separate env reads and iOS override coverage — carried forward as Low (Findings 2–3).

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace from shell injection — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening in the helper: add .trim() before the separator trim, and emit a debug log when a set value is rejected (e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>).

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is still read from the env independently at three sites per request (scopeRoot here; glob root and manual walker in maestro-screenshot-file.js). Divergence between reads fails closed (404, never an escape), so this is coherence-only — but it is the same structural concern raised last round.

  • Suggestion: In the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS) — this also unifies the two branches' slash normalization. Fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (risk is low, and helper semantics are now pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational (not counted against the verdict): on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp (drive-relative there) — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer; both prior blocking concerns resolved with test pins, containment security preserved under relocated roots; remaining findings are Low polish (a two-line helper hardening is recommended pre-merge, the rest can follow up).

…_TMP_DIR
BrowserStack hosts can relocate the App Automate session directories away
from /tmp/{sessionId}, which broke the Maestro screenshot relay's hardcoded
/tmp globs and scope roots. Route every BrowserStack-mode path (glob
patterns, manual-walker fallback, containment scope roots) through a new
appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR — injected
by the host with the relocated root, so no infra path is baked into the
public CLI — and falls back to the legacy /tmp convention when the var is
unset or not absolute, keeping hosts with older injection working during
rollout. Self-hosted mode (PERCY_MAESTRO_SCREENSHOT_DIR) is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10
prklm10force-pushed the fix/maestro-app-automate-tmp-dir branch from d07e89a to fb9afa0CompareJuly 27, 2026 12:40
@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) with a legacy /tmp fallback when unset or non-absolute; self-hosted mode is unchanged.

Note: Head fb9afa0 is a history-only squash — its tree and PR diff are byte-identical to the previously reviewed head d07e89a (verified via git diff --quiet and a byte-compare of the gh pr diff snapshots). The stack:code-reviewer findings below are from that review of this exact diff. The three inline comments from review #4786463906 anchor to the same unchanged lines and remain valid, so they are not re-posted.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this exact tree
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, single squashed commit, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is read from the env independently at three sites per request (scopeRoot, glob root, manual walker). Divergence fails closed (404, never an escape) — coherence-only.

  • Suggestion: Derive the glob from the already-passed scopeRoot in the BS branch of locateScreenshot; fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (low risk, semantics pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational: on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer on this exact diff; remaining findings are Low polish already tracked as inline comments on the PR.

@prklm10
prklm10 merged commit 512e238 into masterJul 27, 2026
46 of 47 checks passed
@prklm10
prklm10 deleted the fix/maestro-app-automate-tmp-dir branch July 27, 2026 12:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@prklm10@ninadbstack
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
prklm10 merged 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Jul 27, 2026
Merged

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

Conversation

@prklm10

@prklm10prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories away from /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 — injected by the host with the relocated root — and falls back to the legacy /tmp convention when unset, so no internal infra path is hardcoded in the CLI and hosts with older injection keep working during rollout. Trailing separators are trimmed, backslashes normalized for the glob, and non-absolute values fall back to /tmp.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing against the default root.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, non-absolute fallback to /tmp, direct trim/fallback assertions on the exported helper, and filePath containment re-scoping to the overridden root.
  • Full packages/core/test/api.test.js run locally: all maestro specs pass; the single unrelated failure (when the server is disabled…, ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue.

🤖 Generated with Claude Code

@prklm10prklm10 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 ownerJuly 27, 2026 05:06

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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 the relocated App Automate session root: 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 the relocated root); self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassOnly a path constant; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
HighSecurityNo IDOR — resource ownership validatedPassCross-session containment re-verified under the overridden root by a new test
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
HighCorrectnessError handling is explicit, no swallowed exceptionsPassMissing screenshots still surface as actionable 404s with the searched pattern
HighCorrectnessNo race conditions or concurrency issuesPassscopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
MediumTestingNew code has corresponding testsPass3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
MediumTestingError paths and edge cases testedPassContainment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
MediumTestingExisting tests still pass (no regressions)PassFull api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassappAutomateTmpDir() is descriptive; no dead code introduced
LowQualityComments explain why, not whatPassComments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
LowQualityNo unnecessary dependencies addedPassNo 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.

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

// producing a cwd-relative glob. BrowserStack-mode only — self-hosted scoping
// stays on PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = (process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/tmp').replace(/[/\\]+$/, '');

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Silent fallback hides host misconfiguration (+ no whitespace trim)

A set-but-invalid value — non-absolute, trimming to empty, or carrying incidental whitespace from shell injection (whitespace fails isAbsolute too) — is silently discarded; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

Suggestion: two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected, e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>.

Reviewer: stack:code-reviewer

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Tmp root still read independently at three sites per request

scopeRoot here, the glob root, and the manual walker each call appAutomateTmpDir() separately. Divergence between reads fails closed (404, never an escape), so this is coherence-only — carried over from the prior round.

Suggestion: in the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS), which also unifies the two branches' slash normalization. Fine as a fast-follow.

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
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Override coverage is Android-glob-only end-to-end

The iOS override glob shares the helper (low risk, and helper semantics are now pinned platform-independently by the direct assertions below) but is never exercised under a custom root.

Suggestion: optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Reviewer: stack:code-reviewer

@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) route through a new appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) and falls back to the legacy /tmp convention when unset or non-absolute; self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; grep-verified no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this head
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Prior-round concerns: rollout-skew (legacy fallback) and env shape validation — resolved with test pins; trailing-slash trim — resolved via direct helper assertions; separate env reads and iOS override coverage — carried forward as Low (Findings 2–3).

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace from shell injection — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening in the helper: add .trim() before the separator trim, and emit a debug log when a set value is rejected (e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>).

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is still read from the env independently at three sites per request (scopeRoot here; glob root and manual walker in maestro-screenshot-file.js). Divergence between reads fails closed (404, never an escape), so this is coherence-only — but it is the same structural concern raised last round.

  • Suggestion: In the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS) — this also unifies the two branches' slash normalization. Fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (risk is low, and helper semantics are now pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational (not counted against the verdict): on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp (drive-relative there) — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer; both prior blocking concerns resolved with test pins, containment security preserved under relocated roots; remaining findings are Low polish (a two-line helper hardening is recommended pre-merge, the rest can follow up).

…_TMP_DIR
BrowserStack hosts can relocate the App Automate session directories away
from /tmp/{sessionId}, which broke the Maestro screenshot relay's hardcoded
/tmp globs and scope roots. Route every BrowserStack-mode path (glob
patterns, manual-walker fallback, containment scope roots) through a new
appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR — injected
by the host with the relocated root, so no infra path is baked into the
public CLI — and falls back to the legacy /tmp convention when the var is
unset or not absolute, keeping hosts with older injection working during
rollout. Self-hosted mode (PERCY_MAESTRO_SCREENSHOT_DIR) is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10
prklm10force-pushed the fix/maestro-app-automate-tmp-dir branch from d07e89a to fb9afa0CompareJuly 27, 2026 12:40
@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) with a legacy /tmp fallback when unset or non-absolute; self-hosted mode is unchanged.

Note: Head fb9afa0 is a history-only squash — its tree and PR diff are byte-identical to the previously reviewed head d07e89a (verified via git diff --quiet and a byte-compare of the gh pr diff snapshots). The stack:code-reviewer findings below are from that review of this exact diff. The three inline comments from review #4786463906 anchor to the same unchanged lines and remain valid, so they are not re-posted.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this exact tree
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, single squashed commit, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is read from the env independently at three sites per request (scopeRoot, glob root, manual walker). Divergence fails closed (404, never an escape) — coherence-only.

  • Suggestion: Derive the glob from the already-passed scopeRoot in the BS branch of locateScreenshot; fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (low risk, semantics pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational: on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer on this exact diff; remaining findings are Low polish already tracked as inline comments on the PR.

@prklm10
prklm10 merged commit 512e238 into masterJul 27, 2026
46 of 47 checks passed
@prklm10
prklm10 deleted the fix/maestro-app-automate-tmp-dir branch July 27, 2026 12:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@prklm10@ninadbstack
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
prklm10 merged 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Jul 27, 2026
Merged

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

Conversation

@prklm10

@prklm10prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories away from /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 — injected by the host with the relocated root — and falls back to the legacy /tmp convention when unset, so no internal infra path is hardcoded in the CLI and hosts with older injection keep working during rollout. Trailing separators are trimmed, backslashes normalized for the glob, and non-absolute values fall back to /tmp.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing against the default root.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, non-absolute fallback to /tmp, direct trim/fallback assertions on the exported helper, and filePath containment re-scoping to the overridden root.
  • Full packages/core/test/api.test.js run locally: all maestro specs pass; the single unrelated failure (when the server is disabled…, ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue.

🤖 Generated with Claude Code

@prklm10prklm10 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 ownerJuly 27, 2026 05:06

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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 the relocated App Automate session root: 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 the relocated root); self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassOnly a path constant; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
HighSecurityNo IDOR — resource ownership validatedPassCross-session containment re-verified under the overridden root by a new test
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
HighCorrectnessError handling is explicit, no swallowed exceptionsPassMissing screenshots still surface as actionable 404s with the searched pattern
HighCorrectnessNo race conditions or concurrency issuesPassscopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
MediumTestingNew code has corresponding testsPass3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
MediumTestingError paths and edge cases testedPassContainment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
MediumTestingExisting tests still pass (no regressions)PassFull api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassappAutomateTmpDir() is descriptive; no dead code introduced
LowQualityComments explain why, not whatPassComments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
LowQualityNo unnecessary dependencies addedPassNo 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.

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

// producing a cwd-relative glob. BrowserStack-mode only — self-hosted scoping
// stays on PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = (process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/tmp').replace(/[/\\]+$/, '');

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Silent fallback hides host misconfiguration (+ no whitespace trim)

A set-but-invalid value — non-absolute, trimming to empty, or carrying incidental whitespace from shell injection (whitespace fails isAbsolute too) — is silently discarded; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

Suggestion: two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected, e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>.

Reviewer: stack:code-reviewer

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Tmp root still read independently at three sites per request

scopeRoot here, the glob root, and the manual walker each call appAutomateTmpDir() separately. Divergence between reads fails closed (404, never an escape), so this is coherence-only — carried over from the prior round.

Suggestion: in the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS), which also unifies the two branches' slash normalization. Fine as a fast-follow.

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
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Override coverage is Android-glob-only end-to-end

The iOS override glob shares the helper (low risk, and helper semantics are now pinned platform-independently by the direct assertions below) but is never exercised under a custom root.

Suggestion: optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Reviewer: stack:code-reviewer

@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) route through a new appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) and falls back to the legacy /tmp convention when unset or non-absolute; self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; grep-verified no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this head
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Prior-round concerns: rollout-skew (legacy fallback) and env shape validation — resolved with test pins; trailing-slash trim — resolved via direct helper assertions; separate env reads and iOS override coverage — carried forward as Low (Findings 2–3).

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace from shell injection — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening in the helper: add .trim() before the separator trim, and emit a debug log when a set value is rejected (e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>).

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is still read from the env independently at three sites per request (scopeRoot here; glob root and manual walker in maestro-screenshot-file.js). Divergence between reads fails closed (404, never an escape), so this is coherence-only — but it is the same structural concern raised last round.

  • Suggestion: In the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS) — this also unifies the two branches' slash normalization. Fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (risk is low, and helper semantics are now pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational (not counted against the verdict): on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp (drive-relative there) — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer; both prior blocking concerns resolved with test pins, containment security preserved under relocated roots; remaining findings are Low polish (a two-line helper hardening is recommended pre-merge, the rest can follow up).

…_TMP_DIR
BrowserStack hosts can relocate the App Automate session directories away
from /tmp/{sessionId}, which broke the Maestro screenshot relay's hardcoded
/tmp globs and scope roots. Route every BrowserStack-mode path (glob
patterns, manual-walker fallback, containment scope roots) through a new
appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR — injected
by the host with the relocated root, so no infra path is baked into the
public CLI — and falls back to the legacy /tmp convention when the var is
unset or not absolute, keeping hosts with older injection working during
rollout. Self-hosted mode (PERCY_MAESTRO_SCREENSHOT_DIR) is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10
prklm10force-pushed the fix/maestro-app-automate-tmp-dir branch from d07e89a to fb9afa0CompareJuly 27, 2026 12:40
@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) with a legacy /tmp fallback when unset or non-absolute; self-hosted mode is unchanged.

Note: Head fb9afa0 is a history-only squash — its tree and PR diff are byte-identical to the previously reviewed head d07e89a (verified via git diff --quiet and a byte-compare of the gh pr diff snapshots). The stack:code-reviewer findings below are from that review of this exact diff. The three inline comments from review #4786463906 anchor to the same unchanged lines and remain valid, so they are not re-posted.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this exact tree
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, single squashed commit, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is read from the env independently at three sites per request (scopeRoot, glob root, manual walker). Divergence fails closed (404, never an escape) — coherence-only.

  • Suggestion: Derive the glob from the already-passed scopeRoot in the BS branch of locateScreenshot; fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (low risk, semantics pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational: on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer on this exact diff; remaining findings are Low polish already tracked as inline comments on the PR.

@prklm10
prklm10 merged commit 512e238 into masterJul 27, 2026
46 of 47 checks passed
@prklm10
prklm10 deleted the fix/maestro-app-automate-tmp-dir branch July 27, 2026 12:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@prklm10@ninadbstack
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
prklm10 merged 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Jul 27, 2026
Merged

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

Conversation

@prklm10

@prklm10prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories away from /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 — injected by the host with the relocated root — and falls back to the legacy /tmp convention when unset, so no internal infra path is hardcoded in the CLI and hosts with older injection keep working during rollout. Trailing separators are trimmed, backslashes normalized for the glob, and non-absolute values fall back to /tmp.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing against the default root.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, non-absolute fallback to /tmp, direct trim/fallback assertions on the exported helper, and filePath containment re-scoping to the overridden root.
  • Full packages/core/test/api.test.js run locally: all maestro specs pass; the single unrelated failure (when the server is disabled…, ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue.

🤖 Generated with Claude Code

@prklm10prklm10 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 ownerJuly 27, 2026 05:06

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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 the relocated App Automate session root: 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 the relocated root); self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassOnly a path constant; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
HighSecurityNo IDOR — resource ownership validatedPassCross-session containment re-verified under the overridden root by a new test
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
HighCorrectnessError handling is explicit, no swallowed exceptionsPassMissing screenshots still surface as actionable 404s with the searched pattern
HighCorrectnessNo race conditions or concurrency issuesPassscopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
MediumTestingNew code has corresponding testsPass3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
MediumTestingError paths and edge cases testedPassContainment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
MediumTestingExisting tests still pass (no regressions)PassFull api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassappAutomateTmpDir() is descriptive; no dead code introduced
LowQualityComments explain why, not whatPassComments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
LowQualityNo unnecessary dependencies addedPassNo 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.

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

// producing a cwd-relative glob. BrowserStack-mode only — self-hosted scoping
// stays on PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = (process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/tmp').replace(/[/\\]+$/, '');

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Silent fallback hides host misconfiguration (+ no whitespace trim)

A set-but-invalid value — non-absolute, trimming to empty, or carrying incidental whitespace from shell injection (whitespace fails isAbsolute too) — is silently discarded; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

Suggestion: two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected, e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>.

Reviewer: stack:code-reviewer

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Tmp root still read independently at three sites per request

scopeRoot here, the glob root, and the manual walker each call appAutomateTmpDir() separately. Divergence between reads fails closed (404, never an escape), so this is coherence-only — carried over from the prior round.

Suggestion: in the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS), which also unifies the two branches' slash normalization. Fine as a fast-follow.

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
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Override coverage is Android-glob-only end-to-end

The iOS override glob shares the helper (low risk, and helper semantics are now pinned platform-independently by the direct assertions below) but is never exercised under a custom root.

Suggestion: optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Reviewer: stack:code-reviewer

@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) route through a new appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) and falls back to the legacy /tmp convention when unset or non-absolute; self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; grep-verified no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this head
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Prior-round concerns: rollout-skew (legacy fallback) and env shape validation — resolved with test pins; trailing-slash trim — resolved via direct helper assertions; separate env reads and iOS override coverage — carried forward as Low (Findings 2–3).

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace from shell injection — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening in the helper: add .trim() before the separator trim, and emit a debug log when a set value is rejected (e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>).

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is still read from the env independently at three sites per request (scopeRoot here; glob root and manual walker in maestro-screenshot-file.js). Divergence between reads fails closed (404, never an escape), so this is coherence-only — but it is the same structural concern raised last round.

  • Suggestion: In the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS) — this also unifies the two branches' slash normalization. Fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (risk is low, and helper semantics are now pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational (not counted against the verdict): on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp (drive-relative there) — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer; both prior blocking concerns resolved with test pins, containment security preserved under relocated roots; remaining findings are Low polish (a two-line helper hardening is recommended pre-merge, the rest can follow up).

…_TMP_DIR
BrowserStack hosts can relocate the App Automate session directories away
from /tmp/{sessionId}, which broke the Maestro screenshot relay's hardcoded
/tmp globs and scope roots. Route every BrowserStack-mode path (glob
patterns, manual-walker fallback, containment scope roots) through a new
appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR — injected
by the host with the relocated root, so no infra path is baked into the
public CLI — and falls back to the legacy /tmp convention when the var is
unset or not absolute, keeping hosts with older injection working during
rollout. Self-hosted mode (PERCY_MAESTRO_SCREENSHOT_DIR) is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10
prklm10force-pushed the fix/maestro-app-automate-tmp-dir branch from d07e89a to fb9afa0CompareJuly 27, 2026 12:40
@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) with a legacy /tmp fallback when unset or non-absolute; self-hosted mode is unchanged.

Note: Head fb9afa0 is a history-only squash — its tree and PR diff are byte-identical to the previously reviewed head d07e89a (verified via git diff --quiet and a byte-compare of the gh pr diff snapshots). The stack:code-reviewer findings below are from that review of this exact diff. The three inline comments from review #4786463906 anchor to the same unchanged lines and remain valid, so they are not re-posted.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this exact tree
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, single squashed commit, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is read from the env independently at three sites per request (scopeRoot, glob root, manual walker). Divergence fails closed (404, never an escape) — coherence-only.

  • Suggestion: Derive the glob from the already-passed scopeRoot in the BS branch of locateScreenshot; fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (low risk, semantics pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational: on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer on this exact diff; remaining findings are Low polish already tracked as inline comments on the PR.

@prklm10
prklm10 merged commit 512e238 into masterJul 27, 2026
46 of 47 checks passed
@prklm10
prklm10 deleted the fix/maestro-app-automate-tmp-dir branch July 27, 2026 12:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@prklm10@ninadbstack
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
prklm10 merged 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Jul 27, 2026
Merged

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

Conversation

@prklm10

@prklm10prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories away from /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 — injected by the host with the relocated root — and falls back to the legacy /tmp convention when unset, so no internal infra path is hardcoded in the CLI and hosts with older injection keep working during rollout. Trailing separators are trimmed, backslashes normalized for the glob, and non-absolute values fall back to /tmp.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing against the default root.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, non-absolute fallback to /tmp, direct trim/fallback assertions on the exported helper, and filePath containment re-scoping to the overridden root.
  • Full packages/core/test/api.test.js run locally: all maestro specs pass; the single unrelated failure (when the server is disabled…, ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue.

🤖 Generated with Claude Code

@prklm10prklm10 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 ownerJuly 27, 2026 05:06

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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 the relocated App Automate session root: 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 the relocated root); self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassOnly a path constant; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
HighSecurityNo IDOR — resource ownership validatedPassCross-session containment re-verified under the overridden root by a new test
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
HighCorrectnessError handling is explicit, no swallowed exceptionsPassMissing screenshots still surface as actionable 404s with the searched pattern
HighCorrectnessNo race conditions or concurrency issuesPassscopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
MediumTestingNew code has corresponding testsPass3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
MediumTestingError paths and edge cases testedPassContainment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
MediumTestingExisting tests still pass (no regressions)PassFull api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassappAutomateTmpDir() is descriptive; no dead code introduced
LowQualityComments explain why, not whatPassComments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
LowQualityNo unnecessary dependencies addedPassNo 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.

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

// producing a cwd-relative glob. BrowserStack-mode only — self-hosted scoping
// stays on PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = (process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/tmp').replace(/[/\\]+$/, '');

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Silent fallback hides host misconfiguration (+ no whitespace trim)

A set-but-invalid value — non-absolute, trimming to empty, or carrying incidental whitespace from shell injection (whitespace fails isAbsolute too) — is silently discarded; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

Suggestion: two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected, e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>.

Reviewer: stack:code-reviewer

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Tmp root still read independently at three sites per request

scopeRoot here, the glob root, and the manual walker each call appAutomateTmpDir() separately. Divergence between reads fails closed (404, never an escape), so this is coherence-only — carried over from the prior round.

Suggestion: in the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS), which also unifies the two branches' slash normalization. Fine as a fast-follow.

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
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Override coverage is Android-glob-only end-to-end

The iOS override glob shares the helper (low risk, and helper semantics are now pinned platform-independently by the direct assertions below) but is never exercised under a custom root.

Suggestion: optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Reviewer: stack:code-reviewer

@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) route through a new appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) and falls back to the legacy /tmp convention when unset or non-absolute; self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; grep-verified no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this head
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Prior-round concerns: rollout-skew (legacy fallback) and env shape validation — resolved with test pins; trailing-slash trim — resolved via direct helper assertions; separate env reads and iOS override coverage — carried forward as Low (Findings 2–3).

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace from shell injection — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening in the helper: add .trim() before the separator trim, and emit a debug log when a set value is rejected (e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>).

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is still read from the env independently at three sites per request (scopeRoot here; glob root and manual walker in maestro-screenshot-file.js). Divergence between reads fails closed (404, never an escape), so this is coherence-only — but it is the same structural concern raised last round.

  • Suggestion: In the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS) — this also unifies the two branches' slash normalization. Fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (risk is low, and helper semantics are now pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational (not counted against the verdict): on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp (drive-relative there) — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer; both prior blocking concerns resolved with test pins, containment security preserved under relocated roots; remaining findings are Low polish (a two-line helper hardening is recommended pre-merge, the rest can follow up).

…_TMP_DIR
BrowserStack hosts can relocate the App Automate session directories away
from /tmp/{sessionId}, which broke the Maestro screenshot relay's hardcoded
/tmp globs and scope roots. Route every BrowserStack-mode path (glob
patterns, manual-walker fallback, containment scope roots) through a new
appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR — injected
by the host with the relocated root, so no infra path is baked into the
public CLI — and falls back to the legacy /tmp convention when the var is
unset or not absolute, keeping hosts with older injection working during
rollout. Self-hosted mode (PERCY_MAESTRO_SCREENSHOT_DIR) is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10
prklm10force-pushed the fix/maestro-app-automate-tmp-dir branch from d07e89a to fb9afa0CompareJuly 27, 2026 12:40
@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) with a legacy /tmp fallback when unset or non-absolute; self-hosted mode is unchanged.

Note: Head fb9afa0 is a history-only squash — its tree and PR diff are byte-identical to the previously reviewed head d07e89a (verified via git diff --quiet and a byte-compare of the gh pr diff snapshots). The stack:code-reviewer findings below are from that review of this exact diff. The three inline comments from review #4786463906 anchor to the same unchanged lines and remain valid, so they are not re-posted.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this exact tree
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, single squashed commit, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is read from the env independently at three sites per request (scopeRoot, glob root, manual walker). Divergence fails closed (404, never an escape) — coherence-only.

  • Suggestion: Derive the glob from the already-passed scopeRoot in the BS branch of locateScreenshot; fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (low risk, semantics pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational: on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer on this exact diff; remaining findings are Low polish already tracked as inline comments on the PR.

@prklm10
prklm10 merged commit 512e238 into masterJul 27, 2026
46 of 47 checks passed
@prklm10
prklm10 deleted the fix/maestro-app-automate-tmp-dir branch July 27, 2026 12:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@prklm10@ninadbstack
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
prklm10 merged 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Jul 27, 2026
Merged

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

Conversation

@prklm10

@prklm10prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories away from /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 — injected by the host with the relocated root — and falls back to the legacy /tmp convention when unset, so no internal infra path is hardcoded in the CLI and hosts with older injection keep working during rollout. Trailing separators are trimmed, backslashes normalized for the glob, and non-absolute values fall back to /tmp.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing against the default root.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, non-absolute fallback to /tmp, direct trim/fallback assertions on the exported helper, and filePath containment re-scoping to the overridden root.
  • Full packages/core/test/api.test.js run locally: all maestro specs pass; the single unrelated failure (when the server is disabled…, ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue.

🤖 Generated with Claude Code

@prklm10prklm10 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 ownerJuly 27, 2026 05:06

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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 the relocated App Automate session root: 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 the relocated root); self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassOnly a path constant; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
HighSecurityNo IDOR — resource ownership validatedPassCross-session containment re-verified under the overridden root by a new test
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
HighCorrectnessError handling is explicit, no swallowed exceptionsPassMissing screenshots still surface as actionable 404s with the searched pattern
HighCorrectnessNo race conditions or concurrency issuesPassscopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
MediumTestingNew code has corresponding testsPass3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
MediumTestingError paths and edge cases testedPassContainment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
MediumTestingExisting tests still pass (no regressions)PassFull api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassappAutomateTmpDir() is descriptive; no dead code introduced
LowQualityComments explain why, not whatPassComments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
LowQualityNo unnecessary dependencies addedPassNo 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.

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

// producing a cwd-relative glob. BrowserStack-mode only — self-hosted scoping
// stays on PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = (process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/tmp').replace(/[/\\]+$/, '');

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Silent fallback hides host misconfiguration (+ no whitespace trim)

A set-but-invalid value — non-absolute, trimming to empty, or carrying incidental whitespace from shell injection (whitespace fails isAbsolute too) — is silently discarded; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

Suggestion: two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected, e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>.

Reviewer: stack:code-reviewer

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Tmp root still read independently at three sites per request

scopeRoot here, the glob root, and the manual walker each call appAutomateTmpDir() separately. Divergence between reads fails closed (404, never an escape), so this is coherence-only — carried over from the prior round.

Suggestion: in the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS), which also unifies the two branches' slash normalization. Fine as a fast-follow.

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
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Override coverage is Android-glob-only end-to-end

The iOS override glob shares the helper (low risk, and helper semantics are now pinned platform-independently by the direct assertions below) but is never exercised under a custom root.

Suggestion: optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Reviewer: stack:code-reviewer

@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) route through a new appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) and falls back to the legacy /tmp convention when unset or non-absolute; self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; grep-verified no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this head
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Prior-round concerns: rollout-skew (legacy fallback) and env shape validation — resolved with test pins; trailing-slash trim — resolved via direct helper assertions; separate env reads and iOS override coverage — carried forward as Low (Findings 2–3).

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace from shell injection — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening in the helper: add .trim() before the separator trim, and emit a debug log when a set value is rejected (e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>).

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is still read from the env independently at three sites per request (scopeRoot here; glob root and manual walker in maestro-screenshot-file.js). Divergence between reads fails closed (404, never an escape), so this is coherence-only — but it is the same structural concern raised last round.

  • Suggestion: In the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS) — this also unifies the two branches' slash normalization. Fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (risk is low, and helper semantics are now pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational (not counted against the verdict): on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp (drive-relative there) — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer; both prior blocking concerns resolved with test pins, containment security preserved under relocated roots; remaining findings are Low polish (a two-line helper hardening is recommended pre-merge, the rest can follow up).

…_TMP_DIR
BrowserStack hosts can relocate the App Automate session directories away
from /tmp/{sessionId}, which broke the Maestro screenshot relay's hardcoded
/tmp globs and scope roots. Route every BrowserStack-mode path (glob
patterns, manual-walker fallback, containment scope roots) through a new
appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR — injected
by the host with the relocated root, so no infra path is baked into the
public CLI — and falls back to the legacy /tmp convention when the var is
unset or not absolute, keeping hosts with older injection working during
rollout. Self-hosted mode (PERCY_MAESTRO_SCREENSHOT_DIR) is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10
prklm10force-pushed the fix/maestro-app-automate-tmp-dir branch from d07e89a to fb9afa0CompareJuly 27, 2026 12:40
@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) with a legacy /tmp fallback when unset or non-absolute; self-hosted mode is unchanged.

Note: Head fb9afa0 is a history-only squash — its tree and PR diff are byte-identical to the previously reviewed head d07e89a (verified via git diff --quiet and a byte-compare of the gh pr diff snapshots). The stack:code-reviewer findings below are from that review of this exact diff. The three inline comments from review #4786463906 anchor to the same unchanged lines and remain valid, so they are not re-posted.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this exact tree
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, single squashed commit, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is read from the env independently at three sites per request (scopeRoot, glob root, manual walker). Divergence fails closed (404, never an escape) — coherence-only.

  • Suggestion: Derive the glob from the already-passed scopeRoot in the BS branch of locateScreenshot; fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (low risk, semantics pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational: on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer on this exact diff; remaining findings are Low polish already tracked as inline comments on the PR.

@prklm10
prklm10 merged commit 512e238 into masterJul 27, 2026
46 of 47 checks passed
@prklm10
prklm10 deleted the fix/maestro-app-automate-tmp-dir branch July 27, 2026 12:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@prklm10@ninadbstack
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
prklm10 merged 1 commit into
masterfrom
fix/maestro-app-automate-tmp-dir
Jul 27, 2026
Merged

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

Conversation

@prklm10

@prklm10prklm10 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BrowserStack hosts relocated the App Automate session directories away from /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 — injected by the host with the relocated root — and falls back to the legacy /tmp convention when unset, so no internal infra path is hardcoded in the CLI and hosts with older injection keep working during rollout. Trailing separators are trimmed, backslashes normalized for the glob, and non-absolute values fall back to /tmp.
  • Self-hosted mode is untouched — it continues to scope via PERCY_MAESTRO_SCREENSHOT_DIR.

Test plan

  • Existing /percy/maestro-screenshot specs (Android + iOS globs, filePath acceptance/containment, PNG-fill, regions) — all passing against the default root.
  • New PERCY_APP_AUTOMATE_TMP_DIR override specs: globbing under an overridden root (real-fs fixture), trailing-slash tolerance, non-absolute fallback to /tmp, direct trim/fallback assertions on the exported helper, and filePath containment re-scoping to the overridden root.
  • Full packages/core/test/api.test.js run locally: all maestro specs pass; the single unrelated failure (when the server is disabled…, ECONNREFUSED vs AggregateError) also fails on a clean master checkout — pre-existing local Node environment issue.

🤖 Generated with Claude Code

@prklm10prklm10 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 ownerJuly 27, 2026 05:06

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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 the relocated App Automate session root: 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 the relocated root); self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassOnly a path constant; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation on name/sessionId untouched; realpath + scope-root containment invariant preserved and fail-closed
HighSecurityNo IDOR — resource ownership validatedPassCross-session containment re-verified under the overridden root by a new test
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, backslash normalization for glob; grep-verified no BS-mode /tmp path survives in source
HighCorrectnessError handling is explicit, no swallowed exceptionsPassMissing screenshots still surface as actionable 404s with the searched pattern
HighCorrectnessNo race conditions or concurrency issuesPassscopeRoot and glob root come from separate env reads (Finding 3), but divergence fails closed (404)
MediumTestingNew code has corresponding testsPass3 new override specs (custom root, trailing slash, containment re-scoping); existing specs migrated to the new default root
MediumTestingError paths and edge cases testedPassContainment/404 paths covered; iOS override branch untested (Finding 4, low risk — same expression as Android)
MediumTestingExisting tests still pass (no regressions)PassFull api.test.js run: 126/127; the 1 failure is a pre-existing environment flake reproduced on clean master
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env-var pattern and the existing real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassappAutomateTmpDir() is descriptive; no dead code introduced
LowQualityComments explain why, not whatPassComments updated everywhere paths changed, incl. the macOS-symlink rationale generalized rather than deleted
LowQualityNo unnecessary dependencies addedPassNo 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.

@prklm10prklm10 left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

// producing a cwd-relative glob. BrowserStack-mode only — self-hosted scoping
// stays on PERCY_MAESTRO_SCREENSHOT_DIR.
export function appAutomateTmpDir() {
let dir = (process.env.PERCY_APP_AUTOMATE_TMP_DIR || '/tmp').replace(/[/\\]+$/, '');

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Silent fallback hides host misconfiguration (+ no whitespace trim)

A set-but-invalid value — non-absolute, trimming to empty, or carrying incidental whitespace from shell injection (whitespace fails isAbsolute too) — is silently discarded; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

Suggestion: two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected, e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>.

Reviewer: stack:code-reviewer

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Tmp root still read independently at three sites per request

scopeRoot here, the glob root, and the manual walker each call appAutomateTmpDir() separately. Divergence between reads fails closed (404, never an escape), so this is coherence-only — carried over from the prior round.

Suggestion: in the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS), which also unifies the two branches' slash normalization. Fine as a fast-follow.

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
ContributorAuthor

Choose a reason for hiding this comment

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

[Low] Override coverage is Android-glob-only end-to-end

The iOS override glob shares the helper (low risk, and helper semantics are now pinned platform-independently by the direct assertions below) but is never exercised under a custom root.

Suggestion: optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Reviewer: stack:code-reviewer

@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths (glob patterns, manual-walker fallback, containment scope roots) route through a new appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) and falls back to the legacy /tmp convention when unset or non-absolute; self-hosted mode is unchanged.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; grep-verified no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this head
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Prior-round concerns: rollout-skew (legacy fallback) and env shape validation — resolved with test pins; trailing-slash trim — resolved via direct helper assertions; separate env reads and iOS override coverage — carried forward as Low (Findings 2–3).

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace from shell injection — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees Screenshot not found 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening in the helper: add .trim() before the separator trim, and emit a debug log when a set value is rejected (e.g. Ignoring non-absolute PERCY_APP_AUTOMATE_TMP_DIR: <raw>).

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is still read from the env independently at three sites per request (scopeRoot here; glob root and manual walker in maestro-screenshot-file.js). Divergence between reads fails closed (404, never an escape), so this is coherence-only — but it is the same structural concern raised last round.

  • Suggestion: In the BS branch of locateScreenshot, derive the glob from the already-passed scopeRoot ({scopeRoot}/logs/*/screenshots/… Android, {scopeRoot}/*_maestro_debug_*/** iOS) — this also unifies the two branches' slash normalization. Fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (risk is low, and helper semantics are now pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational (not counted against the verdict): on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp (drive-relative there) — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer; both prior blocking concerns resolved with test pins, containment security preserved under relocated roots; remaining findings are Low polish (a two-line helper hardening is recommended pre-merge, the rest can follow up).

…_TMP_DIR
BrowserStack hosts can relocate the App Automate session directories away
from /tmp/{sessionId}, which broke the Maestro screenshot relay's hardcoded
/tmp globs and scope roots. Route every BrowserStack-mode path (glob
patterns, manual-walker fallback, containment scope roots) through a new
appAutomateTmpDir() helper that reads PERCY_APP_AUTOMATE_TMP_DIR — injected
by the host with the relocated root, so no infra path is baked into the
public CLI — and falls back to the legacy /tmp convention when the var is
unset or not absolute, keeping hosts with older injection working during
rollout. Self-hosted mode (PERCY_MAESTRO_SCREENSHOT_DIR) is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prklm10
prklm10force-pushed the fix/maestro-app-automate-tmp-dir branch from d07e89a to fb9afa0CompareJuly 27, 2026 12:40
@prklm10

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

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

Summary

Fixes the Maestro screenshot relay after BrowserStack App Automate hosts relocated session artifact directories away from /tmp/{sessionId}: all BS-mode paths route through a new appAutomateTmpDir() helper reading PERCY_APP_AUTOMATE_TMP_DIR (host-injected relocated root — no infra path hardcoded in the public CLI) with a legacy /tmp fallback when unset or non-absolute; self-hosted mode is unchanged.

Note: Head fb9afa0 is a history-only squash — its tree and PR diff are byte-identical to the previously reviewed head d07e89a (verified via git diff --quiet and a byte-compare of the gh pr diff snapshots). The stack:code-reviewer findings below are from that review of this exact diff. The three inline comments from review #4786463906 anchor to the same unchanged lines and remain valid, so they are not re-posted.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo infra path or secret in source; env var is host-injected, never request-controlled
HighSecurityAuthentication/authorization checks presentN/ALocal relay endpoint; no auth surface changed
HighSecurityInput validation and sanitizationPassSAFE_ID validation untouched; env value gets absolute-path check with safe /tmp fallback
HighSecurityNo IDOR — resource ownership validatedPassrealpath + prefix containment preserved; spec pins that containment scopes to whichever root is active
HighSecurityNo SQL injection (parameterized queries)N/ANo database code
HighCorrectnessLogic is correct, handles edge casesPassTrailing-separator trim, non-absolute → /tmp fallback, backslash normalization; no BS-mode /tmp literal survives outside comments
HighCorrectnessError handling is explicit, no swallowed exceptionsPass404s echo the searched pattern; silent-fallback observability noted as Low (Finding 1)
HighCorrectnessNo race conditions or concurrency issuesPassRoot read at multiple sites per request (Finding 2) but divergence fails closed (404), never an escape
MediumTestingNew code has corresponding testsPass5 override specs incl. direct trim/fallback helper assertions and non-absolute fallback behavior
MediumTestingError paths and edge cases testedPassFallback + containment paths pinned; iOS override glob still not exercised end-to-end (Finding 3, low risk)
MediumTestingExisting tests still pass (no regressions)PassFull maestro-screenshot suite (66 specs) verified passing on this exact tree
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo data-fetching changes
MediumPerformanceLong-running tasks use background jobsN/ANot applicable
MediumQualityFollows existing codebase patternsPassMirrors PERCY_MAESTRO_SCREENSHOT_DIR env pattern and the real-fs $bypass test convention
MediumQualityChanges are focused (single concern)Pass3 files, single squashed commit, all scoped to the tmp-root relocation
LowQualityMeaningful names, no dead codePassSingle exported helper; no dead code
LowQualityComments explain why, not whatPassComments explain injection model, trim rationale, and fallback semantics without exposing infra paths
LowQualityNo unnecessary dependencies addedPassNo dependency changes

Findings

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Silent fallback hides host misconfiguration: a set-but-invalid value (non-absolute, trims to empty, or carrying incidental whitespace — whitespace fails isAbsolute too) is discarded without any signal; the operator only sees 404s against /tmp. Partially mitigated because the 404 message echoes the searched pattern.

  • Suggestion: Two-line hardening: add .trim() before the separator trim, and emit a debug log when a set value is rejected.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The tmp root is read from the env independently at three sites per request (scopeRoot, glob root, manual walker). Divergence fails closed (404, never an escape) — coherence-only.

  • Suggestion: Derive the glob from the already-passed scopeRoot in the BS branch of locateScreenshot; fine as a fast-follow.

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

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Override coverage is Android-glob-only end-to-end; the iOS override glob shares the helper (low risk, semantics pinned platform-independently) but is never exercised under a custom root.

  • Suggestion: Optionally add one iOS spec under CUSTOM_ROOT reusing the existing makePngHeader fixture helper.

Informational: on win32 a degenerate root like C:\ trims to a non-absolute value and falls back to /tmp — irrelevant in production since BS hosts are POSIX and CI specs use os.tmpdir() paths that survive the trim.


Verdict: PASS — approved by stack:code-reviewer on this exact diff; remaining findings are Low polish already tracked as inline comments on the PR.

@prklm10
prklm10 merged commit 512e238 into masterJul 27, 2026
46 of 47 checks passed
@prklm10
prklm10 deleted the fix/maestro-app-automate-tmp-dir branch July 27, 2026 12:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@prklm10@ninadbstack