Skip to content

fix(ax-bridge-wrapper): surface structured ErrorJSON from STDOUT (#693 WU1)#694

Merged
shaun0927 merged 2 commits into
developfrom
feat/693-wu1-empty-content-root-diagnostics
Apr 29, 2026
Merged

fix(ax-bridge-wrapper): surface structured ErrorJSON from STDOUT (#693 WU1)#694
shaun0927 merged 2 commits into
developfrom
feat/693-wu1-empty-content-root-diagnostics

Conversation

@shaun0927
Copy link
Copy Markdown
Owner

Summary

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. 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 a 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 exactly this — every app_tree / app_query / app_tap_element diagnostic from the omofictions session read Command failed: ... dump --device ... --max-depth 6 and gave no hint that the underlying condition was DEVICE_CONTENT_ROOT_EMPTY.

Changes

  • src/native/accessibility-bridge.ts:exec() non-zero-exit branch now inspects error.stdout first for parseable ErrorJSON; falls back to error.stderr for legacy paths (and swift interpreter compile errors that emit JSON on stderr).
  • When neither stream parses as ErrorJSON, the thrown message includes both stdout and stderr tails 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.

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

  • 4 new unit tests in tests/unit/native-accessibility-bridge.test.ts:
    • structured ErrorJSON from STDOUT (DEVICE_CONTENT_ROOT_EMPTY round-trip)
    • fallback parse from STDERR (SIMULATOR_NOT_RUNNING legacy shape)
    • 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 src/native/accessibility-bridge.ts tests/unit/native-accessibility-bridge.test.ts — clean

🤖 Generated with Claude Code

…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>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +136 to +141
const error = err as Error & {
stdout?: string;
stderr?: string;
code?: string;
killed?: boolean;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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.

Comment thread src/native/accessibility-bridge.ts Outdated
Comment on lines +183 to +188
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(' / ')}`;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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>
@shaun0927
Copy link
Copy Markdown
Owner Author

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:

  • Non-zero exit: Node populates `error.stdout` / `error.stderr` directly.
  • Successful exit + malformed stdout: Node throws `SyntaxError` (which has no stdout/stderr fields), the catch block reads from the closure variables, and the BRIDGE_EXEC_FAILED message includes both stdout and stderr tails.

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

@shaun0927 shaun0927 merged commit 96063c2 into develop Apr 29, 2026
3 checks passed
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