Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 58 additions & 12 deletions packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,12 @@ const STALL_MS = 10_000;
* The shim's own no-progress bound, mirrored from `bin/run-dev.js`
* (`STDERR_DRAIN_STALL_MS`) and held equal to it by a case below rather than
* trusted. Case 5's ceiling no longer budgets it — that ceiling is a constant
* now — but two cases here are still sized against it and would quietly stop
* now — and case 6 no longer reads it either, having stopped judging by a wall
* clock at all. ONE case is still sized against it and would quietly stop
* discriminating if it moved:
*
* • `STALL_MS` above must stay strictly BELOW it, or case 4's stalled reader
* outlasts the shim's own give-up and reds against a WORKING fix;
* • case 6 reads the closed-reader path as released in less than `STALL_MS`,
* which is evidence of a fast path only while `STALL_MS` is itself below
* the bound.
* outlasts the shim's own give-up and reds against a WORKING fix.
*/
const SHIM_DRAIN_STALL_MS = 15_000;

Expand Down Expand Up @@ -457,12 +455,60 @@ describe('the mirror direction: a reader that is never coming back', () => {
expect(Number(String(declared).replaceAll('_', ''))).toBe(SHIM_DRAIN_STALL_MS);
});

it('a CLOSED read end is released at once, not held for the bound (EPIPE reaches the callback)', () => {
// Pins the fast path measured alongside the hang: when the reader is gone
// rather than idle, the write callback fires with EPIPE and the wait ends
// immediately. A future change to the bound must not quietly make the
// closed-reader paths pay it.
expect(closedEnd.signal).toBeNull();
expect(closedEnd.elapsedMs).toBeLessThan(STALL_MS);
it('a CLOSED read end ends the child on its own — by an uncaught EPIPE, never by the bound', () => {
// ⚠️ This case used to read `elapsedMs < STALL_MS`, and its name used to
// say "released at once … EPIPE reaches the callback". BOTH were wrong
// about this shape, and the trace that settles it is worth more than the
// assertion it replaces.
//
// What the child ACTUALLY does with its read end destroyed: oclif's
// `displayWarnings()` makes the first stderr write, the pipe is already
// gone, node raises `write EPIPE` as an `error` event on `process.stderr`,
// NOTHING IS LISTENING, and the process dies of an uncaught exception —
// exit 1, ~1.4 s in. `writeStderr()` is never called, so the bound this
// case was named after is never armed, let alone paid. Traced on one box
// with a `--import` observer: the shim's own 415-byte write is #175, at
// 9250 ms, behind 174 oclif writes that all EPIPE — 7.8 s after the
// unobserved child is already dead.
//
// ⛔ So the wall-clock bound was not merely fragile, it was a PHANTOM: it
// could not fail for the reason it named. Ablated on `bin/run-dev.js`,
// same box, same probe, with the old bound's verdict in brackets:
//
// pristine exit 1, 1387-1711 ms [green]
// write callback REMOVED, so a closed path
// could only finish on the bound — the
// regression this case named exit 1, 1517-1633 ms [GREEN]
// EPIPE made non-fatal, callback kept exit 2, 8787-8979 ms [green, 1.2 s spare]
// both, so the path really pays the bound exit 2, 23601-23712 ms [red]
//
// The bound moved only on lines 3 and 4, which change the EXIT CODE too;
// against its own regression it stayed green. And its whole measured term
// is child cold start, which is elastic — 1.4 s here, 8.9 s the moment
// anything lets the child run further — judged against 10 s borrowed from
// case 4's parent stall, a number with no relationship to this case.
//
// ⭐ The exit status IS the observation the wall clock was standing in for,
// and it carries no load term at all. 1 means the child died on its first
// write and never reached the drain; 2 means it got through to `handle()`,
// which is only reachable THROUGH `writeStderr()` — bound paid or not. Every
// ablation above that reaches the drain flips it, including the one the old
// assertion could not see.
//
// ⚠️ 1 is what the CLI DOES, not what anyone contracted: a caller whose
// stderr is closed gets 1 where every other reader gets 2, and cannot tell a
// failed command from a crashed CLI. Filed as #14858. If that is fixed to
// exit 2 this case reds, which is the point — the fixing PR flips the number
// here and says why. ⛔ Do not "repair" a red by loosening this to
// `not.toBeNull()`; that is the phantom check all over again.
const evidence =
`closed-read-end child ran ${closedEnd.elapsedMs} ms (harness cap ${UNREAD_HARD_CAP_MS} ms); ` +
`case 1 measured the same child at ${unbuilt.elapsedMs} ms on this runner minutes earlier`;
expect(closedEnd.signal, `the harness SIGKILLed the child — it was still alive at the ceiling. ${evidence}`).toBeNull();
expect(
closedEnd.code,
`the child did not die on its first stderr write — it reached the shim's drain, so something now ` +
`tolerates EPIPE on stderr (see #14858 and the ablation table above this assertion). ${evidence}`,
).toBe(1);
});
});
Loading