From 8e9fbc92113c2fe1456c6a6e03db244392a90776 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 10 Sep 2026 03:21:31 -0500 Subject: [PATCH] test: keep browser and stream fixtures compatible with Node20 Provide navigator.language in the Node-hosted browser bundle and Worker fixtures. Consume async stream results with for-await so Node20 executes the existing lifecycle, schema failure, and cleanup assertions. ### Tests Updated | Test group | Verifies | | --- | --- | | Real safe-bash browser kernel, execution, filesystem, and session cohorts | Actual browser engine and Worker behavior with explicit browser globals. | | defineStreamCommand SDK lifecycle | Lazy consumption, status separation, schema rejection, and exactly-once cleanup without Array.fromAsync. | Validation: Node20.20.0 passed all four browser files (134 tests) and the stream lifecycle file (5 tests). No production runtime or deadline changes. --- .../safe-bash-playground/src/engine/kernel.test.ts | 2 ++ .../src/execution-filesystem.test.ts | 5 ++++- packages/safe-bash-playground/src/execution.test.ts | 2 ++ packages/safe-bash-playground/src/session.test.ts | 2 ++ packages/safe-bash-playground/test/browser-worker.ts | 1 + packages/toolcraft/src/stream.test.ts | 12 +++++++++--- 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/safe-bash-playground/src/engine/kernel.test.ts b/packages/safe-bash-playground/src/engine/kernel.test.ts index 1f4599782e..8e4d648188 100644 --- a/packages/safe-bash-playground/src/engine/kernel.test.ts +++ b/packages/safe-bash-playground/src/engine/kernel.test.ts @@ -13,6 +13,7 @@ describe("real safe-bash browser kernel", () => { const activeWorkers = new Set<{ terminate(): void }>(); beforeAll(async () => { + vi.stubGlobal("navigator", { language: "en-US" }); vi.stubGlobal( "Worker", class extends EventTarget { @@ -28,6 +29,7 @@ describe("real safe-bash browser kernel", () => { const worker = new NodeWorker( ` const { parentPort } = require('node:worker_threads'); + Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { language: 'en-US' } }); globalThis.addEventListener = (event, handler) => parentPort.on(event, data => handler({ data })); globalThis.postMessage = (value, transfer) => parentPort.postMessage(value, transfer); ${code} diff --git a/packages/safe-bash-playground/src/execution-filesystem.test.ts b/packages/safe-bash-playground/src/execution-filesystem.test.ts index 2015b8e722..c988e3dbd7 100644 --- a/packages/safe-bash-playground/src/execution-filesystem.test.ts +++ b/packages/safe-bash-playground/src/execution-filesystem.test.ts @@ -1,8 +1,11 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; import { createMemoryFileSystem, FsError } from "./engine/index.js"; import type { FileSystem } from "./engine/index.js"; import { decodeError, encodeError, hostFileSystem, remoteFileSystem } from "./execution-filesystem.js"; +vi.hoisted(() => vi.stubGlobal("navigator", { language: "en-US" })); +afterAll(() => vi.unstubAllGlobals()); + vi.mock("./engine/index.js", async () => { const { buildBrowserEngine } = await import("./engine/build-plugin.mjs"); const built = await buildBrowserEngine(); diff --git a/packages/safe-bash-playground/src/execution.test.ts b/packages/safe-bash-playground/src/execution.test.ts index ee52c40f2a..0ecf2a3e7d 100644 --- a/packages/safe-bash-playground/src/execution.test.ts +++ b/packages/safe-bash-playground/src/execution.test.ts @@ -7,6 +7,8 @@ import type { ExecutionMessage } from "./execution-protocol.js"; import { browserWorkerFixture } from "../test/browser-worker.js"; import { setTimeout as delay } from "node:timers/promises"; +vi.hoisted(() => vi.stubGlobal("navigator", { language: "en-US" })); + vi.mock("virtual:safe-bash-worker-sources", async () => { const { buildBrowserEngine } = await import("./engine/build-plugin.mjs"); return { sources: (await buildBrowserEngine({ workersOnly: true })).workerSources }; diff --git a/packages/safe-bash-playground/src/session.test.ts b/packages/safe-bash-playground/src/session.test.ts index 4fd62e3d19..0e0ff593f3 100644 --- a/packages/safe-bash-playground/src/session.test.ts +++ b/packages/safe-bash-playground/src/session.test.ts @@ -3,6 +3,8 @@ import { createSession, SESSION_LIMITS } from "./session.js"; import { sampleFiles } from "./samples.js"; import { browserWorkerFixture } from "../test/browser-worker.js"; +vi.hoisted(() => vi.stubGlobal("navigator", { language: "en-US" })); + vi.mock("virtual:safe-bash-worker-sources", async () => { const { buildBrowserEngine } = await import("./engine/build-plugin.mjs"); return { sources: (await buildBrowserEngine({ workersOnly: true })).workerSources }; diff --git a/packages/safe-bash-playground/test/browser-worker.ts b/packages/safe-bash-playground/test/browser-worker.ts index 0bf50ce003..cd415285e5 100644 --- a/packages/safe-bash-playground/test/browser-worker.ts +++ b/packages/safe-bash-playground/test/browser-worker.ts @@ -18,6 +18,7 @@ export function browserWorkerFixture(executionSource: string) { this.worker = source.then((code) => { const worker = new NodeWorker(` const { parentPort } = require("node:worker_threads"); + Object.defineProperty(globalThis, "navigator", { configurable: true, value: { language: "en-US" } }); globalThis.addEventListener = (event, handler) => parentPort.on(event, data => handler({ data })); globalThis.postMessage = value => parentPort.postMessage(value); (() => { ${code} })(); diff --git a/packages/toolcraft/src/stream.test.ts b/packages/toolcraft/src/stream.test.ts index 2a5f5b0c6d..691c458ac2 100644 --- a/packages/toolcraft/src/stream.test.ts +++ b/packages/toolcraft/src/stream.test.ts @@ -7,6 +7,12 @@ import { type ToolcraftStream } from "./index.js"; +async function collect(stream: AsyncIterable): Promise { + const values: Value[] = []; + for await (const value of stream) values.push(value); + return values; +} + describe("defineStreamCommand SDK lifecycle", () => { it("starts lazily and advances only when the consumer pulls", async () => { const produced: number[] = []; @@ -78,7 +84,7 @@ describe("defineStreamCommand SDK lifecycle", () => { const stream = sdk.watch({}, { onStatus: (event) => statuses.push(event) }); - await expect(Array.fromAsync(stream)).resolves.toEqual([{ state: "fresh-token" }]); + await expect(collect(stream)).resolves.toEqual([{ state: "fresh-token" }]); expect(statuses).toEqual([ { type: "reconnecting", message: "Refreshing credentials" } ]); @@ -95,7 +101,7 @@ describe("defineStreamCommand SDK lifecycle", () => { }); const sdk = createSDK(defineGroup({ name: "devices", children: [watch] })); - await expect(Array.fromAsync(sdk.watch({}))).rejects.toThrow("state"); + await expect(collect(sdk.watch({}))).rejects.toThrow("state"); }); it("propagates terminal errors and releases resources once", async () => { @@ -115,7 +121,7 @@ describe("defineStreamCommand SDK lifecycle", () => { }); const sdk = createSDK(defineGroup({ name: "devices", children: [watch] })); - await expect(Array.fromAsync(sdk.watch({}))).rejects.toThrow("connection lost"); + await expect(collect(sdk.watch({}))).rejects.toThrow("connection lost"); expect(cleanup).toHaveBeenCalledOnce(); }); });