mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 14:12:28 +08:00
* test: prepare bounded TypeScript child probes Prepare native test children through the invocation-owned compiler before their existing deadlines. Preserve module boundaries for import guards and script entry detection, and keep source-loader contracts explicit. Retain native process isolation, worker lifetimes, assertions, and deadlines. Cover MTS declaration selection in the existing compiler fixture. Also declare the generated fallback plugin fixture as ESM and remove one unused internal script export. * test: prepare remaining worker and tooling fixtures Keep installed tooling at its native package location so prepared probes retain native bindings and relative assets. Preserve existing child deadlines, assertions, and source-layout contracts. * test: assert native CLI delegation before option rejection * test: preserve sparse prepared fixture entry contracts * test: mark prepared budget fixtures as ES modules * test: preserve serialized fixture inputs under type-aware lint * test: prepare sparse wrapper and report fixtures * test: prepare remaining CLI wrapper children * test: complete bounded child fixture preparation
205 lines
7.6 KiB
TypeScript
205 lines
7.6 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { once } from "node:events";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
inspectManagedProcessGroup,
|
|
waitForManagedProcessGroupExit,
|
|
} from "../../scripts/lib/managed-child-process.mts";
|
|
import { runE2eGlobalSetup } from "../../scripts/lib/vitest-build-prerequisites.mts";
|
|
import { scriptModuleEntrypoints } from "../../scripts/script-module-runtime.test-support.mjs";
|
|
import { forwardSignalToVitestProcessGroup } from "../../scripts/vitest-process-group.mts";
|
|
import {
|
|
resolveRuntimeWorkerArgv,
|
|
resolveRuntimeWorkerUrl,
|
|
} from "../../src/infra/runtime-worker-url.js";
|
|
import { killPidIfAlive } from "../../src/test-utils/process-tree.js";
|
|
import { waitForDead, waitForPidFile } from "../helpers/process-wait.js";
|
|
import { withTestTimeout } from "../helpers/promise.js";
|
|
import { runQaGatewayFixture } from "../helpers/qa-gateway-cleanup.js";
|
|
|
|
type SetupCommandRunner = NonNullable<Parameters<typeof runE2eGlobalSetup>[0]>;
|
|
|
|
const posixIt = process.platform === "win32" ? it.skip : it;
|
|
const PROCESS_TIMEOUT_MS = process.env.CI ? 15_000 : 5_000;
|
|
|
|
describe("vitest E2E global setup", () => {
|
|
it("runs both build commands sequentially with their exact environments", async () => {
|
|
let resolveFirstCommand!: (status: number) => void;
|
|
const firstCommand = new Promise<number>((resolve) => {
|
|
resolveFirstCommand = resolve;
|
|
});
|
|
const runCommand = vi
|
|
.fn<SetupCommandRunner>()
|
|
.mockImplementationOnce(() => firstCommand)
|
|
.mockResolvedValueOnce(0);
|
|
|
|
const setupPromise = runE2eGlobalSetup(runCommand);
|
|
await vi.waitFor(() => expect(runCommand).toHaveBeenCalledTimes(1));
|
|
resolveFirstCommand(0);
|
|
await setupPromise;
|
|
expect(runCommand.mock.calls).toEqual([
|
|
[
|
|
["scripts/run-node.mjs", "--version"],
|
|
{
|
|
...process.env,
|
|
OPENCLAW_BUILD_PRIVATE_QA: "1",
|
|
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "0",
|
|
},
|
|
],
|
|
[
|
|
["--import", "tsx", "scripts/tsdown-build.mts", "--config", "tsdown.ai.config.ts"],
|
|
process.env,
|
|
],
|
|
]);
|
|
});
|
|
|
|
it("propagates a nonzero command status", async () => {
|
|
const runCommand = vi
|
|
.fn<SetupCommandRunner>()
|
|
.mockResolvedValueOnce(0)
|
|
.mockResolvedValueOnce(23);
|
|
await expect(runE2eGlobalSetup(runCommand)).rejects.toThrow(
|
|
"E2E setup command failed with exit code 23: --import tsx scripts/tsdown-build.mts --config tsdown.ai.config.ts",
|
|
);
|
|
});
|
|
|
|
it.each(["OPENCLAW_E2E_SKIP_BUILD", "OPENCLAW_E2E_USE_PREBUILT_DIST"] as const)(
|
|
"skips rebuilding when %s is set",
|
|
async (envName) => {
|
|
const runCommand = vi.fn<SetupCommandRunner>();
|
|
|
|
await runE2eGlobalSetup(runCommand, { [envName]: "1" });
|
|
|
|
expect(runCommand).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
posixIt("forwards output and SIGTERM through the runner process group", async () => {
|
|
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-setup-group-"));
|
|
const fixturePath = path.join(fixtureDir, "scripts", "run-node.mjs");
|
|
const pidPaths = ["child.pid", "descendant.pid"].map((name) => path.join(fixtureDir, name));
|
|
let cleanup = async () => fs.rmSync(fixtureDir, { force: true, recursive: true });
|
|
await runQaGatewayFixture(
|
|
async () => {
|
|
fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
|
|
fs.writeFileSync(
|
|
fixturePath,
|
|
`import { spawn } from "node:child_process";
|
|
import fs from "node:fs";
|
|
process.stdin.once("data", () => {
|
|
const descendant = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" });
|
|
fs.writeFileSync(${JSON.stringify(pidPaths[0])}, String(process.pid));
|
|
fs.writeFileSync(${JSON.stringify(pidPaths[1])}, String(descendant.pid));
|
|
process.stdout.write("setup-stdout\\n");
|
|
process.stderr.write("setup-stderr\\n");
|
|
setInterval(() => {}, 1000);
|
|
});
|
|
process.stdin.resume();
|
|
`,
|
|
);
|
|
const setupUrl = resolveRuntimeWorkerUrl(scriptModuleEntrypoints.vitestBuildPrerequisites);
|
|
const runnerScript = `import { runE2eGlobalSetup } from ${JSON.stringify(setupUrl.href)};
|
|
process.chdir(${JSON.stringify(fixtureDir)});
|
|
await runE2eGlobalSetup(undefined, process.env);`;
|
|
const runner = spawn(
|
|
process.execPath,
|
|
[
|
|
...resolveRuntimeWorkerArgv(setupUrl).slice(0, -1),
|
|
"--input-type=module",
|
|
"--eval",
|
|
runnerScript,
|
|
],
|
|
{ detached: true, stdio: ["pipe", "pipe", "pipe"] },
|
|
);
|
|
const closed = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
|
|
(resolve) => {
|
|
runner.once("close", (code, signal) => resolve({ code, signal }));
|
|
},
|
|
);
|
|
const pids: number[] = [];
|
|
cleanup = () =>
|
|
runQaGatewayFixture(
|
|
async () => {
|
|
if (!runner.pid) {
|
|
return;
|
|
}
|
|
try {
|
|
process.kill(-runner.pid, "SIGKILL");
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ESRCH") {
|
|
throw error;
|
|
}
|
|
}
|
|
},
|
|
() => killPidIfAlive(runner.pid),
|
|
...pidPaths.map((file) => async () => {
|
|
if (!fs.existsSync(file)) {
|
|
return;
|
|
}
|
|
const pid = await waitForPidFile(file, PROCESS_TIMEOUT_MS);
|
|
killPidIfAlive(pid);
|
|
}),
|
|
async () => {
|
|
// Keep PID evidence unless every process and its output have finished.
|
|
await runQaGatewayFixture(
|
|
async () => {
|
|
await withTestTimeout(
|
|
closed,
|
|
PROCESS_TIMEOUT_MS,
|
|
`runner did not close; retained fixture: ${fixtureDir}`,
|
|
);
|
|
},
|
|
async () => {
|
|
if (!runner.pid) {
|
|
return;
|
|
}
|
|
await waitForManagedProcessGroupExit(runner, PROCESS_TIMEOUT_MS, {
|
|
errorPolicy: "indeterminate",
|
|
});
|
|
expect(
|
|
inspectManagedProcessGroup(runner, { errorPolicy: "indeterminate" }),
|
|
`retained fixture: ${fixtureDir}`,
|
|
).toBe("dead");
|
|
},
|
|
);
|
|
fs.rmSync(fixtureDir, { force: true, recursive: true });
|
|
},
|
|
);
|
|
await once(runner, "spawn");
|
|
let stdout = "";
|
|
let stderr = "";
|
|
runner.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
|
|
runner.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
|
|
runner.stdin.write("start\n");
|
|
for (const file of pidPaths) {
|
|
pids.push(await waitForPidFile(file, PROCESS_TIMEOUT_MS));
|
|
}
|
|
await vi.waitFor(() => {
|
|
expect(stdout).toContain("setup-stdout");
|
|
expect(stderr).toContain("setup-stderr");
|
|
});
|
|
expect(
|
|
forwardSignalToVitestProcessGroup({
|
|
child: runner,
|
|
kill: process.kill.bind(process),
|
|
signal: "SIGTERM",
|
|
}),
|
|
).toBe(true);
|
|
await expect(
|
|
withTestTimeout(
|
|
closed,
|
|
PROCESS_TIMEOUT_MS,
|
|
`runner did not close; retained fixture: ${fixtureDir}`,
|
|
),
|
|
).resolves.toEqual({ code: null, signal: "SIGTERM" });
|
|
await Promise.all(pids.map((pid) => waitForDead(pid, PROCESS_TIMEOUT_MS)));
|
|
},
|
|
() => cleanup(),
|
|
);
|
|
});
|
|
});
|