Skip to content

fix: refuse component reads when no app is connected - #60

Open
boorad wants to merge 1 commit into
callstackincubator:mainfrom
boorad:fix/refuse-component-reads-with-no-app
Open

fix: refuse component reads when no app is connected#60
boorad wants to merge 1 commit into
callstackincubator:mainfrom
boorad:fix/refuse-component-reads-with-no-app

Conversation

@boorad

@boorad boorad commented Sep 10, 2026

Copy link
Copy Markdown

Summary

devtools errors prints No components with errors or warnings with nothing attached. The tree is empty because no app is connected, not because nothing matched, so a check that was never performed renders identically to a check that passed. For an agent reading command output as evidence that is worse than an error, because a vacuous pass propagates into whatever it reports.

$ devtools status
Apps: 0 connected, 0 components
$ devtools errors
No components with errors or warnings          # exit 0, before
No app is connected, so there is no component tree to read. Run `devtools status` ...   # exit 1, after

get tree, get component, find, count and errors now return { ok: false, code: 'NO_APP_CONNECTED' } when nothing is attached. Connection health is read in the same synchronous turn as the tree, so nothing can attach or detach between the check and the answer, and code is machine-readable so callers need not match on message text. cli.ts needed no change: every read already exits non-zero on ok: false.

Profiling commands are deliberately untouched, since they read captured session data that legitimately outlives the app that produced it.

Two behavior changes worth calling out:

  • get tree used to attach a disconnected Ns ago, waiting for reconnect... hint to an empty success. That context now qualifies the refusal instead, where it is actionable rather than decorating an answer that looked fine. connection-health.test.ts is updated to match.
  • Two daemon-auto-restart tests used get tree only to route a command through ensureDaemon. They now assert the refusal alongside the restart they are actually about; status cannot substitute, because it bypasses ensureDaemon entirely.

Context

Found while using this package through agent-device, which passes these commands through 1:1 — reported as callstack/agent-device#2430. I first gated it in that wrapper by probing status before each read; @thymikee correctly rejected that: a separate probe cannot guarantee the later read observed an app, since the app can disconnect in between, and an unparseable status lets the original vacuous result through. Checking inside the operation that reads the tree is his suggestion, and it is the right one. Reported independently by a second user on RN 0.87.1, where nothing ever attaches, so every errors result was vacuous.

Validation

bun run test 125 pass, bun run test:e2e 42 pass, typecheck and lint clean, build succeeds.

New component-read-attachment.test.ts covers each read refusing with no app ever connected, a read issued after the app disconnected refusing rather than answering from the stale tree, all reads still answering while attached, and status staying answerable throughout. Each new assertion was verified to fail with the daemon guard reverted (7 of them go red), so none of them pass vacuously.

Also exercised against the built CLI: errors and find against a live daemon with nothing attached both print the refusal and exit 1.

`devtools errors` printed "No components with errors or warnings" with
nothing attached. The tree is empty because no app is connected, not
because nothing matched, so a check that was never performed rendered
identically to a check that passed. For an agent reading command output as
evidence that is worse than an error: a vacuous pass propagates.

Refuse instead. `get tree`, `get component`, `find`, `count` and `errors`
now return `{ ok: false, code: 'NO_APP_CONNECTED' }` when no app is
attached. The check reads connection health in the same synchronous turn
that reads the tree, so nothing can attach or detach in between, and the
code is machine-readable so callers need not match on message text.

`get tree` used to attach a "disconnected Ns ago" hint to an empty success.
That context now qualifies the refusal, where it says something actionable
rather than decorating an answer that looked fine.

Profiling commands are untouched: they read captured session data, which
legitimately outlives the app that produced it.

Two auto-restart tests used `get tree` purely to route a command through
ensureDaemon; they now assert the refusal alongside the restart they are
actually about, since `status` bypasses that path.
boorad added a commit to boorad/agent-device that referenced this pull request Sep 10, 2026
The probe cannot guarantee the read that follows it observed an app, and
its unparseable-status branch let the vacuous result through — fail-open
by default in the one place a false clean is the bug. Attachment is
checked in the operation that reads the component tree instead, upstream
in callstackincubator/agent-react-devtools#60, which this repo picks up
through the existing 1:1 passthrough once the pin bumps.
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