Repository navigation
Description
Iris Maintainer Agent here.
test/editor-patch.test.ts has died twice under the full npm test run, in a way that no assertion explains. Filing it because a silent non-zero exit is the one failure mode that can also hide a real one, and because it is rare enough that the next person to see it will assume it is a flake and move on.
What it looks like
✖ failing tests:
test at test/editor-patch.test.ts:1:1
✖ test/editor-patch.test.ts (1585.773916ms)
'test failed'
'test failed' with no assertion, no diff, and no stderr at all — the run was captured with 2>&1 and a grep for Error|error:|stderr|Warning across the whole log matched nothing from that file. That message is what the runner reports when a test file's process exits non-zero without having reported a failure, so the child died rather than failed.
It died part-way through. The last line it printed was a block that gave its IMAGE to another block is not re-seated either, which is test 40 of 47, and the totals show 1550 registered against 1558 — the ~8 tests after that point were never registered.
Observations so far
| Seen on | branch retire-prd-399 (once), branch docs-concise-400 (once) |
File alone (node --test test/editor-patch.test.ts) |
47/47 pass |
Full suite, repeated on docs-concise-400 |
9 consecutive clean runs, 1558–1559/1559, exit 0, empty stderr |
So it is roughly 2 in ~15 observed full-suite runs and does not reproduce on demand. It appeared before any of the #400 work existed, and neither branch touched the editor pipeline or this test, so it is not either branch's diff.
What I ruled out
- Shared temp state. The file's harness takes
mkdtempSync(join(tmpdir(), "iris-patch-"))per call and removes it in afinally, so two parallel files cannot collide on a path. - An explicit exit. No
process.exitanywhere in the file.
What it is probably worth looking at
node --test runs files in parallel up to the core count (the passing runs sit around 434% CPU), so the candidates are resource pressure or a signal — both of which produce exactly this signature: a dead child, no output. An unhandled rejection would normally print, which argues against it, but a rejection raised inside the harness's router.complete callback after its test has already settled is the one shape worth checking, since assert.ok(shown, …) throws in there.
Two cheap things that would settle it, neither of which needs the failure to be reproducible on demand:
- Run the suite with
--test-reporter=tap, which prints the child'sexitCodeandsignalin the YAML diagnostic. The spec reporter throws both away, which is why two occurrences have told us nothing. (I've had a loop doing this ondocs-concise-400; 8 runs, no failure, so the diagnostic is still uncaptured.) - If it turns out to be a signal under memory pressure,
--test-concurrencyis the knob, and CI is where it will bite — a silent child death there reads as a normal red build with an unhelpful message.
Noted on #404, which is where I hit it the second time.
Activity
bbertucc commentedon Sep 5, 2026
Iris Maintainer Agent here.
Settled, and it is not this test file. It is a SIGSEGV inside node's own garbage collector. PR #409.
The signature was reproducible without reproducing the failure. A throwaway file that kills itself mid-run renders under
specas, character for character, what this issue reports:The same run under
tap:So
'test failed'with no assertion and no stderr is the runner's report of a child that died without reporting — andspecthrows away the two fields that say which. That closes the "no assertion explains it" part of this issue: nothing was hidden, the reporter discarded it. Also worth correcting from the issue body: spec is the default even when stdout is not a TTY (checked by piping throughcat), so CI has been losing it too.What died.
~/Library/Logs/DiagnosticReportsholds sixnode-*.ipscrash reports over the four days here — Sep 1 12:45, Sep 3 14:31, Sep 4 13:48, 15:51, 18:53, 19:14 — every one a SIGSEGV in a child ofnode, all with the same stack and the same frame offsets:Faulting on
0xeand0x6. Those are tagged Smis (7 and 3), so V8 is treating a frame slot holding a small integer as a heap pointer while marking roots. Node v24.16.0, macOS 26.3.1.Each guess in this issue, against evidence rather than argument:
JetsamEvent-*.ips(08-29 02:34 … 09-04 06:56) coincide with none of the six crashes. macOS did not kill it; it crashed.router.complete— no. That exits non-zero with output. This takes SIGSEGV with a native stack and no JS frames.ClearStaleLeftTrimmedPointerVisitoronly has work after an array is left-trimmed, which in V8 meansArray.prototype.shift. The entire tree has one.shift()—src/util/queue.ts:95— andtest/editor-patch.test.tsdoes not reach it (util/queueis imported only bysrc/util/requestLimits.ts,src/routes/sessions.tsandtest/queue.test.ts). The trim is node's own.--test-concurrencyas the knob — plausible as a mitigation, but it would be tuning against a crash we cannot yet attribute to a run. Not doing it on this evidence.What I am not claiming. These six reports are not tied to the two failures in this issue — nothing retained links a crash report to a suite run, and four of the six fall in windows where the suite reported all-pass, so at least some belong to other node processes. The mechanism is certain; the attribution of any single occurrence is not. Which is exactly why the fix is to make the next one name itself rather than to chase this one.
So #409 does two things and stops there:
npm testnow printssignal/exitCodewhen a child dies, at the failure and again as the last line of the run (both CI workflows readnpm testthroughtail,-120and-40, so an early death would otherwise be above the cut); andCONTRIBUTING.mdplus theissue-to-pr.ymlprompt stop telling peoplenpm testis a barenode --test, which would route around it.It leaves the crash upstream where it belongs. If it recurs after #409 it will arrive with
SIGSEGVon the line, and a fresh.ipsto attach to a node report — which is the thing this issue could not produce in ~17 runs of trying.Two corrections to my own reading in the body above: I wrote that the ~8 tests after the last printed one "were never registered". They were registered — registration is synchronous at module load — they were never run, which is why the pass count came out short rather than the suite going red on them. And "roughly 2 in ~15 observed full-suite runs" is a rate for observed suite failures, not for the crash; the crash reports say the segfault happens more often than the suite notices.
fix(test): read test:complete, and stop the probe writing real crash …
fix(test): name the signal, so a crashed child stops reading as a fla…
bbertucc commentedon Sep 9, 2026
Iris Maintainer Agent here.
Reopening: the reporter this issue asked for has caught it, and the answer is not a test change. The children die of SIGSEGV inside V8's garbage collector.
It happened again on run 5 of a 10-run
npm testloop this morning (worktree-strip-boundary-439-440, tree clean, PR #444's merged head), andtest/spec-with-signals.mjsprinted the diagnostic the spec reporter used to throw away:Same signature as the original report:
'test failed'with no assertion, and 1,641 of 1,649 registered — the ~8 tests after the point of death never existed.editor-patch.test.tsis the file this issue was filed about;editor-salvage.test.tsis new, and it died in the same run.The crash reports, which say where
macOS wrote one
.ipsper dead child, both at the same second, and they are identical:~/Library/Logs/DiagnosticReports/node-2026-09-09-075605{,.000}.ipsEXC_BAD_ACCESS (SIGSEGV),KERN_INVALID_ADDRESS at 0x000000000000000eThe faulting stack, top down:
A mark-compact GC, entered from a stack-guard interrupt, dereferencing
0xewhile iterating roots. Every frame is in thenodeimage — the only other images in the report aredyldand two libsystem stubs, and the JS below the interrupt is generic trampolines. So neither test file is doing anything the crash can be attributed to: they died because they were the two processes GC happened to be running in.That resolves the two candidates this issue named. It is not an unhandled rejection in the harness, and it is not a kill under memory pressure — a SIGKILL is what that would look like. It is a V8 bug, reached under the parallelism
node --testuses.Rate
node --test test/editor-patch.test.tsaloneConsistent at roughly 1 run in 8–12, and still not reproducible on demand.
What I am NOT doing
No test change, because there is no honest one. Lowering
--test-concurrencywould make it rarer without making it not happen, and it would slow every clean run to hide a crash that is not ours. The decisions this leaves are a human's: bump or pin Node (v24.16.0 is what crashed; the symbol above is the thing to search a Node/V8 changelog for), and decide whether CI should retry a run whose only failures are dead children — in CI this reads as a normal red build, which is the part that costs someone an afternoon.What #405's reporter bought is that the next occurrence anywhere, including CI, names the signal in the log instead of
'test failed'. That part worked exactly as intended, which is why this is worth reopening rather than refiling.cleanup: the same words are a defect in one file and true history in …
bbertucc commentedon Sep 11, 2026
Iris Maintainer Agent here.
One more dated occurrence, since the rate is the only thing still accumulating on this issue.
Hit it today (2026-09-11) on
worktree-cleanup-passwhile running the gates for #461's round 3. Same signature, same file, and the reporter named it without being asked:One child this time, not two. Node v24.16.0, macOS 26.3.1 — unchanged from the 2026-09-09 report, so nothing about the environment has moved.
The diff it happened on is two comments (
src/config.tsandtest/review-cap.test.ts), which is as strong a "not the branch's code" control as this failure gets. The re-run was 1694/1694, exit 0.Rate, added to what is already here: 1 of 4 full-suite runs in this session's window (three earlier runs today were clean). That keeps it in the same 1-in-8-to-12 band across a bigger denominator rather than moving it.
Nothing new to diagnose — the V8 GC stack in the comment above is still the answer, and the two open decisions are still a human's: whether to bump or pin Node off v24.16.0, and whether CI should retry a run whose only failures are dead children. Recording this only so the next reader can see the frequency has not changed and the diagnostic keeps working outside a deliberate loop.
test: the pin asserts the flag's position, not just its presence
test: run the suite with Sparkplug off, which is what kills the #405 …
bbertucc commentedon Sep 14, 2026
Iris Maintainer Agent here.
Closing this. The crash has an upstream name, and the mitigation shipped in #477 (
72bd975).What it is
nodejs/node#62393, fixed in V8 by CL
0b94a9fd23ba: "[leaptiering] Fix BaselineOutOfLinePrologue builtin … which tried to preservekJavaScriptCallDispatchHandleRegistereven on configurations where it's not used which resulted in arandom value on the stack discoverable by GC."
That is this issue's stack exactly.
BaselineOutOfLinePrologueis Sparkplug's prologue; the strayregister value is a small integer;
ClearStaleLeftTrimmedPointerVisitorreads it as a heap pointerwhile marking roots and faults at
0xe— the address in every report here. Nothing intest/editor-patch.test.tswas ever wrong, which is why the file kept changing and the deaths kepthappening.
The fix
npm testnow runs with--no-sparkplug. No Sparkplug code means that builtin is never entered,so the path is gone. It costs nothing measurable: 55.8 s mean either way over two full runs each here,
and the reviewer measured ~3% on Linux x64 over the two heaviest jsdom files, against a 22-minute
review step. This suite is jsdom- and I/O-bound; the baseline tier was buying it very little.
The flag is on the test script only, deliberately —
npm startandnpm run devkeep the Sparkplugpath, because one dev server dying of SIGSEGV is loud, where a dead test child reads as a clean run
with a short pass count. That asymmetry is the whole reason this issue was hard.
A test pins the flag and its position.
node --test "test/*.test.ts" --no-sparkplugexits 0, warnsabout nothing, and the child's
execArgvdoes not carry the flag — anything after the positional goesto the runner, not to V8. So the pin asserts
--no-sparkplugcomes before--test.test/spec-with-signals.mjsstays. It is what named this crash instead of costing an afternoon, and itis what would catch the next dead child of any cause — including this one, if the flag is ever dropped
early.
The two decisions this issue was holding — both are no
open, not landed — rebased onto
v24.x-stagingafter v24.21.0, CI green, waiting. No released 24.x has the fix, and one upstream report has the
crash live on v26.7.0, which corrects the earlier "fixed in 25.0.0" reading.
.nvmrcstays at24. Separately: this machine runs v24.16.0 while.nvmrcgives CI v24.21.0. Updatinglocally is ordinary hygiene, not a fix for this.
upstream — is macOS arm64, and every workflow here runs
ubuntu-latest. Retrying a failure neverseen on that platform would be masking on speculation, and with Sparkplug off there is nothing left
to retry.
One correction to the rate on this issue
Ten
node-*.ipsreports are retained on this machine. Five carry this stack — 09-08 21:48,09-09 07:56, 09-09 13:02, 09-11 15:23, 09-13 20:19. The other five are a different failure:
SIGABRTout ofv8::internal::Heap::FatalProcessOutOfMemory, i.e. heap exhaustion, clustered inpairs and triples. Worth separating so the next reader does not fold them into this crash's frequency.
When to undo this
Drop
--no-sparkplugand its pin test together when nodejs/node#65753 ships in a 24.x release.That removal condition is written where the flag is, in
CONTRIBUTING.md § Development.
Reopen this if a dead child appears with the flag on — that would be a different bug, and the reporter
will say so.
Thanks for keeping the dated occurrences coming on this one; the rate is what made it worth chasing to
upstream rather than retrying around.
🤖 Generated with Claude Code