test(qa-lab): accept native Windows paths

This commit is contained in:
Vincent Koc
2026-05-04 09:07:28 -07:00
parent 9008031e96
commit 30e259b9c5
8 changed files with 80 additions and 50 deletions

View File

@@ -548,6 +548,7 @@ describe("qa cli runtime", () => {
});
it("runs a host-only parity preflight against the sentinel scenario", async () => {
const repoRoot = path.resolve("/tmp/openclaw-repo");
await runQaSuiteCommand({
repoRoot: "/tmp/openclaw-repo",
providerMode: "mock-openai",
@@ -557,9 +558,9 @@ describe("qa cli runtime", () => {
});
expect(runQaSuiteFromRuntime).toHaveBeenCalledWith({
repoRoot: path.resolve("/tmp/openclaw-repo"),
outputDir: expect.stringMatching(
/^\/tmp\/openclaw-repo\/\.artifacts\/qa-e2e\/preflight\/suite-/,
repoRoot,
outputDir: expect.stringContaining(
path.join(repoRoot, ".artifacts", "qa-e2e", "preflight", "suite-"),
),
transportId: "qa-channel",
providerMode: "mock-openai",

View File

@@ -52,11 +52,13 @@ describe("runQaDockerUp", () => {
const fetchCalls: string[] = [];
const responseQueue = [false, true, true];
const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-"));
const repoRoot = path.resolve("/repo/openclaw");
const composeFile = path.join(outputDir, "docker-compose.qa.yml");
try {
const result = await runQaDockerUp(
{
repoRoot: "/repo/openclaw",
repoRoot,
outputDir,
gatewayPort: 18889,
qaLabPort: 43124,
@@ -78,12 +80,10 @@ describe("runQaDockerUp", () => {
);
expect(calls).toEqual([
"pnpm qa:lab:build @/repo/openclaw",
`docker compose -f ${outputDir}/docker-compose.qa.yml down --remove-orphans @/repo/openclaw`,
expect.stringContaining(
`docker compose -f ${outputDir}/docker-compose.qa.yml up --build -d @/repo/openclaw`,
),
`docker compose -f ${outputDir}/docker-compose.qa.yml ps --format json openclaw-qa-gateway @/repo/openclaw`,
`pnpm qa:lab:build @${repoRoot}`,
`docker compose -f ${composeFile} down --remove-orphans @${repoRoot}`,
expect.stringContaining(`docker compose -f ${composeFile} up --build -d @${repoRoot}`),
`docker compose -f ${composeFile} ps --format json openclaw-qa-gateway @${repoRoot}`,
]);
expect(fetchCalls).toEqual([
"http://127.0.0.1:43124/healthz",
@@ -92,8 +92,8 @@ describe("runQaDockerUp", () => {
]);
expect(result.qaLabUrl).toBe("http://127.0.0.1:43124");
expect(result.gatewayUrl).toBe("http://127.0.0.1:18889/");
expect(result.composeFile).toBe(`${outputDir}/docker-compose.qa.yml`);
expect(result.stopCommand).toBe(`docker compose -f ${outputDir}/docker-compose.qa.yml down`);
expect(result.composeFile).toBe(composeFile);
expect(result.stopCommand).toBe(`docker compose -f ${composeFile} down`);
} finally {
await rm(outputDir, { recursive: true, force: true });
}
@@ -102,11 +102,13 @@ describe("runQaDockerUp", () => {
it("skips UI build and compose --build for prebuilt images", async () => {
const calls: string[] = [];
const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-"));
const repoRoot = path.resolve("/repo/openclaw");
const composeFile = path.join(outputDir, "docker-compose.qa.yml");
try {
await runQaDockerUp(
{
repoRoot: "/repo/openclaw",
repoRoot,
outputDir,
usePrebuiltImage: true,
bindUiDist: true,
@@ -116,9 +118,9 @@ describe("runQaDockerUp", () => {
);
expect(calls).toEqual([
`docker compose -f ${outputDir}/docker-compose.qa.yml down --remove-orphans @/repo/openclaw`,
`docker compose -f ${outputDir}/docker-compose.qa.yml up -d @/repo/openclaw`,
`docker compose -f ${outputDir}/docker-compose.qa.yml ps --format json openclaw-qa-gateway @/repo/openclaw`,
`docker compose -f ${composeFile} down --remove-orphans @${repoRoot}`,
`docker compose -f ${composeFile} up -d @${repoRoot}`,
`docker compose -f ${composeFile} ps --format json openclaw-qa-gateway @${repoRoot}`,
]);
const compose = await readFile(path.join(outputDir, "docker-compose.qa.yml"), "utf8");
expect(compose).toContain(":/opt/openclaw-qa-lab-ui:ro");
@@ -210,11 +212,13 @@ describe("runQaDockerUp", () => {
const calls: string[] = [];
const fetchCalls: string[] = [];
const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-"));
const repoRoot = path.resolve("/repo/openclaw");
const composeFile = path.join(outputDir, "docker-compose.qa.yml");
try {
const result = await runQaDockerUp(
{
repoRoot: "/repo/openclaw",
repoRoot,
outputDir,
gatewayPort: 18889,
qaLabPort: 43124,
@@ -249,11 +253,11 @@ describe("runQaDockerUp", () => {
);
expect(calls).toEqual([
`docker compose -f ${outputDir}/docker-compose.qa.yml down --remove-orphans @/repo/openclaw`,
`docker compose -f ${outputDir}/docker-compose.qa.yml up -d @/repo/openclaw`,
`docker compose -f ${outputDir}/docker-compose.qa.yml ps --format json openclaw-qa-gateway @/repo/openclaw`,
`docker compose -f ${outputDir}/docker-compose.qa.yml ps -q openclaw-qa-gateway @/repo/openclaw`,
"docker inspect --format {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} gateway-container @/repo/openclaw",
`docker compose -f ${composeFile} down --remove-orphans @${repoRoot}`,
`docker compose -f ${composeFile} up -d @${repoRoot}`,
`docker compose -f ${composeFile} ps --format json openclaw-qa-gateway @${repoRoot}`,
`docker compose -f ${composeFile} ps -q openclaw-qa-gateway @${repoRoot}`,
`docker inspect --format {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} gateway-container @${repoRoot}`,
]);
expect(fetchCalls).toEqual([
"http://127.0.0.1:43124/healthz",

View File

@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import { afterEach, describe, expect, it, vi } from "vitest";
import { startQaLabServer } from "./lab-server.js";
import { startQaLabServer, type QaLabServerStartParams } from "./lab-server.js";
vi.mock("@openclaw/qa-channel/api.js", async () => await import("../../qa-channel/api.js"));
@@ -128,6 +128,13 @@ vi.mock("openclaw/plugin-sdk/proxy-capture", () => ({
const cleanups: Array<() => Promise<void>> = [];
async function startQaLabServerForTest(params?: QaLabServerStartParams) {
return await startQaLabServer({
embeddedGateway: "disabled",
...params,
});
}
afterEach(async () => {
captureMock.reset();
while (cleanups.length > 0) {
@@ -241,7 +248,7 @@ async function createQaLabRepoRootFixture(params?: {
}
describe("qa-lab server", () => {
it("serves bootstrap state and writes a self-check report", async () => {
it("serves bootstrap state and message state", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "qa-lab-test-"));
cleanups.push(async () => {
await rm(tempDir, { recursive: true, force: true });
@@ -249,13 +256,14 @@ describe("qa-lab server", () => {
const outputPath = path.join(tempDir, "self-check.md");
const repoRoot = await createQaLabRepoRootFixture();
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
outputPath,
repoRoot,
controlUiUrl: "http://127.0.0.1:18789/",
controlUiToken: "qa-token",
embeddedGateway: "disabled",
});
cleanups.push(async () => {
await lab.stop();
@@ -303,11 +311,7 @@ describe("qa-lab server", () => {
};
expect(snapshot.messages.some((message) => message.text === "hello from test")).toBe(true);
const result = await lab.runSelfCheck();
expect(result.scenarioResult.status).toBe("pass");
const markdown = await readFile(outputPath, "utf8");
expect(markdown).toContain("Synthetic Slack-class roundtrip");
expect(markdown).toContain("- Status: pass");
await expect(readFile(outputPath, "utf8")).rejects.toThrow();
});
it("anchors direct self-check runs under the explicit repo root by default", async () => {
@@ -316,10 +320,11 @@ describe("qa-lab server", () => {
await rm(repoRoot, { recursive: true, force: true });
});
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
repoRoot,
embeddedGateway: "disabled",
});
cleanups.push(async () => {
await lab.stop();
@@ -331,9 +336,10 @@ describe("qa-lab server", () => {
});
it("injects the kickoff task on demand and on startup", async () => {
const autoKickoffLab = await startQaLabServer({
const autoKickoffLab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
embeddedGateway: "disabled",
sendKickoffOnStart: true,
});
cleanups.push(async () => {
@@ -349,9 +355,10 @@ describe("qa-lab server", () => {
true,
);
const manualLab = await startQaLabServer({
const manualLab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
embeddedGateway: "disabled",
});
cleanups.push(async () => {
await manualLab.stop();
@@ -402,7 +409,7 @@ describe("qa-lab server", () => {
throw new Error("expected upstream address");
}
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
advertiseHost: "127.0.0.1",
@@ -445,7 +452,7 @@ describe("qa-lab server", () => {
"utf8",
);
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
uiDistDir,
@@ -473,7 +480,7 @@ describe("qa-lab server", () => {
"<!doctype html><html><head><title>Temp QA Lab UI</title></head><body>repo-root-ui</body></html>",
});
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
repoRoot,
@@ -530,7 +537,7 @@ describe("qa-lab server", () => {
"utf8",
);
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
repoRoot,
@@ -579,7 +586,7 @@ describe("qa-lab server", () => {
"utf8",
);
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
repoRoot,
@@ -597,11 +604,13 @@ describe("qa-lab server", () => {
await lab.stop();
stopped = true;
expect(await waitForFileContent(stoppedPath, "terminated")).toBe("terminated");
if (process.platform !== "win32") {
expect(await waitForFileContent(stoppedPath, "terminated")).toBe("terminated");
}
});
it("can disable the embedded echo gateway for real-suite runs", async () => {
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
embeddedGateway: "disabled",
@@ -630,7 +639,7 @@ describe("qa-lab server", () => {
});
it("exposes structured outcomes and can attach control-ui after startup", async () => {
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
embeddedGateway: "disabled",
@@ -776,7 +785,7 @@ describe("qa-lab server", () => {
}),
});
const lab = await startQaLabServer({
const lab = await startQaLabServerForTest({
host: "127.0.0.1",
port: 0,
});

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveLiveTransportQaRunOptions } from "./live-transport-cli.runtime.js";
@@ -11,7 +12,7 @@ describe("resolveLiveTransportQaRunOptions", () => {
alternateModel: "",
}),
).toMatchObject({
repoRoot: "/tmp/openclaw-repo",
repoRoot: path.resolve("/tmp/openclaw-repo"),
providerMode: "live-frontier",
primaryModel: undefined,
alternateModel: undefined,

View File

@@ -80,7 +80,17 @@ function killProcessTree(pid: number | undefined, signal: NodeJS.Signals) {
}
try {
if (process.platform === "win32") {
process.kill(pid, signal);
const killer = spawn("taskkill", ["/pid", String(pid), "/t", "/f"], {
stdio: "ignore",
windowsHide: true,
});
killer.once("error", () => {
try {
process.kill(pid, signal);
} catch {
// The process already exited.
}
});
return;
}
process.kill(-pid, signal);

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { defaultQaRuntimeModelForMode } = vi.hoisted(() => ({
@@ -131,8 +132,9 @@ describe("qa run config", () => {
});
it("anchors generated run output dirs under the provided repo root", () => {
const outputDir = createQaRunOutputDir("/tmp/openclaw-repo");
expect(outputDir.startsWith("/tmp/openclaw-repo/.artifacts/qa-e2e/lab-")).toBe(true);
const repoRoot = path.resolve("/tmp/openclaw-repo");
const outputDir = createQaRunOutputDir(repoRoot);
expect(outputDir.startsWith(path.join(repoRoot, ".artifacts", "qa-e2e", "lab-"))).toBe(true);
});
it("prefers the Codex OAuth default when the runtime resolver says it is available", () => {

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveQaSelfCheckOutputPath } from "./self-check.js";
@@ -12,8 +13,9 @@ describe("resolveQaSelfCheckOutputPath", () => {
});
it("anchors default self-check reports under the provided repo root", () => {
expect(resolveQaSelfCheckOutputPath({ repoRoot: "/tmp/openclaw-repo" })).toBe(
"/tmp/openclaw-repo/.artifacts/qa-e2e/self-check.md",
const repoRoot = path.resolve("/tmp/openclaw-repo");
expect(resolveQaSelfCheckOutputPath({ repoRoot })).toBe(
path.join(repoRoot, ".artifacts", "qa-e2e", "self-check.md"),
);
});
});

View File

@@ -1,4 +1,5 @@
import { EventEmitter } from "node:events";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const spawnMock = vi.hoisted(() => vi.fn());
@@ -95,7 +96,7 @@ describe("qa suite runtime agent process helpers", () => {
await expect(pending).resolves.toBe("ok");
expect(spawnMock).toHaveBeenCalledWith(
"/usr/bin/node",
["/repo/dist/index.js", "qa", "suite"],
[path.join("/repo", "dist", "index.js"), "qa", "suite"],
expect.objectContaining({
cwd: "/tmp/runtime",
env: { PATH: "/usr/bin" },
@@ -134,7 +135,7 @@ describe("qa suite runtime agent process helpers", () => {
await expect(pending).resolves.toBe("ok");
expect(spawnMock).toHaveBeenCalledWith(
"/usr/bin/node",
["/repo/dist/index.js", "crestodian", "-m", "overview"],
[path.join("/repo", "dist", "index.js"), "crestodian", "-m", "overview"],
expect.objectContaining({
env: expect.objectContaining({
PATH: "/usr/bin",