2026-03-27 12:20:51 +00:00
|
|
|
|
import { EventEmitter } from "node:events";
|
|
|
|
|
|
import { PassThrough } from "node:stream";
|
2026-03-28 11:53:14 +00:00
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
2026-03-31 20:26:13 +09:00
|
|
|
|
import { createExecutionArtifacts, executePlan } from "../../scripts/test-planner/executor.mjs";
|
2026-03-27 12:20:51 +00:00
|
|
|
|
|
2026-03-28 11:53:14 +00:00
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
|
|
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
|
|
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
|
|
|
|
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-27 12:20:51 +00:00
|
|
|
|
afterEach(() => {
|
2026-03-27 08:40:48 -05:00
|
|
|
|
vi.useRealTimers();
|
2026-03-27 12:20:51 +00:00
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe("test planner executor", () => {
|
|
|
|
|
|
it("falls back to child exit when close never arrives", async () => {
|
2026-03-31 19:20:07 +01:00
|
|
|
|
vi.useFakeTimers();
|
2026-03-27 12:20:51 +00:00
|
|
|
|
const stdout = new PassThrough();
|
|
|
|
|
|
const stderr = new PassThrough();
|
|
|
|
|
|
const fakeChild = Object.assign(new EventEmitter(), {
|
|
|
|
|
|
stdout,
|
|
|
|
|
|
stderr,
|
|
|
|
|
|
pid: 12345,
|
|
|
|
|
|
kill: vi.fn(),
|
|
|
|
|
|
});
|
|
|
|
|
|
const spawnMock = vi.fn(() => {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
fakeChild.emit("exit", 0, null);
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
return fakeChild;
|
|
|
|
|
|
});
|
|
|
|
|
|
const artifacts = createExecutionArtifacts({ OPENCLAW_TEST_CLOSE_GRACE_MS: "10" });
|
|
|
|
|
|
const executePromise = executePlan(
|
|
|
|
|
|
{
|
2026-03-27 23:20:41 -05:00
|
|
|
|
failurePolicy: "fail-fast",
|
2026-03-27 12:20:51 +00:00
|
|
|
|
passthroughMetadataOnly: true,
|
|
|
|
|
|
passthroughOptionArgs: [],
|
|
|
|
|
|
runtimeCapabilities: { isWindowsCi: false, isCI: false, isWindows: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
env: { OPENCLAW_TEST_CLOSE_GRACE_MS: "10" },
|
|
|
|
|
|
artifacts,
|
2026-03-31 20:26:13 +09:00
|
|
|
|
spawn: spawnMock,
|
2026-03-27 12:20:51 +00:00
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-31 19:20:07 +01:00
|
|
|
|
await vi.runAllTimersAsync();
|
2026-03-27 23:20:41 -05:00
|
|
|
|
await expect(executePromise).resolves.toMatchObject({
|
|
|
|
|
|
exitCode: 0,
|
|
|
|
|
|
summary: {
|
|
|
|
|
|
failedRunCount: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-03-27 12:20:51 +00:00
|
|
|
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
|
|
|
|
|
|
|
artifacts.cleanupTempArtifacts();
|
|
|
|
|
|
});
|
2026-03-27 23:20:41 -05:00
|
|
|
|
|
|
|
|
|
|
it("collects failures across planned units when failure policy is collect-all", async () => {
|
2026-03-31 19:20:07 +01:00
|
|
|
|
vi.useFakeTimers();
|
2026-03-27 23:20:41 -05:00
|
|
|
|
const children = [1, 2].map((pid, index) => {
|
|
|
|
|
|
const stdout = new PassThrough();
|
|
|
|
|
|
const stderr = new PassThrough();
|
|
|
|
|
|
return Object.assign(new EventEmitter(), {
|
|
|
|
|
|
stdout,
|
|
|
|
|
|
stderr,
|
|
|
|
|
|
pid,
|
|
|
|
|
|
kill: vi.fn(),
|
|
|
|
|
|
index,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
let childIndex = 0;
|
|
|
|
|
|
const spawnMock = vi.fn(() => {
|
|
|
|
|
|
const child = children[childIndex];
|
|
|
|
|
|
childIndex += 1;
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
child.stdout.write(
|
|
|
|
|
|
child.index === 0
|
|
|
|
|
|
? " ❯ src/alpha.test.ts (1 test | 1 failed)\n"
|
|
|
|
|
|
: " ❯ src/beta.test.ts (1 test | 1 failed)\n",
|
|
|
|
|
|
);
|
|
|
|
|
|
child.emit("exit", 1, null);
|
|
|
|
|
|
child.emit("close", 1, null);
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
return child;
|
|
|
|
|
|
});
|
|
|
|
|
|
const artifacts = createExecutionArtifacts({});
|
2026-03-31 19:20:07 +01:00
|
|
|
|
const reportPromise = executePlan(
|
2026-03-27 23:20:41 -05:00
|
|
|
|
{
|
|
|
|
|
|
failurePolicy: "collect-all",
|
|
|
|
|
|
passthroughMetadataOnly: false,
|
|
|
|
|
|
passthroughOptionArgs: [],
|
|
|
|
|
|
targetedUnits: [],
|
|
|
|
|
|
parallelUnits: [
|
|
|
|
|
|
{ id: "unit-a", args: ["vitest", "run", "src/alpha.test.ts"] },
|
|
|
|
|
|
{ id: "unit-b", args: ["vitest", "run", "src/beta.test.ts"] },
|
|
|
|
|
|
],
|
|
|
|
|
|
serialUnits: [],
|
|
|
|
|
|
serialPrefixUnits: [],
|
|
|
|
|
|
shardCount: 1,
|
|
|
|
|
|
shardIndexOverride: null,
|
|
|
|
|
|
topLevelSingleShardAssignments: new Map(),
|
|
|
|
|
|
runtimeCapabilities: { isWindowsCi: false, isCI: false, isWindows: false },
|
|
|
|
|
|
topLevelParallelEnabled: false,
|
|
|
|
|
|
topLevelParallelLimit: 1,
|
|
|
|
|
|
deferredRunConcurrency: 1,
|
|
|
|
|
|
passthroughRequiresSingleRun: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
env: {},
|
|
|
|
|
|
artifacts,
|
2026-03-31 20:26:13 +09:00
|
|
|
|
spawn: spawnMock,
|
2026-03-27 23:20:41 -05:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-03-31 19:20:07 +01:00
|
|
|
|
await vi.runAllTimersAsync();
|
|
|
|
|
|
const report = await reportPromise;
|
2026-03-27 23:20:41 -05:00
|
|
|
|
|
|
|
|
|
|
expect(spawnMock).toHaveBeenCalledTimes(2);
|
|
|
|
|
|
expect(report.exitCode).toBe(1);
|
|
|
|
|
|
expect(report.summary.failedRunCount).toBe(2);
|
|
|
|
|
|
expect(report.summary.failedTestFileCount).toBe(2);
|
|
|
|
|
|
expect(report.results.map((result) => result.classification)).toEqual([
|
|
|
|
|
|
"test-failure",
|
|
|
|
|
|
"test-failure",
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
artifacts.cleanupTempArtifacts();
|
|
|
|
|
|
});
|
2026-03-28 12:45:19 +00:00
|
|
|
|
|
|
|
|
|
|
it("injects a valid localstorage file path into child NODE_OPTIONS", async () => {
|
2026-03-31 19:20:07 +01:00
|
|
|
|
vi.useFakeTimers();
|
2026-03-28 12:45:19 +00:00
|
|
|
|
const stdout = new PassThrough();
|
|
|
|
|
|
const stderr = new PassThrough();
|
|
|
|
|
|
const fakeChild = Object.assign(new EventEmitter(), {
|
|
|
|
|
|
stdout,
|
|
|
|
|
|
stderr,
|
|
|
|
|
|
pid: 123,
|
|
|
|
|
|
kill: vi.fn(),
|
|
|
|
|
|
});
|
|
|
|
|
|
let capturedEnv;
|
|
|
|
|
|
const spawnMock = vi.fn((_command, _args, options) => {
|
|
|
|
|
|
capturedEnv = options?.env;
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
fakeChild.emit("exit", 0, null);
|
|
|
|
|
|
fakeChild.emit("close", 0, null);
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
return fakeChild;
|
|
|
|
|
|
});
|
|
|
|
|
|
const artifacts = createExecutionArtifacts({
|
|
|
|
|
|
NODE_OPTIONS: "--max_old_space_size=4096 --localstorage-file",
|
|
|
|
|
|
});
|
2026-03-31 19:20:07 +01:00
|
|
|
|
const executePromise = executePlan(
|
|
|
|
|
|
{
|
|
|
|
|
|
failurePolicy: "fail-fast",
|
|
|
|
|
|
passthroughMetadataOnly: false,
|
|
|
|
|
|
passthroughOptionArgs: [],
|
|
|
|
|
|
targetedUnits: [],
|
|
|
|
|
|
parallelUnits: [{ id: "unit-a", args: ["vitest", "run", "src/alpha.test.ts"] }],
|
|
|
|
|
|
serialUnits: [],
|
|
|
|
|
|
serialPrefixUnits: [],
|
|
|
|
|
|
shardCount: 1,
|
|
|
|
|
|
shardIndexOverride: null,
|
|
|
|
|
|
topLevelSingleShardAssignments: new Map(),
|
|
|
|
|
|
runtimeCapabilities: { isWindowsCi: false, isCI: false, isWindows: false },
|
|
|
|
|
|
topLevelParallelEnabled: false,
|
|
|
|
|
|
topLevelParallelLimit: 1,
|
|
|
|
|
|
deferredRunConcurrency: 1,
|
|
|
|
|
|
passthroughRequiresSingleRun: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
env: {
|
|
|
|
|
|
NODE_OPTIONS: "--max_old_space_size=4096 --localstorage-file",
|
2026-03-28 12:45:19 +00:00
|
|
|
|
},
|
2026-03-31 19:20:07 +01:00
|
|
|
|
artifacts,
|
|
|
|
|
|
spawn: spawnMock,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
await vi.runAllTimersAsync();
|
|
|
|
|
|
await expect(executePromise).resolves.toMatchObject({
|
2026-03-28 12:45:19 +00:00
|
|
|
|
exitCode: 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
|
expect(capturedEnv?.NODE_OPTIONS).toContain("--max_old_space_size=4096");
|
|
|
|
|
|
expect(capturedEnv?.NODE_OPTIONS).toMatch(
|
|
|
|
|
|
/--localstorage-file=[^\s]+\.localstorage\.json(?:\s|$)/u,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(capturedEnv?.NODE_OPTIONS).not.toMatch(/(^|\s)--localstorage-file(?=\s|$)/u);
|
|
|
|
|
|
|
|
|
|
|
|
artifacts.cleanupTempArtifacts();
|
|
|
|
|
|
});
|
2026-03-27 12:20:51 +00:00
|
|
|
|
});
|