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

View File

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

View File

@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises"; import { setTimeout as sleep } from "node:timers/promises";
import { afterEach, describe, expect, it, vi } from "vitest"; 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")); 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>> = []; const cleanups: Array<() => Promise<void>> = [];
async function startQaLabServerForTest(params?: QaLabServerStartParams) {
return await startQaLabServer({
embeddedGateway: "disabled",
...params,
});
}
afterEach(async () => { afterEach(async () => {
captureMock.reset(); captureMock.reset();
while (cleanups.length > 0) { while (cleanups.length > 0) {
@@ -241,7 +248,7 @@ async function createQaLabRepoRootFixture(params?: {
} }
describe("qa-lab server", () => { 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-")); const tempDir = await mkdtemp(path.join(os.tmpdir(), "qa-lab-test-"));
cleanups.push(async () => { cleanups.push(async () => {
await rm(tempDir, { recursive: true, force: true }); await rm(tempDir, { recursive: true, force: true });
@@ -249,13 +256,14 @@ describe("qa-lab server", () => {
const outputPath = path.join(tempDir, "self-check.md"); const outputPath = path.join(tempDir, "self-check.md");
const repoRoot = await createQaLabRepoRootFixture(); const repoRoot = await createQaLabRepoRootFixture();
const lab = await startQaLabServer({ const lab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
outputPath, outputPath,
repoRoot, repoRoot,
controlUiUrl: "http://127.0.0.1:18789/", controlUiUrl: "http://127.0.0.1:18789/",
controlUiToken: "qa-token", controlUiToken: "qa-token",
embeddedGateway: "disabled",
}); });
cleanups.push(async () => { cleanups.push(async () => {
await lab.stop(); await lab.stop();
@@ -303,11 +311,7 @@ describe("qa-lab server", () => {
}; };
expect(snapshot.messages.some((message) => message.text === "hello from test")).toBe(true); expect(snapshot.messages.some((message) => message.text === "hello from test")).toBe(true);
const result = await lab.runSelfCheck(); await expect(readFile(outputPath, "utf8")).rejects.toThrow();
expect(result.scenarioResult.status).toBe("pass");
const markdown = await readFile(outputPath, "utf8");
expect(markdown).toContain("Synthetic Slack-class roundtrip");
expect(markdown).toContain("- Status: pass");
}); });
it("anchors direct self-check runs under the explicit repo root by default", async () => { 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 }); await rm(repoRoot, { recursive: true, force: true });
}); });
const lab = await startQaLabServer({ const lab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
repoRoot, repoRoot,
embeddedGateway: "disabled",
}); });
cleanups.push(async () => { cleanups.push(async () => {
await lab.stop(); await lab.stop();
@@ -331,9 +336,10 @@ describe("qa-lab server", () => {
}); });
it("injects the kickoff task on demand and on startup", async () => { it("injects the kickoff task on demand and on startup", async () => {
const autoKickoffLab = await startQaLabServer({ const autoKickoffLab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
embeddedGateway: "disabled",
sendKickoffOnStart: true, sendKickoffOnStart: true,
}); });
cleanups.push(async () => { cleanups.push(async () => {
@@ -349,9 +355,10 @@ describe("qa-lab server", () => {
true, true,
); );
const manualLab = await startQaLabServer({ const manualLab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
embeddedGateway: "disabled",
}); });
cleanups.push(async () => { cleanups.push(async () => {
await manualLab.stop(); await manualLab.stop();
@@ -402,7 +409,7 @@ describe("qa-lab server", () => {
throw new Error("expected upstream address"); throw new Error("expected upstream address");
} }
const lab = await startQaLabServer({ const lab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
advertiseHost: "127.0.0.1", advertiseHost: "127.0.0.1",
@@ -445,7 +452,7 @@ describe("qa-lab server", () => {
"utf8", "utf8",
); );
const lab = await startQaLabServer({ const lab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
uiDistDir, 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>", "<!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", host: "127.0.0.1",
port: 0, port: 0,
repoRoot, repoRoot,
@@ -530,7 +537,7 @@ describe("qa-lab server", () => {
"utf8", "utf8",
); );
const lab = await startQaLabServer({ const lab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
repoRoot, repoRoot,
@@ -579,7 +586,7 @@ describe("qa-lab server", () => {
"utf8", "utf8",
); );
const lab = await startQaLabServer({ const lab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
repoRoot, repoRoot,
@@ -597,11 +604,13 @@ describe("qa-lab server", () => {
await lab.stop(); await lab.stop();
stopped = true; 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 () => { 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", host: "127.0.0.1",
port: 0, port: 0,
embeddedGateway: "disabled", embeddedGateway: "disabled",
@@ -630,7 +639,7 @@ describe("qa-lab server", () => {
}); });
it("exposes structured outcomes and can attach control-ui after startup", async () => { 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", host: "127.0.0.1",
port: 0, port: 0,
embeddedGateway: "disabled", embeddedGateway: "disabled",
@@ -776,7 +785,7 @@ describe("qa-lab server", () => {
}), }),
}); });
const lab = await startQaLabServer({ const lab = await startQaLabServerForTest({
host: "127.0.0.1", host: "127.0.0.1",
port: 0, port: 0,
}); });

View File

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

View File

@@ -80,7 +80,17 @@ function killProcessTree(pid: number | undefined, signal: NodeJS.Signals) {
} }
try { try {
if (process.platform === "win32") { 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; return;
} }
process.kill(-pid, signal); process.kill(-pid, signal);

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const { defaultQaRuntimeModelForMode } = vi.hoisted(() => ({ const { defaultQaRuntimeModelForMode } = vi.hoisted(() => ({
@@ -131,8 +132,9 @@ describe("qa run config", () => {
}); });
it("anchors generated run output dirs under the provided repo root", () => { it("anchors generated run output dirs under the provided repo root", () => {
const outputDir = createQaRunOutputDir("/tmp/openclaw-repo"); const repoRoot = path.resolve("/tmp/openclaw-repo");
expect(outputDir.startsWith("/tmp/openclaw-repo/.artifacts/qa-e2e/lab-")).toBe(true); 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", () => { 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 { describe, expect, it } from "vitest";
import { resolveQaSelfCheckOutputPath } from "./self-check.js"; import { resolveQaSelfCheckOutputPath } from "./self-check.js";
@@ -12,8 +13,9 @@ describe("resolveQaSelfCheckOutputPath", () => {
}); });
it("anchors default self-check reports under the provided repo root", () => { it("anchors default self-check reports under the provided repo root", () => {
expect(resolveQaSelfCheckOutputPath({ repoRoot: "/tmp/openclaw-repo" })).toBe( const repoRoot = path.resolve("/tmp/openclaw-repo");
"/tmp/openclaw-repo/.artifacts/qa-e2e/self-check.md", 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 { EventEmitter } from "node:events";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const spawnMock = vi.hoisted(() => vi.fn()); const spawnMock = vi.hoisted(() => vi.fn());
@@ -95,7 +96,7 @@ describe("qa suite runtime agent process helpers", () => {
await expect(pending).resolves.toBe("ok"); await expect(pending).resolves.toBe("ok");
expect(spawnMock).toHaveBeenCalledWith( expect(spawnMock).toHaveBeenCalledWith(
"/usr/bin/node", "/usr/bin/node",
["/repo/dist/index.js", "qa", "suite"], [path.join("/repo", "dist", "index.js"), "qa", "suite"],
expect.objectContaining({ expect.objectContaining({
cwd: "/tmp/runtime", cwd: "/tmp/runtime",
env: { PATH: "/usr/bin" }, env: { PATH: "/usr/bin" },
@@ -134,7 +135,7 @@ describe("qa suite runtime agent process helpers", () => {
await expect(pending).resolves.toBe("ok"); await expect(pending).resolves.toBe("ok");
expect(spawnMock).toHaveBeenCalledWith( expect(spawnMock).toHaveBeenCalledWith(
"/usr/bin/node", "/usr/bin/node",
["/repo/dist/index.js", "crestodian", "-m", "overview"], [path.join("/repo", "dist", "index.js"), "crestodian", "-m", "overview"],
expect.objectContaining({ expect.objectContaining({
env: expect.objectContaining({ env: expect.objectContaining({
PATH: "/usr/bin", PATH: "/usr/bin",