Commit b3ef687
fix(cli): make a failed stderr write non-fatal on the published entry point (#16686)
* fix(cli): make a failed stderr write non-fatal on the published entry
`bin/run.js` writes to `process.stderr` with no `error` listener, so an
`error` event there is an uncaught exception. #15564 was filed NOT
REPRODUCED, and its fence was explicit: symmetry with `bin/run-dev.js` is
not evidence, establish reachability first.
Both of the card's probes were re-run before anything was written here and
both still read clean — exit 2, no `uncaughtException`, 3/3 each. They were
not a guard, they were the wrong lifecycle: everything a failing invocation
puts on stderr is written after `run()` has settled, by `handle()`, which
exits on top of its own report, and a failing write reports through libuv's
completion callback that a synchronous exit is never told about.
Widening to a lifecycle that outlives its first failed write reaches it.
`os serve` on `examples/app-todo`, read end destroyed:
uncaughtException code=EPIPE msg=write EPIPE
at afterWriteDispatched (node:internal/stream_base_commons:159:15)
exit code=1
3 of 3 runs, 3049-3433 ms in — the same frame and status #14858 traced on
the dev shim. The same child read by a draining parent boots, serves, and
exits 0 after 7926 bytes over 16.6 s.
Pinned with the hazard manufactured inside the published binary's own
process, alongside a live positive control that removes the listener in the
child and re-crashes it, so the guarded arm's silence is a reading.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
* fix(cli): name the stderr error listener so the pin can see it
The first ablation of this change caught a phantom check in its own pin: a
probe polling `process.stderr.listenerCount('error') > 0` reported
`LISTENER ATTACHED after 20 ms` against a tree with the whole listener
block deleted. Node parks an anonymous `once('error', noop)` on this stream
for the duration of every `console.error` (`ignoreErrors`), so a count
cannot tell this listener apart from that one, and the poll doubled as the
probe's only synchronisation point — a slow box would have written before
the attach and red a working tree.
The listener is named, the probe waits for that name, and a parity case
holds the probe's mirror equal to the entry's spelling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
* test(cli): pin the attach ORDER, the real writer, and a named control
Three review-adopted fixes to the #15564 pin, none of which change
`bin/run.js`'s behaviour.
F1 — the order case pinned PRESENCE, not order. `LISTENER ATTACHED after
N ms` is true for any N inside the probe's 15 s wait, so an attach moved
below `await run(…)` would keep every case green while the entry's own
claim ("BEFORE `run()`, and that order is the whole point") had stopped
being true. The runtime cases cannot see it — one process, `--version`
settles oclif in a few hundred ms, both have happened by the time the
poll looks — so the order is now read STRUCTURALLY from the entry's
comment-masked source, in a case of its own, and the presence case is
renamed to claim only what it pins.
F2 — the premise case asserted that `serve.ts` contains
`process.stderr.write(`. That file holds about a dozen such sites, so it
stayed green even if `printDiagnostic` — the one writer the reproduction
ran through (#7915) — moved to `console.error` and stopped being able to
crash anything. It now anchors on `printDiagnostic`'s own body, located
by symbol and brace-matched, with a length bound so a desynchronised
match reds instead of reporting green about some other writer.
F3 — the unguarded (positive-control) arm called
`removeAllListeners('error')`. Equivalent today, but it measures "no
listener at all" rather than "the entry's listener absent"; it now
removes the guard BY NAME, and marks the resulting `guard=` reading.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
* test(cli): run the stderr-error pin per-PR, without a borrowed dist gate
The pin drives `bin/run.js --version` under a `--import` probe and reads the
entry's source; nothing it measures lives in `dist`. Measured with
`packages/cli/dist` moved aside: `--version` printed
`@objectstack/cli/17.3.0 linux-x64 node-v22.22.2`, and all six cases passed —
guarded arm `LISTENER ATTACHED … WROTE … SURVIVED … EXIT code=7`, unguarded arm
`UNCAUGHT code=EPIPE … EXIT code=1`. So `PUBLISHED_ENTRY_NEEDS_DIST` was a
sentence borrowed from `published-entry-stderr-nonblocking.e2e.test.ts` (whose
guard genuinely IS the compiled `../dist/utils/stderr-nonblocking.js` import)
without the reason coming with it — the exact failure the helper's docblock
above `RUN_JS_RESOLVES_FROM_DIST` names. Removed, and replaced by a note that
states the measurement and the difference from that neighbour.
With no dist dependency the file has no reason to sit in the nightly `.e2e`
tier: renamed out of it, so it runs per-PR in `--project integration` (34 → 35
files). Both neighbouring `.e2e` suites are untouched (#16455).
Also corrects a wrong count this branch introduced: `serve.ts` holds 2
code-position `process.stderr.write(` sites (`printDiagnostic` and the
artifact `warn:` adapter), not "about a dozen".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <sales@objectstack.ai>1 parent 7797102 commit b3ef687
4 files changed
Lines changed: 617 additions & 0 deletions
File tree
- .changeset
- packages/cli
- bin
- test
- fixtures
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
73 | 151 | | |
74 | 152 | | |
75 | 153 | | |
| |||
Lines changed: 154 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
0 commit comments