Skip to content

fix(device): hard-fail device_screenshot on explicit-platform raw failure (closes #136 sub-issue 1)#174

Merged
Lykhoyda merged 2 commits into
mainfrom
fix/gh-136-screenshot-hard-fail
May 19, 2026
Merged

fix(device): hard-fail device_screenshot on explicit-platform raw failure (closes #136 sub-issue 1)#174
Lykhoyda merged 2 commits into
mainfrom
fix/gh-136-screenshot-hard-fail

Conversation

@Lykhoyda
Copy link
Copy Markdown
Owner

Summary

Closes the first of three sub-issues in #136device_screenshot platform: \"android\" silently capturing the iOS screen when both simulators are booted. PR #142 (the prior fix) shipped a raw-command path that bypasses agent-device's broken --platform routing, but kept a "graceful fallback" that fell through to that same broken path on any raw failure. The fallback was the regression vector.

Root cause

In the user-reported session, the Android emulator was in an OOM-unstable state. adb devices reported it as offline, parseAdbDevicesEmu's ^(emulator-\\d+)\\s+device\\b regex skipped it, the resolver returned null, the raw path returned null, and the fallback to runAgentDeviceFn fired — which then routed via agent-device's --platform flag (the exact broken path #142 was meant to avoid), capturing the iOS screen.

The comment in device-list.ts defended this as graceful degradation preserving single-device behavior. That reasoning was backwards: single-device users don't pass platform: explicitly (it's inferred from CDP target), so they never hit this branch. Only the multi-device case sets platformExplicit=true, and for that case silent fallback defeats the entire purpose of the arg.

Fix

When platformExplicit && raw fails, return a structured failure with the actionable reason instead of falling through.

  • device-screenshot-raw.ts: tryRawScreenshot now returns a discriminated union {ok:true,path} | {ok:false,reason: 'no-device' | 'capture-failed'} so callers can distinguish a resolver miss (no device booted / offline) from a capturer failure (transient adb/simctl error).
  • device-list.ts: captureAndResizeScreenshot returns failResult(...) with code=SCREENSHOT_FAILED and meta={platform, reason} on explicit-platform failure. Error message names the CLI (xcrun simctl / adb) and the sub-cause so callers can act.
  • Implicit-platform calls (platformExplicit=false) still route through runAgentDevice unchanged — backward parity preserved by the existing 'platformExplicit=false → uses runAgentDevice (backward parity)' test.

Test plan

  • 1465/1465 cdp-bridge unit tests passing locally
  • Inverted the existing 'falls through to runAgentDevice' test to assert hard-fail (the test had been encoding the regression behavior)
  • Added a second hard-fail test for the capture-failed surface (distinct from no-device)
  • Tightened 3 pre-existing tryRawScreenshot tests from assert.equal(result, null) to assert.deepEqual(result, {ok:false, reason:...}) on the new union shape
  • codex-pair reviewed every edit; final state has zero outstanding concerns from this PR
  • CI green
  • Live multi-device verification (out of scope for this PR — needs the dual-device harness mentioned in the deferral comment)

Out of scope

  • Sub-issue 2 (cdp_status dev-client picker auto-tap) — needs an Expo Dev Client test-app to reproduce. Tracked in Multi-device routing + dev-client picker hangs (combined session feedback) #136.
  • Sub-issue 3 (launchApp dev-launcher handler) — same prereq.
  • Unrelated pre-existing HIGH flagged by codex-pair: deriveScreenshotPath throws PathTraversalScreenshotError for .. paths, but no catch wraps the handler — a bad path becomes an uncaught tool exception instead of a structured failResult. Filed as a follow-up, not addressed in this PR.

Refs

🤖 Generated with Claude Code

Lykhoyda and others added 2 commits May 19, 2026 22:37
codex-pair (ask-llm plugin) is opt-in via a .codex-pair-context.md marker
in the project root. The marker enables the PostToolUse hook; its content
is the per-file project context the reviewer uses.

Marker is per-developer (own model/threshold preferences, own rules) so
it does not belong in shared history. Same for the log, cache, and pause
sentinel directories — all developer-local.

See https://github.com/Lykhoyda/ask-llm marketplace plugin for details.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…raw path fails

Closes #136 (sub-issue 1 — re-evidence on #60).

PR #142's "graceful degradation" — on raw screenshot failure, fall through to
runAgentDevice — was the regression vector. agent-device's --platform routing
is exactly the broken path PR #142 was created to avoid. The fallback re-
introduced the bug whenever the raw path tripped on transient state (the
user's session: OOM-unstable emulator → adb devices reports it as 'offline'
→ parseAdbDevicesEmu skips it → resolver returns null → fallback fires →
iOS screen captured for a platform=android request).

New behavior: when platform: is passed explicitly and the raw path fails,
return a structured failResult with code=SCREENSHOT_FAILED and
meta.reason=('no-device' | 'capture-failed') instead of trying agent-device.
The error message names the underlying CLI (xcrun simctl / adb) and the
sub-cause so the caller knows what to fix (boot a device vs retry a transient
state). Implicit-platform calls (platformExplicit=false) still route through
runAgentDevice — backward parity preserved by the existing
'platformExplicit=false → uses runAgentDevice (backward parity)' test.

Changes:
- src/tools/device-screenshot-raw.ts: tryRawScreenshot now returns a
  discriminated union {ok:true,path} | {ok:false,reason} so callers can
  distinguish resolver miss from capturer failure.
- src/tools/device-list.ts: captureAndResizeScreenshot returns failResult
  on explicit-platform raw failure; imports failResult.
- test/unit/gh-136-screenshot-raw-platform.test.js: inverted the existing
  'falls through' test to assert hard-fail; added a second hard-fail test
  for the capture-failed surface; tightened the three pre-existing
  resolver/capturer null-checks to deepEqual on the new union shape.

Verified: 1465/1465 cdp-bridge unit tests passing (+1 net new test).

Also included: scripts/cdp-bridge/dist/tools/device-permission.js — dist
was stale relative to source (PR #169's second escapeRegex site fix never
made it to dist). Rebuilt locally; no source change.

NOTE: codex-pair flagged an unrelated pre-existing issue in this file:
deriveScreenshotPath throws PathTraversalScreenshotError on a '..' path,
but no catch wraps the handler, so a bad path becomes an uncaught tool
exception. Filed as #136-followup, NOT addressed here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Lykhoyda Lykhoyda merged commit 3f23ac6 into main May 19, 2026
7 checks passed
@Lykhoyda Lykhoyda deleted the fix/gh-136-screenshot-hard-fail branch May 19, 2026 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant