fix(ax-bridge-wrapper): surface structured ErrorJSON from STDOUT (#693 WU1)#694
Conversation
…WU1) Issue #693 WU1: the Swift bridge writes its structured ErrorJSON (`{ error, code }`) to STDOUT and then `exit(1)`, NOT to STDERR — see `outputError()` in `src/native/ax-bridge.swift`. Until now the wrapper's non-zero-exit branch only inspected `error.stderr`, so every typed bridge error (`DEVICE_CONTENT_ROOT_EMPTY`, `DEVICE_RESOLUTION_FAILED`, `SIMULATOR_NOT_RUNNING`, `DEVICE_WINDOW_NOT_FOUND`, etc.) collapsed to the generic `BRIDGE_EXEC_FAILED` shape with the `Command failed: <cmd>` tail and the caller could not branch on `code`. The user-reported repro on `iPhone 17 Pro / iOS 26.4` (#693) showed the exact symptom: every `app_tree`, `app_query`, `app_tap_element` diagnostic the omofictions session produced read `Command failed: … dump --device … --max-depth 6` and gave no hint that the underlying condition was `DEVICE_CONTENT_ROOT_EMPTY` (Flutter Semantics not exposed on the visible sub-screen). Now: - Inspect `error.stdout` first for parseable ErrorJSON; fall back to `error.stderr` for legacy paths and `swift` interpreter compile errors that emit JSON on stderr. - When neither stream parses as ErrorJSON, include both stdout and stderr tails in the thrown message so the caller sees the same diagnostic shape regardless of which stream the bridge wrote to. - `AX_TIMEOUT` (killed-process) handling unchanged. This is a strictly additive change to the error-surfacing layer. Stdout parsing on the success path is untouched. No call site assumes `BRIDGE_EXEC_FAILED` for typed-error responses, so this fix does not break callers that branch on `code` — it gives them the structured codes they were always supposed to see. Verified locally: - 4 new unit tests in tests/unit/native-accessibility-bridge.test.ts cover: structured ErrorJSON from STDOUT, fallback parse from STDERR, unstructured both-stream output (BRIDGE_EXEC_FAILED with both tails), killed-process (AX_TIMEOUT preserved). - npx jest tests/unit/ — 2187 tests pass across 149 suites. - npx eslint — clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request enhances the AccessibilityBridge error handling by parsing structured JSON from both stdout and stderr and including output tails in failure messages. Feedback suggests improving SyntaxError handling to preserve diagnostic data and truncating long lines in error tails to prevent oversized log entries.
| const error = err as Error & { | ||
| stdout?: string; | ||
| stderr?: string; | ||
| code?: string; | ||
| killed?: boolean; | ||
| }; |
There was a problem hiding this comment.
Note that if JSON.parse(stdout) fails at line 125 (e.g., if the bridge exits with 0 but produces invalid JSON), the err object will be a SyntaxError which does not contain stdout or stderr properties. In this case, the error object here will have undefined for those fields, and the final error message will not include the diagnostic tails. While this PR focuses on non-zero exits, ensuring stdout is available in the catch block for all error types would improve debuggability for malformed output cases.
| const tail = (label: string, value: string | undefined): string => { | ||
| if (!value) return ''; | ||
| const trimmed = value.trim(); | ||
| if (trimmed.length === 0) return ''; | ||
| return ` | ${label}: ${trimmed.split('\n').slice(-5).join(' / ')}`; | ||
| }; |
There was a problem hiding this comment.
The tail function currently includes the last 5 lines of output without limiting the length of each line. Since maxBuffer is set to 10MB, a single line could be extremely large, leading to excessively long error messages that might be truncated or cause issues in logging systems. Consider truncating individual lines to a reasonable length.
const tail = (label: string, value: string | undefined): string => {
if (!value) return '';
const trimmed = value.trim();
if (trimmed.length === 0) return '';
const formatted = trimmed
.split('\n')
.slice(-5)
.map(line => (line.length > 256 ? line.substring(0, 253) + "..." : line))
.join(" / ");
return " | " + label + ": " + formatted;
};…xError + truncate (#693 WU1) Address gemini-code-assist on PR #694: - @:141 — `JSON.parse(stdout)` SyntaxError loses diagnostic tails: when the bridge exits 0 but stdout is malformed, the catch block runs but Node's `SyntaxError` does not carry stdout/stderr, so the previous implementation discarded the actual bridge output. Capture `{ stdout, stderr }` into closure variables on the resolve path before `JSON.parse` runs; the catch block reads from those captures when `error.stdout/stderr` are undefined. - @:188 — oversized single-line tails: with `maxBuffer: 10 MB`, a degenerate dump on a deep tree could produce a multi-megabyte error message. Truncate per-line at `STREAM_TAIL_MAX_LINE_LENGTH = 512` and append a `…[+N chars]` marker so the truncation is visible rather than silent. Two new unit tests cover both cases: - success-path malformed JSON: `mockExecSuccess('not json', 'warning')` → BRIDGE_EXEC_FAILED with both `stdout: this is not json` and `stderr: some warning on stderr` in the message - oversized single-line stdout: 2000-char line on the failure path → message contains `…[+1488 chars]` marker, full line is NOT in the message verbatim Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re: gemini @:180 (SyntaxError diagnostic-tail loss)The latest commit (`4a945f9f`) addresses this exact concern. The wrapper now captures `{ stdout, stderr }` into closure variables `capturedStdout` / `capturedStderr` on the resolve path before `JSON.parse` runs (lines 156-158), and the catch block reads them via `stdoutForDiag = error.stdout ?? capturedStdout` (lines 191-193). This covers both rejection paths:
Verified by the unit test "surfaces captured stdout in error message when JSON.parse throws SyntaxError on successful exit" in `tests/unit/native-accessibility-bridge.test.ts`. Bot review appears to have re-evaluated the new commit and re-flagged based on the same line range without picking up the closure-variable fix above. 🤖 Generated with Claude Code |
Summary
Issue #693 WU1: the Swift bridge writes its structured ErrorJSON (
{ error, code }) to STDOUT and thenexit(1), NOT to stderr — seeoutputError()insrc/native/ax-bridge.swift. The wrapper's non-zero-exit branch only inspectederror.stderr, so every typed bridge error (DEVICE_CONTENT_ROOT_EMPTY,DEVICE_RESOLUTION_FAILED,SIMULATOR_NOT_RUNNING,DEVICE_WINDOW_NOT_FOUND, etc.) collapsed to the genericBRIDGE_EXEC_FAILEDshape with aCommand failed: <cmd>tail and the caller could not branch oncode.The user-reported repro on iPhone 17 Pro / iOS 26.4 (#693) showed exactly this — every
app_tree/app_query/app_tap_elementdiagnostic from the omofictions session readCommand failed: ... dump --device ... --max-depth 6and gave no hint that the underlying condition wasDEVICE_CONTENT_ROOT_EMPTY.Changes
src/native/accessibility-bridge.ts:exec()non-zero-exit branch now inspectserror.stdoutfirst for parseable ErrorJSON; falls back toerror.stderrfor legacy paths (andswiftinterpreter compile errors that emit JSON on stderr).AX_TIMEOUT(killed-process) handling unchanged.This is a strictly additive change to the error-surfacing layer. Stdout parsing on the success path is untouched. No call site assumes
BRIDGE_EXEC_FAILEDfor typed-error responses, so this fix does not break callers that branch oncode— it gives them the structured codes they were always supposed to see.Why this is WU1 of #693
Issue #693 has two entangled failure surfaces (AX-empty Flutter sub-screens + AX-frame to iOS-point coordinate mismatch). Localising either requires the wrapper to surface the bridge's typed error code rather than
Command failed: …. WU1 unblocks all subsequent work units; WU2-WU5 in the issue body cover the substantive Flutter Semantics activation + coordinate conversion fixes.Test plan
tests/unit/native-accessibility-bridge.test.ts:DEVICE_CONTENT_ROOT_EMPTYround-trip)SIMULATOR_NOT_RUNNINGlegacy shape)AX_TIMEOUTpreserved)npx jest tests/unit/— 2187 tests pass across 149 suitesnpx eslint src/native/accessibility-bridge.ts tests/unit/native-accessibility-bridge.test.ts— clean🤖 Generated with Claude Code