mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
25f7fc0ec5
Co-authored-by: Amp <amp@ampcode.com>
138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
forceKillVitestProcessGroup,
|
|
forwardSignalToVitestProcessGroup,
|
|
} from "../../scripts/vitest-process-group.mts";
|
|
import {
|
|
isProcessAlive,
|
|
waitForChildClose,
|
|
waitForDead,
|
|
waitForPidFile,
|
|
} from "../helpers/process-wait.js";
|
|
import { runE2eGlobalSetup } from "../vitest/vitest.e2e.global-setup.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, "build-fixture.mjs");
|
|
const pidPaths = ["child.pid", "descendant.pid"].map((name) => path.join(fixtureDir, name));
|
|
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 = new URL("../vitest/vitest.e2e.global-setup.ts", import.meta.url).href;
|
|
const runnerScript = `import { runE2eSetupCommand } from ${JSON.stringify(setupUrl)};
|
|
await runE2eSetupCommand([${JSON.stringify(fixturePath)}], process.env);`;
|
|
const runner = spawn(
|
|
process.execPath,
|
|
["--import", "tsx", "--input-type=module", "--eval", runnerScript],
|
|
{ detached: true, stdio: ["pipe", "pipe", "pipe"] },
|
|
);
|
|
const pids: number[] = [];
|
|
let stdout = "";
|
|
let stderr = "";
|
|
runner.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
|
|
runner.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
|
|
|
try {
|
|
runner.stdin.write("start\n");
|
|
pids.push(
|
|
...(await Promise.all(pidPaths.map((file) => waitForPidFile(file, PROCESS_TIMEOUT_MS)))),
|
|
);
|
|
await vi.waitFor(() => {
|
|
expect(stdout).toContain("setup-stdout");
|
|
expect(stderr).toContain("setup-stderr");
|
|
});
|
|
const closed = waitForChildClose(runner, PROCESS_TIMEOUT_MS);
|
|
expect(
|
|
forwardSignalToVitestProcessGroup({
|
|
child: runner,
|
|
kill: process.kill.bind(process),
|
|
signal: "SIGTERM",
|
|
}),
|
|
).toBe(true);
|
|
await expect(closed).resolves.toEqual({ code: null, signal: "SIGTERM" });
|
|
await Promise.all(pids.map((pid) => waitForDead(pid, PROCESS_TIMEOUT_MS)));
|
|
} finally {
|
|
forceKillVitestProcessGroup(runner);
|
|
for (const pid of pids) {
|
|
if (isProcessAlive(pid)) {
|
|
process.kill(pid, "SIGKILL");
|
|
}
|
|
}
|
|
fs.rmSync(fixtureDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|