fix(test): keep subprocess fixtures consistent across Node and Bun (#163805)

Couple test-worker arguments to the executable selected by the shared subprocess helper and migrate its runtime-dependent callers. Repair command parsing, runtime pinning, worker observation, storage settlement, and cleanup-clock fixtures while preserving their contracts.

Fourteen scoped suites pass on Node 24 and Bun; the Bun readSync gap remains an unchanged stop. Refreshed proof covers 638 passed/1 existing skip on Node and 636 passed/3 existing skips on Bun. P2 review and exact-head ClawSweeper found no actionable defects. Production LOC delta is zero.

The first CI head exposed an inherited UI-validator fixture omission. After incorporating its existing main fix, exact-head CI passed, including the published-driver update job. No same-head retry or admin exception was used.
This commit is contained in:
Peter Steinberger 2026-10-02 15:46:52 -07:00 • committed by GitHub
parent db30aa2108
commit ee51203de7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 209 additions and 160 deletions

View file

@ -3,10 +3,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { runNodeScript } from "../../../test/helpers/run-node-script.js";
import {
resolveRuntimeWorkerArgv,
resolveRuntimeWorkerUrl,
} from "../../infra/runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "../../infra/runtime-worker-url.js";
import { prepareSecretsRuntimeFastPathSnapshot } from "../../secrets/runtime-fast-path.js";
import { activateSecretsRuntimeSnapshotState } from "../../secrets/runtime-state.js";
import { openOpenClawStateDatabase } from "../../state/openclaw-state-db.js";
@ -661,8 +658,8 @@ describe("auth publication owner receipts", () => {
const laterCwd = path.join(tempDirs.make("openclaw-auth-scope-later-cwd-"), "nested");
fs.mkdirSync(laterCwd);
const result = await runNodeScript(
[
...resolveRuntimeWorkerArgv(resolveRuntimeWorkerUrl(authProfileScopeCwdEntrypoint)),
(workerArgv) => [
...workerArgv(resolveRuntimeWorkerUrl(authProfileScopeCwdEntrypoint)),
stateDir,
agentDir,
laterCwd,

View file

@ -1,8 +1,7 @@
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import {
codeModeDescriptionRetentionEntrypoint,
codeModeRetentionEntrypoint,
@ -21,10 +20,7 @@ it.for([
},
])("$name", { timeout: 30_000 }, async ({ entrypoint, expected }, { signal }) => {
const result = await runNodeScript(
[
"--expose-gc",
...resolveRuntimeWorkerArgv(resolveRuntimeWorkerUrl(entrypoint), resolveTestNodeExecPath()),
],
(workerArgv) => ["--expose-gc", ...workerArgv(resolveRuntimeWorkerUrl(entrypoint))],
{ ...process.env, NODE_OPTIONS: "", TSX_DISABLE_CACHE: "1" },
15_000,
{

View file

@ -3,8 +3,7 @@ import { describe, expect, it, vi } from "vitest";
import * as fences from "../../packages/markdown-core/src/fences.js";
import { markdownToIR } from "../../packages/markdown-core/src/ir.js";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { EmbeddedBlockChunker } from "./embedded-agent-block-chunker.js";
import { agentProcessTestEntrypoints } from "./process-runtime.test-support.js";
@ -223,8 +222,8 @@ describe("EmbeddedBlockChunker", () => {
// A synchronous stalled drain needs an external deadline, not Vitest's in-process timer.
const chunkerUrl = resolveRuntimeWorkerUrl(agentProcessTestEntrypoints.blockChunker);
const result = await runNodeScript(
[
...resolveRuntimeWorkerArgv(chunkerUrl, resolveTestNodeExecPath()).slice(0, -1),
(workerArgv) => [
...workerArgv(chunkerUrl).slice(0, -1),
"--input-type=module",
"--eval",
`

View file

@ -55,10 +55,20 @@ it("prepares known provider owners once while keeping catalog execution and auth
`provider: ${JSON.stringify(provider)}, filename: __filename`,
);
const execution = (provider: string) =>
`require("node:v8").queryObjects(WeakRef);` +
`require("node:v8").queryObjects(WeakRef);
const retainedPayloads = globalThis[Symbol.for("catalog.reuse.payloads")];
const activePayload = retainedPayloads.at(-1);
const currentPayload = activePayload.ref.deref();
` +
record(
"heap.jsonl",
`memory: process.memoryUsage(), payloads: globalThis[Symbol.for("catalog.reuse.payloads")].filter(ref => ref.deref()).length`,
`memory: process.memoryUsage(),
payloads: retainedPayloads.filter(({ ref }) => ref.deref()).length,
currentPayload: currentPayload && {
filename: activePayload.filename,
row: currentPayload.rows[99_999].index,
bufferBytes: currentPayload.buffer.byteLength,
}`,
) +
record(
"executions.jsonl",
@ -77,7 +87,7 @@ it("prepares known provider owners once while keeping catalog execution and auth
fs.writeFileSync(
baseEntry,
`const { payload } = require("./payload.mjs");
(globalThis[Symbol.for("catalog.reuse.payloads")] ??= []).push(new WeakRef(payload));
(globalThis[Symbol.for("catalog.reuse.payloads")] ??= []).push({ ref: new WeakRef(payload), filename: __filename });
` +
fs
.readFileSync(baseEntry, "utf8")
@ -242,7 +252,7 @@ module.exports = { id: ${JSON.stringify(provider)}, register(api) {
).toEqual(index === 0 ? [PROVIDER_ID, ...providerIds].toSorted() : []);
const expanded = registrations();
const footprint = readCatalogCaptureFootprint(captureRoot);
// Retired capture files disappear even though Node retains their native ESM payloads.
// Retired capture files disappear even if native ESM payloads remain cached.
expect(footprint.captures).toHaveLength(1);
await refreshScope((index + 1) % 2, provider);
expect(registrations()).toEqual(expanded);
@ -285,9 +295,28 @@ module.exports = { id: ${JSON.stringify(provider)}, register(api) {
.readFileSync(path.join(fixture.root, "heap.jsonl"), "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line) as { memory: NodeJS.MemoryUsage; payloads: number });
.map(
(line) =>
JSON.parse(line) as {
memory: NodeJS.MemoryUsage;
payloads: number;
currentPayload?: { filename: string; row: number; bufferBytes: number };
},
);
expect(heap).toHaveLength(providerIds.length * agentIds.length * 2);
expect(heap.map(({ payloads }) => payloads)).toEqual(heap.map(() => 2));
const currentFilename = warmedRegistrations.findLast(
({ provider }) => provider === PROVIDER_ID,
)!.filename;
for (const sample of heap) {
// Engines may collect the retired ESM payload; the current capture must remain usable.
expect(sample.payloads).toBeGreaterThanOrEqual(1);
expect(sample.payloads).toBeLessThanOrEqual(2);
expect(sample.currentPayload).toEqual({
filename: currentFilename,
row: 99_999,
bufferBytes: 1024 * 1024,
});
}
expect(heap.at(-1)!.memory.heapUsed - heap[0]!.memory.heapUsed).toBeLessThan(16 * 1024 * 1024);
expect(heap.at(-1)!.memory.arrayBuffers - heap[0]!.memory.arrayBuffers).toBeLessThan(1024 * 1024);
console.log("Provider-scope heap", JSON.stringify({ first: heap[0], last: heap.at(-1) }));

View file

@ -193,8 +193,8 @@ describe("FileSettingsStorage", () => {
const root = fixtures.createTempDir("openclaw-settings-umask-");
const agentDir = join(root, "agent");
const result = await runNodeScript(
[
...resolveRuntimeWorkerArgv(storageUrl, resolveTestNodeExecPath()).slice(0, -1),
(workerArgv) => [
...workerArgv(storageUrl).slice(0, -1),
"--input-type=module",
"--eval",
String.raw`

View file

@ -1,11 +1,7 @@
import { fileURLToPath } from "node:url";
import { beforeAll, expect, it } from "vitest";
import { runNodeScript } from "../../../../test/helpers/run-node-script.js";
import {
resolveRuntimeWorkerArgv,
resolveRuntimeWorkerUrl,
} from "../../../infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../../../test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../../../infra/runtime-worker-url.js";
import { agentProcessTestEntrypoints } from "../../process-runtime.test-support.js";
type Observation = {
@ -19,12 +15,9 @@ let observations: Observation[] = [];
beforeAll(async () => {
const result = await runNodeScript(
[
(workerArgv) => [
"--expose-gc",
...resolveRuntimeWorkerArgv(
resolveRuntimeWorkerUrl(agentProcessTestEntrypoints.readRetention),
resolveTestNodeExecPath(),
),
...workerArgv(resolveRuntimeWorkerUrl(agentProcessTestEntrypoints.readRetention)),
"all",
],
{ ...process.env, NODE_OPTIONS: "", TSX_DISABLE_CACHE: "1" },

View file

@ -8,17 +8,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createDeferred } from "../../../test/helpers/promise.js";
import { runNodeScript } from "../../../test/helpers/run-node-script.js";
import * as backoff from "../../infra/backoff.js";
import {
resolveRuntimeWorkerArgv,
resolveRuntimeWorkerUrl,
} from "../../infra/runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "../../infra/runtime-worker-url.js";
import { createWarnLogCapture } from "../../logging/test-helpers/warn-log-capture.js";
import * as pidAlive from "../../shared/pid-alive.js";
import {
closeOpenClawStateDatabaseAsync,
closeOpenClawStateDatabaseForTest,
} from "../../state/openclaw-state-db.js";
import { resolveTestNodeExecPath } from "../../test-utils/node-process.js";
import * as worktreeCapacity from "./capacity.js";
import * as worktreeGit from "./git.js";
import { requireGit } from "./git.js";
@ -338,11 +334,8 @@ describe("ManagedWorktreeService garbage collection", () => {
// Gateway cleanup runs on Node's main thread, whose stack limit differs from Vitest workers.
const collected = await runNodeScript(
[
...resolveRuntimeWorkerArgv(
resolveRuntimeWorkerUrl(managedWorktreeGcEntrypoint),
resolveTestNodeExecPath(),
),
(workerArgv) => [
...workerArgv(resolveRuntimeWorkerUrl(managedWorktreeGcEntrypoint)),
String(now),
],
env,

View file

@ -10,7 +10,7 @@ import { resolveVitestNodeArgs } from "../../scripts/lib/vitest-process-env.mts"
import { createFixtureLifetime } from "../../test/helpers/fixture-lifetime.js";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import {
cliMessageExitEntrypoints,
@ -486,9 +486,9 @@ await runCliWithExitFinalization({
const spawned: { child?: ChildProcess } = {};
const child = await lifetime.track(
runNodeScript(
[
(workerArgv) => [
...resolveVitestNodeArgs(),
...resolveRuntimeWorkerArgv(helpersUrl, nodeExecutable).slice(0, -1),
...workerArgv(helpersUrl).slice(0, -1),
entryPath,
],
{

View file

@ -140,13 +140,10 @@ export function createCandidateAdmissionFixtures(f: CandidateAdmissionFixture) {
);
const originalSpawn = expectDefined(spawn.getMockImplementation(), "default child transport");
spawn.mockImplementation((command: string, args: string[], options: SpawnOptions) => {
if (args[1] !== "update" || args[2] !== "admit") {
if (args.at(-4) !== "update" || args.at(-3) !== "admit" || args.at(-2) !== "--context") {
return originalSpawn(command, args, options);
}
const contextPath = expectDefined(
args[args.indexOf("--context") + 1],
"candidate admission context",
);
const contextPath = expectDefined(args.at(-1), "candidate admission context");
contexts.push(JSON.parse(fsSync.readFileSync(contextPath, "utf8")) as UpdateAdmissionContext);
events.push("admission");
databaseExistsAtAdmission.push(

View file

@ -3,6 +3,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import {
resolveDaemonInstallRuntimeInputs,
resolveDaemonServicePathDirs,
@ -15,7 +16,7 @@ describe("resolveDaemonInstallRuntimeInputs", () => {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "daemon-pin-")));
const pinned = path.join(root, "node");
try {
fs.symlinkSync(process.execPath, pinned);
fs.symlinkSync(resolveTestNodeExecPath(), pinned);
await expect(
resolveDaemonInstallRuntimeInputs({
env: {},

View file

@ -32,13 +32,22 @@ const postGrantPreload = `
const fixture = workerData.cronMutationProbe;
let gate;
let held = false;
parentPort.on("message", (request) => {
const observeRequest = (request) => {
if (request.type === "reclaim" &&
["entry", "lifecycle-projection-commit"].includes(request.plan.kind) &&
request.plan.descendantRunBasis?.sessionKeys.includes(fixture.sessionKey)) {
gate = request.commitGate;
}
});
};
const on = parentPort.on;
parentPort.on = function(event, listener) {
if (event === "message") {
// An eager preload listener consumes queued requests before the real worker imports finish.
parentPort.on = on;
on.call(this, event, observeRequest);
}
return on.call(this, event, listener);
};
const load = Atomics.load;
Atomics.load = function(view, index) {
const observed = load(view, index);

View file

@ -1,14 +1,14 @@
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { sessionChildCacheRetentionEntrypoint } from "./session-child-cache-retention-entrypoint.test-support.js";
it("keeps child links without retaining released session metadata", async ({ signal }) => {
const result = await runNodeScript(
[
(workerArgv) => [
"--expose-gc",
...resolveRuntimeWorkerArgv(resolveRuntimeWorkerUrl(sessionChildCacheRetentionEntrypoint)),
...workerArgv(resolveRuntimeWorkerUrl(sessionChildCacheRetentionEntrypoint)),
],
{ ...process.env, NODE_OPTIONS: "", TSX_DISABLE_CACHE: "1" },
15_000,

View file

@ -23,7 +23,7 @@ import {
withStateDatabaseColdAdmission,
} from "./gateway-state-owner.js";
import * as nodeSqlite from "./node-sqlite.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "./runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "./runtime-worker-url.js";
describe("Gateway state ownership", () => {
it.skipIf(process.platform === "win32").each(["state", "explicit"] as const)(
@ -492,8 +492,8 @@ describe("Gateway state ownership", () => {
);
const stateUrl = resolveRuntimeWorkerUrl(stateNativeProcessEntrypoints.stateDatabase);
const child = await runNodeScript(
[
...resolveRuntimeWorkerArgv(stateUrl).slice(0, -1),
(workerArgv) => [
...workerArgv(stateUrl).slice(0, -1),
"--input-type=module",
"--eval",
`import { runOpenClawStateWriteTransaction, closeOpenClawStateDatabaseForTest } from ${JSON.stringify(stateUrl.href)};

View file

@ -1,8 +1,7 @@
import { afterEach, expect, it } from "vitest";
import { createFixtureLifetime } from "../../test/helpers/fixture-lifetime.js";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "./runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "./runtime-worker-url.js";
import { sqliteReadOnlyCompileCacheParentEntrypoint } from "./sqlite-readonly-worker.compile-cache-runtime.test-support.js";
const fixture = createFixtureLifetime();
@ -22,11 +21,8 @@ it.for(["sync", "async", "scoped"] as const)(
delete env.NODE_OPTIONS;
const result = await fixture.track(
runNodeScript(
[
...resolveRuntimeWorkerArgv(
resolveRuntimeWorkerUrl(sqliteReadOnlyCompileCacheParentEntrypoint),
resolveTestNodeExecPath(),
),
(workerArgv) => [
...workerArgv(resolveRuntimeWorkerUrl(sqliteReadOnlyCompileCacheParentEntrypoint)),
root,
mode,
],

View file

@ -3,8 +3,7 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "./runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "./runtime-worker-url.js";
import { sqliteWorkerStoreCompileCacheParentEntrypoint } from "./sqlite-worker-store.compile-cache-runtime.test-support.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
@ -58,11 +57,8 @@ describe("SQLite store worker compile cache", () => {
delete env.NODE_DISABLE_COMPILE_CACHE;
delete env.NODE_OPTIONS;
const result = await runNodeScript(
[
...resolveRuntimeWorkerArgv(
resolveRuntimeWorkerUrl(sqliteWorkerStoreCompileCacheParentEntrypoint),
resolveTestNodeExecPath(),
),
(workerArgv) => [
...workerArgv(resolveRuntimeWorkerUrl(sqliteWorkerStoreCompileCacheParentEntrypoint)),
root,
testCase.owner,
testCase.cache ?? "unset",

View file

@ -57,8 +57,9 @@ export function scopeWrapperSource(helperExitPath: string): string {
return `#!${process.execPath}
const fs = require("node:fs");
const { spawn } = require("node:child_process");
const [command, scriptPath, paramsPath] = process.argv.slice(-3);
const helper = spawn(command, [scriptPath, paramsPath], { stdio: ["pipe", "pipe", "ignore"] });
const args = process.argv.slice(2);
const [command, ...helperArgs] = args.slice(args.findIndex((arg) => !arg.startsWith("-")));
const helper = spawn(command, helperArgs, { stdio: ["pipe", "pipe", "ignore"] });
let helperAlive = true;
helper.stdin.on("error", () => {});
helper.stdout.pipe(process.stdout, { end: false });

View file

@ -2,8 +2,7 @@ import type { Profiler } from "node:inspector";
import { fileURLToPath } from "node:url";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { diagnosticProfileEntrypoints } from "./diagnostic-profile-runtime.test-support.js";
const native = vi.hoisted(() => ({
@ -518,8 +517,8 @@ assert.equal(process.pid, pid);
console.log(JSON.stringify({ node: process.version, platform: process.platform, arch: process.arch, actualDurationMs: result.actualDurationMs, samples: result.profile.samples.length, nodes: result.profile.nodes.length, listener: false }));
`;
const result = await runNodeScript(
[
...resolveRuntimeWorkerArgv(ownerUrl, resolveTestNodeExecPath()).slice(0, -1),
(workerArgv) => [
...workerArgv(ownerUrl).slice(0, -1),
"--input-type=module",
"--eval",
source,

View file

@ -3,8 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { DiagnosticsHeapProfileParams } from "../../packages/gateway-protocol/src/schema/diagnostics.js";
import { createDeferred } from "../../test/helpers/promise.js";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { diagnosticProfileEntrypoints } from "./diagnostic-profile-runtime.test-support.js";
const hostBunVersion = Object.getOwnPropertyDescriptor(process.versions, "bun");
@ -378,8 +377,8 @@ for (const includeCollected of [false, true]) {
assert.equal(url(), undefined);
`;
const result = await runNodeScript(
[
...resolveRuntimeWorkerArgv(ownerUrl, resolveTestNodeExecPath()).slice(0, -1),
(workerArgv) => [
...workerArgv(ownerUrl).slice(0, -1),
"--expose-gc",
"--input-type=module",
"--eval",

View file

@ -17,7 +17,7 @@ import {
completeAgentDeletionJournalInDatabase,
} from "../state/agent-deletion-journal.js";
import {
closeOpenClawAgentDatabasesForTest,
closeOpenClawAgentDatabasesAsync,
openOpenClawAgentDatabase,
} from "../state/openclaw-agent-db.js";
import { SQLITE_SESSION_WRITER_QUEUES } from "../state/openclaw-agent-write-admission.js";
@ -62,7 +62,7 @@ describe("plugin host cleanup session stores", () => {
},
);
}
closeOpenClawAgentDatabasesForTest();
await closeOpenClawAgentDatabasesAsync();
const retainedDatabase = path.join(
fixtureStateDir,
"agents",
@ -224,7 +224,7 @@ describe("plugin host cleanup session stores", () => {
},
);
}
closeOpenClawAgentDatabasesForTest();
await closeOpenClawAgentDatabasesAsync();
const retiredBefore = loadSessionEntry({
agentId: "retired",
storePath: sharedStore,
@ -288,7 +288,7 @@ describe("plugin host cleanup session stores", () => {
pluginExtensions: { fixture: { active: true } },
},
);
closeOpenClawAgentDatabasesForTest();
await closeOpenClawAgentDatabasesAsync();
const databasePath = path.join(
fixtureStateDir,
"agents",
@ -532,7 +532,7 @@ describe("plugin host cleanup session stores", () => {
});
expect(result).toEqual({ cleanupCount: 1, failures: [] });
closeOpenClawAgentDatabasesForTest();
await closeOpenClawAgentDatabasesAsync();
const target = loadSessionEntry({ sessionKey: targetKey, storePath });
expect(target?.pluginExtensions).toEqual({ other: { state: { preserved: true } } });
expect(target?.pluginNextTurnInjections).toBeUndefined();

View file

@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { expect, it, vi } from "vitest";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createDeferredCore } from "../shared/deferred.js";
import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js";
@ -15,12 +15,30 @@ import { createTestPluginRegistry } from "./registry-runtime.test-helpers.js";
import { disposePluginRegistryInstances } from "./runtime.js";
import { getPluginRuntimeGatewayRequestScope } from "./runtime/gateway-request-scope.js";
const cleanupClock = vi.hoisted(() => ({ setTimeout: globalThis.setTimeout }));
beforeEach(() => {
const nativeSetTimeout = globalThis.setTimeout;
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"], shouldClearNativeTimers: true });
cleanupClock.setTimeout = globalThis.setTimeout;
globalThis.setTimeout = nativeSetTimeout;
});
afterEach(() => vi.useRealTimers());
vi.mock("./host-hook-cleanup-timeout.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./host-hook-cleanup-timeout.js")>();
return {
...actual,
withPluginHostCleanupTimeout: <T>(hookId: string, cleanup: () => T | Promise<T>) =>
actual.withPluginHostCleanupTimeout(hookId, cleanup, 500),
withPluginHostCleanupTimeout: <T>(hookId: string, cleanup: () => T | Promise<T>) => {
const nativeSetTimeout = globalThis.setTimeout;
// Only the cleanup deadline is virtual; asynchronous workers and watchers keep real timers.
globalThis.setTimeout = cleanupClock.setTimeout;
try {
return actual.withPluginHostCleanupTimeout(hookId, cleanup, 500);
} finally {
globalThis.setTimeout = nativeSetTimeout;
}
},
};
});
@ -144,6 +162,7 @@ it.each([
const entered = createDeferredCore();
const releaseCreate = createDeferredCore();
const releaseClose = createDeferredCore();
const closeEntered = createDeferredCore();
const probeEntered = createDeferredCore();
const releaseProbe = createDeferredCore();
let rejectClose = mode === "failed-close";
@ -173,6 +192,7 @@ it.each([
};
});
const close = vi.fn(async () => {
closeEntered.resolve();
expect(getPluginRuntimeGatewayRequestScope()?.pluginId).toBe(targetId);
if (rejectClose) {
throw new Error("synthetic provider cleanup refused");
@ -366,7 +386,10 @@ it.each([
activatePluginRegistry(next, null, "gateway-bindable", undefined, owner.registry);
reload.commit();
} else if (mode === "timeout") {
await expect(drain).rejects.toThrow("plugin host cleanup timed out");
const expired = expect(drain).rejects.toThrow("plugin host cleanup timed out");
await closeEntered.promise;
await vi.advanceTimersByTimeAsync(500);
await expired;
activatePluginRegistry(next, null, "gateway-bindable", undefined, owner.registry);
reload.commit();
const fresh = await owner.runtime.getMemorySearchManager({ cfg: config, agentId: "main" });
@ -383,7 +406,9 @@ it.each([
await finalClose;
expect(close).toHaveBeenCalledOnce();
} else if (mode === "late-probe") {
await expect(drain).rejects.toThrow("plugin host cleanup timed out");
const expired = expect(drain).rejects.toThrow("plugin host cleanup timed out");
await vi.advanceTimersByTimeAsync(500);
await expired;
activatePluginRegistry(next, null, "gateway-bindable", undefined, owner.registry);
reload.commit();
const fresh = await owner.runtime.getMemorySearchManager({ cfg: config, agentId: "main" });

View file

@ -1,20 +1,16 @@
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { execOutputRetentionEntrypoint } from "./retention-runtime.test-support.js";
it("releases discarded backing buffers while capped commands are still running", async ({
signal,
}) => {
const result = await runNodeScript(
[
(workerArgv) => [
"--expose-gc",
...resolveRuntimeWorkerArgv(
resolveRuntimeWorkerUrl(execOutputRetentionEntrypoint),
resolveTestNodeExecPath(),
),
...workerArgv(resolveRuntimeWorkerUrl(execOutputRetentionEntrypoint)),
],
{ ...process.env, NODE_OPTIONS: "", TSX_DISABLE_CACHE: "1" },
15_000,

View file

@ -10,8 +10,8 @@ import {
readClawPackageRefs,
} from "../claws/provenance.js";
import type { ClawAddPlan } from "../claws/types.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { createNodeEvalArgs, resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { createNodeEvalArgs } from "../test-utils/node-process.js";
import { markClawPackageIndependentlyOwned } from "./claw-package-adoption.js";
import { acquireClawPackageLifecycleLease } from "./claw-package-lifecycle-lease.js";
import { stateNativeProcessEntrypoints } from "./native-process-runtime.test-support.js";
@ -286,8 +286,8 @@ describe("Claw package independent adoption", () => {
stateNativeProcessEntrypoints.clawPackageLifecycleLease,
);
const result = await runNodeScript(
[
...resolveRuntimeWorkerArgv(moduleUrl, resolveTestNodeExecPath()).slice(0, -1),
(workerArgv) => [
...workerArgv(moduleUrl).slice(0, -1),
...createNodeEvalArgs(
`
import { withClawPackageLifecycleLease } from ${JSON.stringify(moduleUrl.href)};

View file

@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { requireNodeSqlite } from "../infra/node-sqlite.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { readStableSqliteFileGeneration } from "../infra/sqlite-file-generation.js";
import { readMainDatabasePosixLocks } from "../infra/sqlite-posix-locks.test-support.js";
import { readSqliteNumberPragma } from "../infra/sqlite-pragma.test-support.js";
@ -234,7 +234,7 @@ describe("OpenClaw database integrity verifier", () => {
it("relays a late restart-receipt Worker open to the parent verifier after native opening settles", async () => {
const result = await runNodeScript(
resolveRuntimeWorkerArgv(resolveRuntimeWorkerUrl(databaseVerifyHostRuntimeEntrypoint)),
(workerArgv) => workerArgv(resolveRuntimeWorkerUrl(databaseVerifyHostRuntimeEntrypoint)),
process.env,
30_000,
{ requireProcessTreeExit: true },

View file

@ -1,4 +1,3 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
@ -7,14 +6,14 @@ import {
TRANSCRIPTS_LEGACY_RESULT_MAX_BYTES,
TRANSCRIPTS_RESULT_MAX_BYTES,
} from "../../packages/gateway-protocol/src/schema/transcripts.js";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { executeSqliteQuerySync } from "../infra/kysely-sync.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import {
closeOpenClawStateDatabaseAsync,
closeOpenClawStateDatabaseForTest,
} from "../state/openclaw-state-db.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { activeSessions } from "./capture-startup.js";
import { transcriptLibraryTimezoneEntrypoint } from "./library-timezone-runtime.test-support.js";
import { exportTranscriptLibrary, getTranscriptLibrary, listTranscriptLibrary } from "./library.js";
@ -146,24 +145,21 @@ describe("transcript library SQLite reads", () => {
it(
"uses the process timezone for unzoned stored dates and range bounds",
{ timeout: 45_000 },
() => {
async ({ signal }) => {
const stateDir = tempDirs.make("transcript-library-timezone-");
const child = spawnSync(
resolveTestNodeExecPath(),
[
...resolveRuntimeWorkerArgv(resolveRuntimeWorkerUrl(transcriptLibraryTimezoneEntrypoint)),
const child = await runNodeScript(
(workerArgv) => [
...workerArgv(resolveRuntimeWorkerUrl(transcriptLibraryTimezoneEntrypoint)),
stateDir,
],
{
encoding: "utf8",
timeout: 30_000,
env: {
...process.env,
TZ: "America/Los_Angeles",
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
},
...process.env,
TZ: "America/Los_Angeles",
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
},
30_000,
{ signal, maxBuffer: 1024 * 1024, requireProcessTreeExit: process.platform !== "win32" },
);
expect(child.error, child.stderr).toBeUndefined();
expect(child.status, child.stderr).toBe(0);

View file

@ -4,8 +4,7 @@ import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { runNodeScript } from "../../test/helpers/run-node-script.js";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js";
import { workerImportRuntimeEntrypoints } from "./worker-import-runtime.test-support.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
@ -23,9 +22,9 @@ describe("worker runtime imports during admission", () => {
const workspace = path.join(root, "workspace");
await mkdir(workspace);
const result = await runNodeScript(
[
(workerArgv) => [
"--unhandled-rejections=strict",
...resolveRuntimeWorkerArgv(runtimeUrl, resolveTestNodeExecPath()).slice(0, -1),
...workerArgv(runtimeUrl).slice(0, -1),
fileURLToPath(new URL("./worker.runtime-imports.test-support.mjs", import.meta.url)),
mode,
workspace,

View file

@ -1,6 +1,7 @@
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { afterEach, expect, it, vi } from "vitest";
import { createVitestResourceOwner } from "../../scripts/lib/vitest-resource-ownership.mts";
import { createFixtureLifetime } from "./fixture-lifetime.js";
@ -64,6 +65,32 @@ child.once("message", () => process.exit(17));
});
});
it.for(["node", "current"] as const)(
"builds source worker arguments for the selected %s runtime",
async (runtime) => {
const script = join(tempDirs.make("openclaw-node-script-runtime-"), "worker.ts");
writeFileSync(
script,
`enum Answer { value = 42 }
console.log(JSON.stringify({ answer: Answer.value, bun: Boolean(process.versions.bun), args: process.argv.slice(2) }));
`,
);
const result = await runNodeScript(
(workerArgv) => [...workerArgv(pathToFileURL(script)), "worker-argument"],
process.env,
5_000,
{ executable: runtime === "current" ? process.execPath : undefined },
);
expect(result.error, result.stderr).toBeUndefined();
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
answer: 42,
bun: runtime === "current" && Boolean(process.versions.bun),
args: ["worker-argument"],
});
},
);
it.for(["at limit", "stdout overflow", "stderr overflow"])(
"preserves independent 2 MiB output failure boundaries: %s",
async (mode) => {

View file

@ -1,10 +1,12 @@
import type { ChildProcess } from "node:child_process";
import { runManagedCommand } from "../../scripts/lib/managed-child-process.mts";
import { resolveRuntimeWorkerArgv } from "../../src/infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { createBoundedChildOutput } from "./bounded-child-output.js";
/** Worker argv is resolved against the executable selected for this child. */
export async function runNodeScript(
scriptPathOrArgs: string | string[],
scriptPathOrArgs: string | string[] | ((workerArgv: (url: URL) => string[]) => string[]),
env: NodeJS.ProcessEnv,
timeoutMs: number | undefined,
{
@ -31,9 +33,16 @@ export async function runNodeScript(
let status: number | null = null;
let error: unknown;
try {
const execPath = executable ?? resolveTestNodeExecPath();
const args =
typeof scriptPathOrArgs === "function"
? scriptPathOrArgs((url) => resolveRuntimeWorkerArgv(url, execPath))
: typeof scriptPathOrArgs === "string"
? [scriptPathOrArgs]
: scriptPathOrArgs;
status = await runManagedCommand({
bin: executable ?? resolveTestNodeExecPath(),
args: typeof scriptPathOrArgs === "string" ? [scriptPathOrArgs] : scriptPathOrArgs,
bin: execPath,
args,
cwd,
env,
timeoutMs,

View file

@ -3,11 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, expect, it } from "vitest";
import { scriptProcessEntrypoints } from "../../scripts/script-process-runtime.test-support.js";
import {
resolveRuntimeWorkerArgv,
resolveRuntimeWorkerUrl,
} from "../../src/infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../../src/infra/runtime-worker-url.js";
import { createFixtureLifetime } from "../helpers/fixture-lifetime.js";
import { createDeferred } from "../helpers/promise.js";
import { runNodeScript } from "../helpers/run-node-script.js";
@ -40,15 +36,17 @@ it.skipIf(process.platform === "win32").for(cases)(
const ready = createDeferred();
const receivedSignal = createDeferred();
let child: ChildProcess | undefined;
const scriptArgs = resolveRuntimeWorkerArgv(scriptUrl, resolveTestNodeExecPath());
const command = fixtures.track(
runNodeScript(
[
...scriptArgs.slice(0, -1),
"--import",
new URL("./fixtures/agent-plugin-gateway-cancellation.mjs", import.meta.url).href,
...scriptArgs.slice(-1),
],
(workerArgv) => {
const scriptArgs = workerArgv(scriptUrl);
return [
...scriptArgs.slice(0, -1),
"--import",
new URL("./fixtures/agent-plugin-gateway-cancellation.mjs", import.meta.url).href,
...scriptArgs.slice(-1),
];
},
{
PATH: process.env.PATH,
HOME: home,

View file

@ -13,6 +13,7 @@ import {
PACKAGE_LIFECYCLE_PENDING_RELATIVE_PATH,
} from "../../scripts/lib/package-lifecycle-marker.mjs";
import { WORKSPACE_TEMPLATE_PACK_PATHS } from "../../scripts/lib/workspace-bootstrap-smoke.mts";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
import {
CODE_MODE_WORKER_PATH,
@ -220,7 +221,7 @@ childProcess.spawnSync = function (...callArgs) {
syncBuiltinESMExports();
`,
);
const result = spawnSync(process.execPath, [resolve(CHECK_SCRIPT), tarball], {
const result = spawnSync(resolveTestNodeExecPath(), [resolve(CHECK_SCRIPT), tarball], {
encoding: "utf8",
env: {
...process.env,

View file

@ -1,10 +1,6 @@
import { afterEach, expect, it } from "vitest";
import { scriptModuleEntrypoints } from "../../scripts/script-module-runtime.test-support.mjs";
import {
resolveRuntimeWorkerArgv,
resolveRuntimeWorkerUrl,
} from "../../src/infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { resolveRuntimeWorkerUrl } from "../../src/infra/runtime-worker-url.js";
import { runNodeScript } from "../helpers/run-node-script.js";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
@ -15,11 +11,8 @@ it("loads installed tooling when the prepared CLI validates native Vitest option
}) => {
const directory = tempDirs.make("openclaw-compiled-tooling-");
const result = await runNodeScript(
[
...resolveRuntimeWorkerArgv(
resolveRuntimeWorkerUrl(scriptModuleEntrypoints.testProjects),
resolveTestNodeExecPath(),
),
(workerArgv) => [
...workerArgv(resolveRuntimeWorkerUrl(scriptModuleEntrypoints.testProjects)),
"test/scripts/test-projects.test.ts",
"--",
"--invalid-native-vitest-option",

View file

@ -67,7 +67,7 @@ function createWorkerArtifactFixtures(
return joined;
}
function node(args: string[], cwd = root, env = process.env) {
function node(args: Parameters<typeof runNodeScript>[0], cwd = root, env = process.env) {
const completion = fixtureLifetime.track(
runNodeScript(args, compilerEnv(env, "node"), undefined, {
cwd,
@ -401,7 +401,9 @@ export function workerProbe(
const sourceLoader = sourceMode && !process.versions.bun;
expect(args.includes('--import')).toBe(sourceLoader);
if (sourceLoader) expect(args[1].startsWith('file:')).toBe(true);
expect(args[sourceLoader ? 2 : 0]).toMatch(sourceMode ? /\\.ts$/ : /\\.js$/);
const runtimeArgs = sourceLoader ? ['--import', expect.stringMatching(/^file:/)] : process.versions.bun ? ['--no-install'] : [];
expect(args.slice(0, runtimeArgs.length + 1)).toEqual([...runtimeArgs, fileURLToPath(generation)]);
expect(args[runtimeArgs.length]).toMatch(sourceMode ? /\\.ts$/ : /\\.js$/);
fs.appendFileSync(${JSON.stringify(path.join(directory, "observations.jsonl"))}, JSON.stringify({args, tuiUrls, setupUrls, retentionUrl, value, configValue:inject('configValue'), knn:resolveRuntimeWorkerUrl(vectorKnnProcessEntrypoint).href})+'\\n');
fs.appendFileSync(${JSON.stringify(path.join(directory, "generations.jsonl"))}, JSON.stringify(generation)+'\\n');
${receiptEndpoint ? `sendReceipt(${JSON.stringify(path.join(directory, "generations.jsonl"))}, 'written');` : ""}

View file

@ -15,8 +15,6 @@ import {
} from "../../scripts/lib/vitest-worker-artifacts.mts";
import { resolveVitestSpawnParams, spawnWatchedVitestProcess } from "../../scripts/run-vitest.mts";
import { createVitestProcessCompletion } from "../../scripts/vitest-process-group.mts";
import { resolveRuntimeWorkerArgv } from "../../src/infra/runtime-worker-url.js";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import {
fixtureReceiptClientSource,
openFixtureReceiptChannel,
@ -751,8 +749,8 @@ describe.concurrent("fresh compiled subprocess invocation", () => {
: path.join(root, "src/agents/embedded-agent-runner/run/payloads.ts"),
);
const result = await node(
[
...resolveRuntimeWorkerArgv(pathToFileURL(probe), resolveTestNodeExecPath()),
(workerArgv) => [
...workerArgv(pathToFileURL(probe)),
url.href,
pathToFileURL(
owner