Skip to content

Node 20.20.2 node:test crashes on non-ASCII startup logs, not the named test #158

Closed

Description

@tryingET

Summary

On Node 20.20.2, npm run test:node can fail a file at :1:1 with Unable to deserialize cloned data due to invalid or unsupported version. The stack is Node's test-runner parser. The named file is the worker whose stdout pipe carried the bytes, not a failed assertion. The trigger is already on main: boot logs write non-ASCII to stdout, and those bytes can land in the same read as a report frame. A green rerun is not a repair.

On current main the node:test step is report-only (continue-on-error: true at .github/workflows/test.yml:244). Run 35908127184 failed because that PR's workflow made the suite a Node 20 gate without continue-on-error. Removing the report-only line on main would turn this into a red check.

Reproduction

This is the file that failed 2 of 8 runs on Node 20.20.2. Node 26.9.0 passed 4 of 4 runs of the same file. One isolated write of the banner, with no surrounding tests, passed on both.

const test = require('node:test');
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));

test('ordinary assertion before console banner', async () => {
  await tick();
});

test('console.log writes the AGNT banner between runner frames', async () => {
  for (let i = 0; i < 40; i++) {
    console.log(`📁 AGNT root: /tmp/agnt-example ${i}`);
    await tick();
  }
});

test('ordinary assertion after console banner', async () => {
  await tick();
});

Run node --test console-banner.test.js. A failing run looks like this:

not ok 1 - console-banner.test.js
  location: '.../console-banner.test.js:1:1'
  failureType: 'uncaughtException'
  error: 'Unable to deserialize cloned data due to invalid or unsupported version.'
  stack: |-
    #proccessRawBuffer (node:internal/test_runner/runner:358:20)
    FileTest.parseMessage (node:internal/test_runner/runner:294:28)
    Socket.<anonymous> (node:internal/test_runner/runner:393:15)

Those three stack lines match run 35908127184, which blamed tests/unit/asyncToolsToggle.test.js:1:1.

A separate write crashes on the first run on both Node 20.20.2 and Node 26.9.0: fs.writeSync(1, Buffer.from([0xff, 0x0f, 0x00, 0x00, 0x00, 0x08, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48])). I am not claiming the hosted job wrote those exact bytes.

Root cause

node --test reads each worker's stdout as V8 frames. A frame starts with FF 0F and a 4-byte size. Bytes before that header are printed as text. On Node 20.20.2, #proccessRawBuffer then reads the four bytes after FF 0F as a signed length, with no >>> 0, and readHeader() throws when that length is negative. A UTF-8 lead byte is >= 0x80. The banner does not itself contain FF 0F. It crashes when one of those lead bytes is spliced into that length field in the same stdout read as a report frame. That is why one isolated write passed and the interleaved run failed 2 of 8.

Node 26.9.0 has the unsigned read, so this banner case passed there. It still crashes on the explicit FF 0F write. That remaining case is nodejs/node#66164 and nodejs/node#66273. Related: nodejs/node#64061 and nodejs/node#65934.

On main at 1b103e62, tests/unit/asyncToolsToggle.test.js does not import the database directly. It imports backend/src/services/orchestrator/tools.js (line 22), which imports AuthManager.js (line 24), which imports backend/src/models/database/index.js (line 1). That import boots PathManager. The boot logs are:

  • backend/src/utils/PathManager.js:74 when root and data are the same directory, and :76 plus :77 when they differ. The hosted log printed the :76 line, 📁 AGNT root:.
  • backend/src/models/database/index.js:634 is one ✓ Added ${col.name} column... log. The hosted flood also matches the other checkmark logs in that file (39 calls). A stderr change has to cover that whole set, not only line 634.

Suggested fix

Do not treat the named test as the failing assertion. Do not treat a green rerun, a serialized run, or filtering this error text as a repair.

  1. In this repo, send the boot logs above to stderr while node --test is running. The runner reads stderr as text and does not deserialize it. Leave assertion failures and nonzero exits alone. A test that still writes FF 0F to stdout will keep crashing. That is the Node bug, and hiding it would be wrong.
  2. The parser repair is the open Node change above. Do not drop the Node 20 job, and do not treat a Node 26 pass as proof that 20.20.2 is fixed.

Environment

  • agnt main at 1b103e62
  • Node 20.20.2, the version actions/setup-node acquired for node-version: 20 in run 35908127184
  • Node 26.9.0 as the control
  • Linux x64

Activity

  1. agnt-gg commented on Oct 2, 2026

    @agnt-gg
    Owner

    Fixed by #159, which is now in our 0.6.7 release branch: boot logs go to stderr during node:test runs, so the non-ASCII bytes can no longer land in the worker's report frames. Thanks @tryingET for the precise diagnosis. Closing.

    — Annie

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions