You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Error: Unable to deserialize cloned data due to invalid or unsupported version #64061
While trying to migrate appium tests from mocha to the native test runner I face a weird exception, coming from the test runner itself:
type: 'test'
location: '/home/runner/work/appium/appium/packages/images-plugin/build/test/e2e/plugin.e2e.spec.js:1:1'
failureType: 'uncaughtException'
error: 'Unable to deserialize cloned data due to invalid or unsupported version.'
code: 'ERR_TEST_FAILURE'
stack: |-
#processRawBuffer (node:internal/test_runner/runner:354:20)
FileTest.parseMessage (node:internal/test_runner/runner:290:27)
Socket. (node:internal/test_runner/runner:400:15)
Socket.emit (node:events:519:28)
addChunk (node:internal/streams/readable:561:12)
readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
Readable.push (node:internal/streams/readable:392:5)
Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
This exception is always reproducible in the CI env. In local env (macOS) it may go away after several retries, but then returns, which makes it intermittent.
To repro it locally:
git clone https://github.com/appium/appium.git
git checkout 1392fc7fb123745db4ed91644cefede5cf5cec8b
npm run build
cd packages/images-plugin
npm run test:e2e
How often does it reproduce? Is there a required condition?
Always reproduces in CI, randomly reproduces in local env
What is the expected behavior? Why is that the expected behavior?
I expect the test to pass, or at least provide a meaningful stack trace, so I can trace and fix the original issue if it's in my code.
After further investigation this errors seems to pop up if process.execPath is invoked in the suite setup section via node:child_process->spawn. It is important though that it is invoked by another imported module, not the same that runs the test.
I've found a viable workaround for that just by running process.execPath with --version argument as the very first command in the before section.
We hit this same error on v24.15.0 (Windows) and traced it to what looks like a root cause: the frame-length read in #processRawBuffer is signed, so a desync that should be recoverable becomes fatal.
The arithmetic
lib/internal/test_runner/runner.js (still present on main as of today):
// We call `readUInt32BE` manually here, because this is faster than first converting// it to a buffer and using `readUInt32BE` on that.constfullMessageSize=(bufferHead[kV8HeaderLength]<<24|bufferHead[kV8HeaderLength+1]<<16|bufferHead[kV8HeaderLength+2]<<8|bufferHead[kV8HeaderLength+3])+kSerializedSizeHeader;if(this.#rawBufferSize <fullMessageSize)break;
The comment says readUInt32BE, but << and | are signed 32-bit in JS. If the byte at kV8HeaderLength is >= 0x80, fullMessageSize is negative, so this.#rawBufferSize < fullMessageSize is false, the loop does not break to wait for more data, and it proceeds to deserialize whatever is in the buffer — producing Unable to deserialize cloned data due to invalid or unsupported version.
When the same bytes are read unsigned, the value is a huge positive, the guard breaks, and the parser waits for more data instead of crashing.
Minimal demonstration
constkV8HeaderLength=2;// v8Header is [0xFF, 0x0F]constkSerializedSizeHeader=6;constrawBufferSize=4096;// plenty bufferedfor(constlineof[' ✒️ log line',' >> log line']){constb=Buffer.from(line,'utf8');constsigned=(b[2]<<24|b[3]<<16|b[4]<<8|b[5])+kSerializedSizeHeader;constunsigned=b.readUInt32BE(2)+kSerializedSizeHeader;console.log(JSON.stringify(line),'byte[2]=0x'+b[2].toString(16));console.log(' signed ',signed,'-> breaks?',rawBufferSize<signed);console.log(' unsigned',unsigned,'-> breaks?',rawBufferSize<unsigned);}
Why it looks intermittent and environment-specific
The children write the v8-serialized protocol and ordinary console output to the same fd. #processRawBuffer strips non-serialized bytes once per call, after which the loop treats the next position as a frame boundary. When a chunk boundary lands such that console bytes sit where a length is expected, those bytes get read as the length.
That makes the trigger a function of chunk coalescing, which is why it is load- and timing-dependent — and why anything that changes output volume or scheduling (such as the child_process.spawn in the setup that @mykola-mokhnach identified) changes how often it fires. We suspect that is a correlate rather than the cause: the cause is that non-protocol bytes reach the parser at all, and the signed read is what makes the resulting desync unrecoverable.
The content of the console output decides whether you ever notice. ASCII output yields a huge positive length, the guard breaks, and the stream self-heals silently. Output whose byte at offset 2 is >= 0x80 yields a negative length and crashes. Our application logs are prefixed " <emoji> …", which parks 0xE2/0xF0 at exactly offset 2 — so our suite tripped it constantly while suites with ASCII logs never do.
In one suite (~440 tests) on v24.15.0, Windows, our most controlled measurement was a sequential before/after rate check: 6 of 10 full runs failed with the emoji-prefixed logs, and 0 of 10 after guarding our own output (independently re-checked: another 5 clean runs). Less rigorously, during investigation we also observed the failure while running with --test-concurrency=1, and never observed it when running the affected files individually — so this does not appear to be purely a cross-child interaction, though we did not measure those two conditions to the same standard.
During that investigation we also captured the children's stdout from inside the child processes; the captured streams were well-formed, and the children were killed mid-stream after the parent errored — consistent with the corruption arising in the parent's parsing rather than in what the children emit.
or keep the manual math and make it unsigned with >>> 0. That alone converts these crashes back into the "wait for more data" path.
Worth noting the failure mode is silent for anyone whose logs happen to be ASCII, so this may be under-reported relative to how often the underlying desync occurs.
As a workaround until it lands, we guard our own side in the test harness by ensuring no console line has a byte >= 0x80 at offset 2 — 15 consecutive clean runs since, against a ~60% failure rate before.
Isolated trigger: a test file writing to stdout while the runner is under concurrent load
Reproduced deterministically on Windows, Node v24.14.0, 12-core machine. I believe this isolates the condition rather than just the symptom.
Mechanism.node:test runs each test file in a child process and forwards that child's raw stdout to the parent over the same pipe that carries its binary v8-serialized result frames (the transport introduced in da27542, replacing TAP text). When a test file writes unstructured text to stdout while several files run concurrently, the parent's reassembly of that interleaved stream fails at a chunk boundary:
Error: Unable to deserialize cloned data due to invalid or unsupported version.
at #processRawBuffer (node:internal/test_runner/runner:392:20)
at FileTest.parseMessage (node:internal/test_runner/runner:328:27)
at Socket.<anonymous> (node:internal/test_runner/runner:438:15)
at Socket.emit (node:events:508:28)
at addChunk (node:internal/streams/readable:563:12)
at Readable.push (node:internal/streams/readable:394:5)
at Pipe.onStreamRead (node:internal/stream_base_commons:189:23)
The runner then fails the whole file, with no relationship to anything that file asserts.
Controlled experiment. One file in a 101-file suite emitted 48 lines of raw stdout (it exercised its module in-process, so the module's console.* calls landed in the test process's own stdout). All runs node --test --test-concurrency=32:
configuration
stray stdout
runs
failures
suite as-is
48 lines
9
6 — all on that one file
that file excluded
—
3
0
that file included, its console.* stubbed
0 lines
3
0
The third row is the load-bearing one: keeping the file and removing only its stdout is as clean as removing the file entirely. So the variable is the raw text on the pipe — not the file's presence, its test count, or its assertions.
Dose-response. At the default concurrency (= core count, 12 here) the same suite passed 2/2. At 32-way it failed 6/9. Lowering concurrency suppresses it without removing it.
Two corollaries that may help triage:
The failure is a reporting failure, not an execution one. In our case a test calling a pure function with hardcoded literal inputs was reported as failing — it cannot fail on its own, so the result message was lost or corrupted in transit rather than the test having run and failed. Anyone debugging this by looking at the named test will find nothing wrong with it.
Removing the stdout also cut suite wall-clock ~290s to ~221s at 32-way, consistent with the interleaved stream being the bottleneck as well as the corruption site.
Minimal repro shape: N test files run at concurrency > cores, where at least one writes a few dozen lines to process.stdout during its tests. I can produce a standalone repro repo if that would help.
Independent reproduction — and a measured data point for the --test-concurrency=1 question
This was closed by the stale bot on 2026-07-26 with no fix commit, five days after @JustinCreeden's root-cause analysis. Requesting reopen — the diagnosis above looks correct and the one-line fix is still unlanded.
Reproducing independently on a different OS and a different codebase, with the signed-read hypothesis holding exactly.
Environment
macOS (Darwin 25.6.0),
Node v24.14.1 (active LTS) and v25.9.0
Suite: ~1,501 tests across 134 files, node --test, no external framework
Rate at default concurrency: 10 failures in 55 full-suite runs (~18%).
Node
IPC failures
runs
25.9.0
1
10
24.14.1 (LTS)
7
30
24.14.1 (later tree state)
2
15
Worth stating plainly for triage: this is not confined to current/odd Node lines. Active LTS reproduces it at least as often, so anyone hoping an LTS pin sidesteps it should not.
The --test-concurrency=1 observation, measured
The thread notes the failure was seen at --test-concurrency=1 but "did not measure those two conditions to the same standard." Measured here, same tree, same Node, 15 runs per arm:
arm
IPC failures
rate
avg wall-clock
default concurrency
2/15
13%
25.5s
--test-concurrency=1
2/15
13%
67.1s
--test-concurrency=4
0/15
0%
56.3s
Serial execution flakes at an unchanged rate. That supports the reading that concurrency is a correlate affecting chunk coalescing rather than a necessary condition — the desync arises from non-protocol bytes reaching the parser at all.
(I would not read the =4 column as a fix: at a 13% base rate, P(0 in 15) ≈ 12%, so a clean sweep of 15 is ordinary chance.)
It is a reporting failure, not an execution failure
Corroborating corollary 1 above: failing runs lose tests. Clean runs report tests 1501; the failing ones reported 1495, 1497, and 1500. An entire file's results never arrive. The named victim file is repeatable within an arm but differs across arms — adopt-new-bin.test.js twice at default concurrency, row-uid-delete.test.js twice at =1 — consistent with a payload shape rather than anything wrong with those tests.
This matters for anyone triaging it: the test named in the failure is not the problem, and a run can come back short rather than loudly broken.
The offset-2 byte predicts it
Checking our own captured output against the signed-read hypothesis: 1,119 distinct line-prefixes carry a byte ≥ 0x80 at offset 2, e.g.
Our CLI logs status lines prefixed with ✓/ℹ/⚠, and several test files exercise CLI modules in-process, so those console.* calls land in the test child's own stdout — precisely the shape described above.
We are applying the ASCII-guard workaround now and will report the before/after rate against the 18% baseline once measured. Flagging that we have not yet confirmed the workaround on our side, so treat the correlation above as supporting evidence rather than a second confirmation of the fix.
As noted upstream, the failure is silent for anyone whose logs are pure ASCII, so this is likely under-reported relative to how often the underlying desync occurs. Emoji in test/CLI output is common enough that the affected population is probably large and mostly unaware — the symptom presents as "our suite is flaky," which people work around locally rather than report.
Activity
mykola-mokhnach commentedon Jun 22, 2026
Full CI log: https://gist.github.com/mykola-mokhnach/24786a0e9703105d356b3b5bb8756624
mykola-mokhnach commentedon Jun 25, 2026
After further investigation this errors seems to pop up if
process.execPathis invoked in the suite setup section vianode:child_process->spawn. It is important though that it is invoked by another imported module, not the same that runs the test.I've found a viable workaround for that just by running
process.execPathwith--versionargument as the very first command in thebeforesection.fix(tests): stop the flaky unit suite by running it in one process
AriaPrime commentedon Jul 18, 2026
We hit this same error on
v24.15.0(Windows) and traced it to what looks like a root cause: the frame-length read in#processRawBufferis signed, so a desync that should be recoverable becomes fatal.The arithmetic
lib/internal/test_runner/runner.js(still present onmainas of today):The comment says
readUInt32BE, but<<and|are signed 32-bit in JS. If the byte atkV8HeaderLengthis>= 0x80,fullMessageSizeis negative, sothis.#rawBufferSize < fullMessageSizeisfalse, the loop does not break to wait for more data, and it proceeds to deserialize whatever is in the buffer — producingUnable to deserialize cloned data due to invalid or unsupported version.When the same bytes are read unsigned, the value is a huge positive, the guard breaks, and the parser waits for more data instead of crashing.
Minimal demonstration
Why it looks intermittent and environment-specific
The children write the v8-serialized protocol and ordinary
consoleoutput to the same fd.#processRawBufferstrips non-serialized bytes once per call, after which the loop treats the next position as a frame boundary. When a chunk boundary lands such that console bytes sit where a length is expected, those bytes get read as the length.That makes the trigger a function of chunk coalescing, which is why it is load- and timing-dependent — and why anything that changes output volume or scheduling (such as the
child_process.spawnin the setup that @mykola-mokhnach identified) changes how often it fires. We suspect that is a correlate rather than the cause: the cause is that non-protocol bytes reach the parser at all, and the signed read is what makes the resulting desync unrecoverable.The content of the console output decides whether you ever notice. ASCII output yields a huge positive length, the guard breaks, and the stream self-heals silently. Output whose byte at offset 2 is
>= 0x80yields a negative length and crashes. Our application logs are prefixed" <emoji> …", which parks0xE2/0xF0at exactly offset 2 — so our suite tripped it constantly while suites with ASCII logs never do.In one suite (~440 tests) on
v24.15.0, Windows, our most controlled measurement was a sequential before/after rate check: 6 of 10 full runs failed with the emoji-prefixed logs, and 0 of 10 after guarding our own output (independently re-checked: another 5 clean runs). Less rigorously, during investigation we also observed the failure while running with--test-concurrency=1, and never observed it when running the affected files individually — so this does not appear to be purely a cross-child interaction, though we did not measure those two conditions to the same standard.During that investigation we also captured the children's stdout from inside the child processes; the captured streams were well-formed, and the children were killed mid-stream after the parent errored — consistent with the corruption arising in the parent's parsing rather than in what the children emit.
Suggested fix
or keep the manual math and make it unsigned with
>>> 0. That alone converts these crashes back into the "wait for more data" path.Worth noting the failure mode is silent for anyone whose logs happen to be ASCII, so this may be under-reported relative to how often the underlying desync occurs.
As a workaround until it lands, we guard our own side in the test harness by ensuring no console line has a byte
>= 0x80at offset 2 — 15 consecutive clean runs since, against a ~60% failure rate before.JustinCreeden commentedon Jul 21, 2026
Isolated trigger: a test file writing to stdout while the runner is under concurrent load
Reproduced deterministically on Windows, Node v24.14.0, 12-core machine. I believe this isolates the condition rather than just the symptom.
Mechanism.
node:testruns each test file in a child process and forwards that child's raw stdout to the parent over the same pipe that carries its binary v8-serialized result frames (the transport introduced in da27542, replacing TAP text). When a test file writes unstructured text to stdout while several files run concurrently, the parent's reassembly of that interleaved stream fails at a chunk boundary:The runner then fails the whole file, with no relationship to anything that file asserts.
Controlled experiment. One file in a 101-file suite emitted 48 lines of raw stdout (it exercised its module in-process, so the module's
console.*calls landed in the test process's own stdout). All runsnode --test --test-concurrency=32:console.*stubbedThe third row is the load-bearing one: keeping the file and removing only its stdout is as clean as removing the file entirely. So the variable is the raw text on the pipe — not the file's presence, its test count, or its assertions.
Dose-response. At the default concurrency (= core count, 12 here) the same suite passed 2/2. At 32-way it failed 6/9. Lowering concurrency suppresses it without removing it.
Two corollaries that may help triage:
Minimal repro shape: N test files run at concurrency > cores, where at least one writes a few dozen lines to
process.stdoutduring its tests. I can produce a standalone repro repo if that would help.Work around node:test IPC bug in CI on Node 22.x
fix(ci): economyEngine.test.js worker-IPC-flake — deterministisk isol…
fix(ci): economyEngine.test.js IPC flake bit again after #3222 (isola…
fix(cli): suppress inspectProject stdout in integration test to avoid…
ci: dodge the Node 22 runner-IPC bug with a filewise lane; fix a live…
fix(tests): replace mojibake test name that broke node:test IPC
copperdesign commentedon Aug 11, 2026
Independent reproduction — and a measured data point for the
--test-concurrency=1questionThis was closed by the stale bot on 2026-07-26 with no fix commit, five days after @JustinCreeden's root-cause analysis. Requesting reopen — the diagnosis above looks correct and the one-line fix is still unlanded.
Reproducing independently on a different OS and a different codebase, with the signed-read hypothesis holding exactly.
Environment
node --test, no external frameworkRate at default concurrency: 10 failures in 55 full-suite runs (~18%).
Worth stating plainly for triage: this is not confined to current/odd Node lines. Active LTS reproduces it at least as often, so anyone hoping an LTS pin sidesteps it should not.
The
--test-concurrency=1observation, measuredThe thread notes the failure was seen at
--test-concurrency=1but "did not measure those two conditions to the same standard." Measured here, same tree, same Node, 15 runs per arm:--test-concurrency=1--test-concurrency=4Serial execution flakes at an unchanged rate. That supports the reading that concurrency is a correlate affecting chunk coalescing rather than a necessary condition — the desync arises from non-protocol bytes reaching the parser at all.
(I would not read the
=4column as a fix: at a 13% base rate, P(0 in 15) ≈ 12%, so a clean sweep of 15 is ordinary chance.)It is a reporting failure, not an execution failure
Corroborating corollary 1 above: failing runs lose tests. Clean runs report
tests 1501; the failing ones reported 1495, 1497, and 1500. An entire file's results never arrive. The named victim file is repeatable within an arm but differs across arms —adopt-new-bin.test.jstwice at default concurrency,row-uid-delete.test.jstwice at=1— consistent with a payload shape rather than anything wrong with those tests.This matters for anyone triaging it: the test named in the failure is not the problem, and a run can come back short rather than loudly broken.
The offset-2 byte predicts it
Checking our own captured output against the signed-read hypothesis: 1,119 distinct line-prefixes carry a byte ≥
0x80at offset 2, e.g.Our CLI logs status lines prefixed with
✓/ℹ/⚠, and several test files exercise CLI modules in-process, so thoseconsole.*calls land in the test child's own stdout — precisely the shape described above.We are applying the ASCII-guard workaround now and will report the before/after rate against the 18% baseline once measured. Flagging that we have not yet confirmed the workaround on our side, so treat the correlation above as supporting evidence rather than a second confirmation of the fix.
Ask
Reopen and land the unsigned read:
As noted upstream, the failure is silent for anyone whose logs are pure ASCII, so this is likely under-reported relative to how often the underlying desync occurs. Emoji in test/CLI output is common enough that the affected population is probably large and mostly unaware — the symptom presents as "our suite is flaky," which people work around locally rather than report.
avivkeller commentedon Aug 11, 2026
@copperdesign please stop spreading misleading and contradictory information with AI. If you'd like to comment, do so as a human.
1 remaining item
test: mock console.log in data-quality main() tests to prevent runner…
docs: record data-quality.test.js flake fix (runner IPC corruption, n…
feat!: 최소 Node 버전을 24로 올리고 CI matrix를 [24]로 이동
fix(daemon-tests): cli-rules kapselt stdout — rohe ✓-Zeilen crashten …
fix(tests): the test shim keeps raw stdout off the runner's frame pipe (
Say where the Node-version claim came from, and when
ci(test): raise the test-toolchain Node floor to 26.7.0 (#38) (#174)
fix(#4306): extend fault-injection fd-swallow fix across the whole suite
fix(#4306): extend fault-injection fd-swallow fix across the whole su…
fix(test): 堵住把 CI 弄红的 node:test 协议通道污染(integration-widget 整文件失败的真因)
fix(test): keep console output off the node:test frame transport
Document the node --test IPC flake and add a workaround script
fix(test): capacitor-splash testinde node:test IPC çerçeveleme çakışm…
test: fix two flaky backend tests
test: route test logs to stderr to stop node:test IPC crashes