refactor(core,cli-app): decompose merged self-hosted Maestro relay + app:exec injection#2315
refactor(core,cli-app): decompose merged self-hosted Maestro relay + app:exec injection#2315Sriram567 wants to merge 6 commits into
Conversation
…son-upload.js Move handleComparisonUpload out of api.js into its own module (handleSyncJob extraction pattern); promote the shared encodeURLSearchParams helper to utils.js so both api.js and the new module use it without a circular import. No behavior change.
…-hosted) into modules Extract the ~517-line maestro-screenshot handler out of api.js into cohesive modules, folding in #2261's self-hosted branches: - maestro-screenshot.js — handleMaestroScreenshot orchestrator + parsePngDimensions; runtime detection (selfHosted = !sessionId), conditional sessionId validation, PERCY_MAESTRO_SCREENSHOT_DIR scope-root resolution + filePath rejection. - maestro-screenshot-file.js — locateScreenshot({...,scopeRoot,selfHosted}): BS session glob + self-hosted recursive dot:true glob (Windows-normalized) + manualScreenshotWalk (BS only) + realpath/scopeRoot prefix check. - maestro-regions.js — validateRegionInputs + resolveRegions (explicit-key merge). api.js is now a route table (1072 → 382 lines) delegating via named imports; dead ServerError/maestro-hierarchy imports dropped. semgrep path-traversal suppression retargeted to maestro-screenshot-file.js (+ api.js createStaticServer). Behavior-preserving: api.test.js (BS + self-hosted) passes unchanged.
…ject.js Move maybeInjectMaestroServer + maybeInjectScreenshotDir (and the private argv helpers findTestSubcommandIdx / hasExistingPercyServerFlag / findTestOutputDirValue + constants) out of exec.js into a dedicated maestro-inject.js. exec.js returns to a thin command (211 → 47 lines) whose app:exec callback calls the injectors; index.js re-exports them from the new module. No behavior change — exec.test.js 43/43 unchanged (imports via the @percy/cli-app barrel).
Port deriveDeviceInsets (+ findStatusBarFrameHeight, deriveIosInsets, parseStableInsets, deriveAndroidInsets) from PR #2286 onto the decomposed modules: iOS status bar from /viewHierarchy statusBars frame × empirical PNG scale; Android status+nav via adb dumpsys mStableInsets; iOS navBar always 0; null on any failure. Additive; dump()/resolver cascade untouched. ~25 specs.
Add this.maestroInsetCache (Map keyed by sessionId), constructed alongside grpcClientCache and cleared in stop() — insets are device-constant within a session, so the relay derives once and reuses (incl. a null outcome). Ported from PR #2286.
handleMaestroScreenshot derives insets once per session (cached) and uses them for the tile's statusBarHeight/navBarHeight, authoritative over the SDK static defaults; iOS navBar=0; any failure falls back to the SDK value. Relay override/fallback/iOS-0/derive-and-cache specs + the beforeEach cache-seed seam, ported from PR #2286 onto the self-hosted-inclusive orchestrator.
Sriram567
left a comment
There was a problem hiding this comment.
Claude Code Review (automated) — 2 inline finding(s). Full report in the PR comment below. Verdict: Passed.
| // caller's single `== null` check covers every malformed-frame case. | ||
| /* istanbul ignore next — frame optional-chain guards a malformed status-bar | ||
| frame; real /viewHierarchy responses always carry a positive frame.Height. */ | ||
| return node.frame?.Height || null; |
There was a problem hiding this comment.
[Medium] Zero-height status bar collapses to null
node.frame?.Height || null treats a legitimate numeric 0 as absent, falling back to SDK defaults. Safe on today's devices (no Apple device reports a 0pt status bar), but the intent is implicit and a future immersive-mode layout reporting 0 would silently fall through.
Suggestion: make the guard explicit:
| return node.frame?.Height || null; | |
| return (typeof node.frame?.Height === 'number' && node.frame.Height > 0) ? node.frame.Height : null; |
Reviewer: stack:code-reviewer
| // SDK's static defaults (those are SDK internal constants, not customer-set); | ||
| // any derivation failure falls back to the SDK value. iOS navBarHeight is | ||
| // always 0 — the home indicator is static and unmeasured, fleet-consistent. | ||
| let insets = percy.maestroInsetCache.get(sessionId); |
There was a problem hiding this comment.
[Medium] Inset cache keys on undefined in self-hosted mode
Self-hosted runs have sessionId === undefined, so the cache keys on undefined. Correct for a single self-hosted run, but a later self-hosted snapshot with a different platform/pngDims on the same Percy instance would reuse the first derived value. Unlikely to bite (one self-hosted run per instance) but fragile.
Suggestion: use a sentinel key, e.g. const cacheKey = sessionId ?? '__self_hosted__'; and get/set on cacheKey.
Reviewer: stack:code-reviewer
Claude Code PR ReviewPR: #2315 • Head: efd6501 • Reviewers: stack:code-reviewer SummaryDecomposes the merged self-hosted Maestro relay (PR #2261) — extracting the Review Table
Findings
Low / informational (non-blocking, not anchored)
Verdict: PASS — faithful decomposition with security invariants intact and comprehensive coverage for the new inset derivation; two Medium nits worth tightening but nothing blocking. |
Summary
Behavior-preserving decomposition of the code merged in #2261 ("self-hosted (non-BrowserStack) Maestro + Percy support — V1"), which layered self-hosted logic onto two already-large files:
packages/core/src/api.js— the/percy/maestro-screenshothandler had grown to ~517 lines (BS file-find + feat(cli): self-hosted (non-BrowserStack) Maestro + Percy support — V1 #2261's runtime detection,PERCY_MAESTRO_SCREENSHOT_DIRscope-root, recursivedot:trueglob, branched realpath, all inline).packages/cli-app/src/exec.js— 34 → 211 lines: five Maestro argv/env-injection functions buried in the command file.This restores the module boundary (route table delegating to domain modules — the
handleSyncJobpattern) with no wire/CLI behavior change.What changes
@percy/core—api.js1072 → 382 lines (a route table):comparison-upload.js—handleComparisonUpload(multipart/percy/comparison/upload);encodeURLSearchParamspromoted toutils.js.maestro-screenshot.js—handleMaestroScreenshotorchestrator +parsePngDimensions; owns runtime detection (selfHosted = !sessionId), conditionalsessionIdvalidation, andPERCY_MAESTRO_SCREENSHOT_DIRscope-root resolution + self-hostedfilePathrejection.maestro-screenshot-file.js—locateScreenshot({…, scopeRoot, selfHosted}): BS session glob and self-hosted recursivedot:trueglob (Windows-normalized) +manualScreenshotWalk(BS only) + realpath/scopeRoot containment check (forward-slash normalized).maestro-regions.js—validateRegionInputs+resolveRegions(request-scoped dump cache, explicit-key payload merge).@percy/cli-app—exec.js211 → 47 lines (a thin command):maestro-inject.js—maybeInjectMaestroServer+maybeInjectScreenshotDir(and the private argv helpers).app:exec's callback calls them;index.jsre-exports them.Not touched:
maestro-hierarchy.js's #2261 iOS dispatch (small/coherent — the elaborate lsof cascade in #2261's description was never actually implemented)..semgrepignorepath-traversal suppression retargeted tomaestro-screenshot-file.js(+api.jsforcreateStaticServer's sitemap join).Testing
@percy/coreapi.test.js(the HTTP-level contract covering both BrowserStack and self-hosted paths, incl. feat(cli): self-hosted (non-BrowserStack) Maestro + Percy support — V1 #2261's self-hosted specs) passes unchanged — the only failure is the pre-existing IPv4/IPv6 server-disabledECONNREFUSED→AggregateErrorflake.percy.test.js+maestro-hierarchy.test.jsgreen.@percy/cli-appexec.test.js— 43/43 unchanged (imports the injectors via the@percy/cli-appbarrel).api.test.jsdrives the same route into the relocated modules;/* istanbul ignore */annotations travel verbatim). The 100% NYC gate, lint, and semgrep are validated authoritatively in CI.Post-Deploy Monitoring & Validation
nyc --check-coverage(100% across@percy/core+@percy/cli-app), lint, and the semgreppath-join-resolve-traversaljob (confirms the.semgrepignoreretarget tomaestro-screenshot-file.js).Also included: exact device system-bar insets (commits
77cfec38,ba51f3d5,efd6501a)Ported the device-inset feature onto the new decomposed modules (originally built on the now-superseded #2286 branch). The Maestro tile's
statusBarHeight/navBarHeightare now derived per-device, authoritative over the SDK's static defaults, cached per session, with graceful fallback:/viewHierarchystatusBarselement frame × empirical PNG→points scale (iPhone 14 → 141px). iOS bottom =0(static home indicator; fleet-consistent withpercy-xcui-swift/percy-appium-python).adb dumpsys windowmStableInsets(status + nav).deriveDeviceInsetsis additive inmaestro-hierarchy.js(the resolver cascade is untouched); per-sessionpercy.maestroInsetCache; any derivation failure → SDK default (never blocks a snapshot).docs/plans/2026-06-24-001-feat-port-status-bar-insets-plan.md; requirements:docs/.../2026-06-19-maestro-exact-system-bar-insets-requirements.md.Known limitation: self-hosted iOS status-bar derivation needs
PERCY_IOS_DRIVER_HOST_PORT, whichmaestro-inject.jsdoes not set, so self-hosted iOS falls back to the SDK default there (graceful). Tracked as a follow-up.Follow-ups
🤖 Generated with Claude Code