From f4ecf9d11aa8c94d1105158167042362c23a9539 Mon Sep 17 00:00:00 2001 From: madjack <148759141+m4djack@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:08:11 +0100 Subject: [PATCH] fix: use Array.from instead of Buffer.from for native processStreamChunk state (#2348) The napi StreamState fields (utf8Pending, ansiPending) expect plain arrays (Vec), not Buffers. Passing Buffer.from() caused 'Given napi value is not an array on StreamState.utf8Pending' crash on multi-chunk bash output. Added regression test for multi-chunk state passing. AI-assisted: This change was authored with Claude (AI pair programming). --- .../src/__tests__/stream-process.test.mjs | 34 +++++++++++++++++++ packages/native/src/stream-process/index.ts | 4 +-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 packages/native/src/__tests__/stream-process.test.mjs diff --git a/packages/native/src/__tests__/stream-process.test.mjs b/packages/native/src/__tests__/stream-process.test.mjs new file mode 100644 index 000000000..224f0bffa --- /dev/null +++ b/packages/native/src/__tests__/stream-process.test.mjs @@ -0,0 +1,34 @@ +import { test, describe } from "node:test"; +import assert from "node:assert/strict"; +import { processStreamChunk } from "../stream-process/index.ts"; + +describe("processStreamChunk", () => { + test("processes a single chunk without state", () => { + const result = processStreamChunk(Buffer.from("hello world\n")); + assert.equal(result.text, "hello world\n"); + assert.ok(Array.isArray(result.state.utf8Pending)); + assert.ok(Array.isArray(result.state.ansiPending)); + }); + + test("processes multiple chunks passing state between calls", () => { + const result1 = processStreamChunk(Buffer.from("first\n")); + assert.equal(result1.text, "first\n"); + + // This was the crash: passing state back caused + // "Given napi value is not an array on StreamState.utf8Pending" + // when state arrays were wrapped in Buffer.from() instead of Array.from() + const result2 = processStreamChunk(Buffer.from("second\n"), result1.state); + assert.equal(result2.text, "second\n"); + + const result3 = processStreamChunk(Buffer.from("third\n"), result2.state); + assert.equal(result3.text, "third\n"); + }); + + test("state fields are plain arrays, not Buffers", () => { + const result = processStreamChunk(Buffer.from("test\n")); + assert.ok(Array.isArray(result.state.utf8Pending), "utf8Pending should be a plain array"); + assert.ok(Array.isArray(result.state.ansiPending), "ansiPending should be a plain array"); + assert.ok(!(result.state.utf8Pending instanceof Buffer), "utf8Pending should not be a Buffer"); + assert.ok(!(result.state.ansiPending instanceof Buffer), "ansiPending should not be a Buffer"); + }); +}); diff --git a/packages/native/src/stream-process/index.ts b/packages/native/src/stream-process/index.ts index 5fa3c2ab9..4a622b144 100644 --- a/packages/native/src/stream-process/index.ts +++ b/packages/native/src/stream-process/index.ts @@ -33,8 +33,8 @@ export function processStreamChunk( // Convert StreamState arrays to the format napi expects (Vec) const napiState = state ? { - utf8Pending: Buffer.from(state.utf8Pending), - ansiPending: Buffer.from(state.ansiPending), + utf8Pending: Array.from(state.utf8Pending), + ansiPending: Array.from(state.ansiPending), } : undefined;