diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 91c891d44..621384eba 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Normalized `ptree.ChildProcess.bytes()` to always return a `Uint8Array`. Bun's `Response(stream).bytes()` returns the raw `ArrayBuffer` once the body arrives in more than one chunk (subprocess stdout past ~128 KB), which broke callers relying on `Uint8Array` methods like `.indexOf` and `.subarray` — including the `ssh://` read path's `decodeUtf8Text`, which crashed with `indexOf is not a function` on larger remote text files. ([#3712](https://github.com/can1357/oh-my-pi/issues/3712)) + ## [16.2.0] - 2026-06-27 ### Added diff --git a/packages/utils/src/ptree.ts b/packages/utils/src/ptree.ts index 67154f183..528f5dc30 100644 --- a/packages/utils/src/ptree.ts +++ b/packages/utils/src/ptree.ts @@ -247,7 +247,12 @@ export class ChildProcess { } async bytes(): Promise { - return new Response(this.stdout).bytes(); + // Bun's `Response(stream).bytes()` returns the raw `ArrayBuffer` once the + // stream emits more than one chunk (subprocess stdout chunks past ~128 KB). + // Normalize at the contract boundary so every caller — SSH read, + // `decodeUtf8Text`, callers slicing with `.subarray` — sees a `Uint8Array`. + const body = (await new Response(this.stdout).bytes()) as Uint8Array | ArrayBuffer; + return body instanceof Uint8Array ? body : new Uint8Array(body); } // ── Wait ───────────────────────────────────────────────────────────── diff --git a/packages/utils/test/ptree-bytes.test.ts b/packages/utils/test/ptree-bytes.test.ts new file mode 100644 index 000000000..693c35eee --- /dev/null +++ b/packages/utils/test/ptree-bytes.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "bun:test"; +import { spawn } from "@oh-my-pi/pi-utils/ptree"; + +describe("ptree.ChildProcess.bytes()", () => { + // Regression for https://github.com/can1357/oh-my-pi/issues/3712: + // `Response(stream).bytes()` returns the raw `ArrayBuffer` once the body + // arrives in more than one chunk (which happens for subprocess stdout past + // ~128 KB). Downstream code — e.g. the SSH read path's `decodeUtf8Text` — + // relied on `Uint8Array` methods (`.indexOf`, `.subarray`) and crashed. + it("returns a Uint8Array regardless of stdout size", async () => { + // 256 KB is comfortably past the multi-chunk boundary observed on Bun + // 1.3.x; the test then asserts only on the contract, not on the exact + // chunk threshold, so it stays robust to future Bun runtime changes. + const size = 256 * 1024; + const child = spawn(["bun", "-e", `process.stdout.write("a".repeat(${size}))`]); + const bytes = await child.bytes(); + await child.exitedCleanly; + + expect(bytes).toBeInstanceOf(Uint8Array); + expect(bytes.length).toBe(size); + // The two methods the SSH read path depends on. + expect(typeof bytes.indexOf).toBe("function"); + expect(typeof bytes.subarray).toBe("function"); + expect(bytes.indexOf(0)).toBe(-1); + expect(bytes.subarray(0, 4)).toEqual(new Uint8Array([0x61, 0x61, 0x61, 0x61])); + }); +});