diff --git a/extensions/qa-lab/src/gateway-child.test.ts b/extensions/qa-lab/src/gateway-child.test.ts index b1a550863386..ae6787aa604a 100644 --- a/extensions/qa-lab/src/gateway-child.test.ts +++ b/extensions/qa-lab/src/gateway-child.test.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; // Qa Lab tests cover gateway child plugin behavior. import { EventEmitter, once } from "node:events"; -import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; +import { lstat, mkdir, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Writable } from "node:stream"; @@ -35,16 +35,12 @@ vi.mock("./node-exec.js", () => ({ resolveQaNodeExecPath: resolveQaNodeExecPathMock, })); -const cleanups: Array<() => Promise> = []; const tempDirs = createTempDirHarness(); afterEach(async () => { fetchWithSsrFGuardMock.mockReset(); resolveQaNodeExecPathMock.mockReset(); qaTempPathState.preferredTmpDir = process.env.TMPDIR || "/tmp"; - while (cleanups.length > 0) { - await cleanups.pop()?.(); - } await tempDirs.cleanup(); }); @@ -118,6 +114,17 @@ async function expectPathMissing(filePath: string): Promise { throw new Error(`expected ${filePath} to be missing`); } +async function writeJsonFixture(filePath: string, value: unknown, space?: number) { + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, JSON.stringify(value, null, space), "utf8"); +} + +async function writeTempProviderConfig(value: unknown) { + const configPath = path.join(await tempDirs.makeTempDir("qa-provider-config-"), "openclaw.json"); + await writeJsonFixture(configPath, value); + return configPath; +} + describe("runQaGatewayCliCommand", () => { it("runs CLI commands with the Gateway fixture environment", async () => { const output = await testing.runQaGatewayCliCommand({ @@ -396,10 +403,7 @@ describe("Gateway child fixture helpers", () => { describe("buildQaRuntimeEnv", () => { it("cleans up temp QA gateway roots when node path resolution fails before startup", async () => { - const tempParent = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-node-exec-fail-")); - cleanups.push(async () => { - await rm(tempParent, { recursive: true, force: true }); - }); + const tempParent = await tempDirs.makeTempDir("qa-gateway-node-exec-fail-"); qaTempPathState.preferredTmpDir = tempParent; resolveQaNodeExecPathMock.mockRejectedValueOnce(new Error("node missing")); @@ -418,16 +422,8 @@ describe("buildQaRuntimeEnv", () => { }); it("reports command spawn errors instead of leaking unhandled child errors", async () => { - const preferredTempParent = await mkdtemp( - path.join(os.tmpdir(), "qa-gateway-default-spawn-fail-"), - ); - const commandTempParent = await mkdtemp( - path.join(os.tmpdir(), "qa-gateway-command-spawn-fail-"), - ); - cleanups.push(async () => { - await rm(preferredTempParent, { recursive: true, force: true }); - await rm(commandTempParent, { recursive: true, force: true }); - }); + const preferredTempParent = await tempDirs.makeTempDir("qa-gateway-default-spawn-fail-"); + const commandTempParent = await tempDirs.makeTempDir("qa-gateway-command-spawn-fail-"); qaTempPathState.preferredTmpDir = preferredTempParent; const missingExecutable = path.join(commandTempParent, "missing-openclaw-node"); @@ -505,10 +501,7 @@ describe("buildQaRuntimeEnv", () => { }); it("preserves Codex CLI auth home for live frontier runs while sandboxing OpenClaw home", async () => { - const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-")); - cleanups.push(async () => { - await rm(hostHome, { recursive: true, force: true }); - }); + const hostHome = await tempDirs.makeTempDir("qa-host-home-"); const codexHome = path.join(hostHome, ".codex"); await mkdir(codexHome); @@ -525,10 +518,7 @@ describe("buildQaRuntimeEnv", () => { }); it("forwards host HOME for live Claude CLI runs while keeping OpenClaw home sandboxed", async () => { - const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-")); - cleanups.push(async () => { - await rm(hostHome, { recursive: true, force: true }); - }); + const hostHome = await tempDirs.makeTempDir("qa-host-home-"); const env = buildQaRuntimeEnv({ ...createParams({ @@ -544,10 +534,7 @@ describe("buildQaRuntimeEnv", () => { }); it("can forward host HOME for browser-backed QA runs while keeping OpenClaw home sandboxed", async () => { - const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-")); - cleanups.push(async () => { - await rm(hostHome, { recursive: true, force: true }); - }); + const hostHome = await tempDirs.makeTempDir("qa-host-home-"); const env = buildQaRuntimeEnv({ ...createParams({ @@ -563,10 +550,7 @@ describe("buildQaRuntimeEnv", () => { }); it("preserves the live Anthropic key for live Claude CLI runs without writing it into config", async () => { - const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-")); - cleanups.push(async () => { - await rm(hostHome, { recursive: true, force: true }); - }); + const hostHome = await tempDirs.makeTempDir("qa-host-home-"); const env = buildQaRuntimeEnv({ ...createParams({ @@ -585,10 +569,7 @@ describe("buildQaRuntimeEnv", () => { }); it("removes preserved Anthropic keys for live Claude CLI subscription runs", async () => { - const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-")); - cleanups.push(async () => { - await rm(hostHome, { recursive: true, force: true }); - }); + const hostHome = await tempDirs.makeTempDir("qa-host-home-"); const env = buildQaRuntimeEnv({ ...createParams({ @@ -666,10 +647,7 @@ describe("buildQaRuntimeEnv", () => { }); it("re-scrubs blocked credentials in the spawned gateway child env", async () => { - const tempParent = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-env-scrub-")); - cleanups.push(async () => { - await rm(tempParent, { recursive: true, force: true }); - }); + const tempParent = await tempDirs.makeTempDir("qa-gateway-env-scrub-"); qaTempPathState.preferredTmpDir = tempParent; const observedEnvPath = path.join(tempParent, "observed-env.json"); const captureScript = [ @@ -719,10 +697,7 @@ describe("buildQaRuntimeEnv", () => { }); it("requires an Anthropic key for live Claude CLI API-key mode", async () => { - const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-")); - cleanups.push(async () => { - await rm(hostHome, { recursive: true, force: true }); - }); + const hostHome = await tempDirs.makeTempDir("qa-host-home-"); expect(() => buildQaRuntimeEnv({ @@ -852,10 +827,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages a live Anthropic setup-token profile for isolated QA workers", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-setup-token-state-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-setup-token-state-"); const token = `sk-ant-oat01-${"c".repeat(80)}`; const cfg = await testing.stageQaLiveAnthropicSetupToken({ @@ -879,10 +851,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages live env API-key profiles for isolated QA workers", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-live-api-key-state-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-live-api-key-state-"); const cfg = await testing.stageQaLiveApiKeyProfiles({ cfg: {}, @@ -910,10 +879,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages the OpenAI API-key fallback for live OpenAI QA workers", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-live-codex-api-key-state-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-live-codex-api-key-state-"); const cfg = await testing.stageQaLiveApiKeyProfiles({ cfg: {}, @@ -948,10 +914,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages direct live OpenAI API-key aliases for isolated QA workers", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-live-codex-direct-key-state-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-live-codex-direct-key-state-"); const cfg = await testing.stageQaLiveApiKeyProfiles({ cfg: {}, @@ -1059,10 +1022,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages configured OpenAI API keys for live QA runs", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-live-codex-config-key-state-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-live-codex-config-key-state-"); const cfg = await testing.stageQaLiveApiKeyProfiles({ cfg: { models: { @@ -1104,10 +1064,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages configured OpenAI env secret refs for default OpenAI live QA runs", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-live-codex-config-ref-state-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-live-codex-config-ref-state-"); const env = { OPENCLAW_LIVE_CODEX_API_KEY: "qa-configured-env-ref-not-a-real-key", }; @@ -1151,10 +1108,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages configured OpenAI env markers for live QA runs", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-live-codex-config-marker-state-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-live-codex-config-marker-state-"); const cfg = await testing.stageQaLiveApiKeyProfiles({ cfg: { models: { @@ -1219,10 +1173,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages placeholder mock auth profiles per agent dir so mock-openai runs can resolve credentials", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-mock-auth-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-mock-auth-"); const cfg = await testing.stageQaMockAuthProfiles({ cfg: {}, @@ -1256,10 +1207,7 @@ describe("buildQaRuntimeEnv", () => { }); it("stages mock profiles only for the requested agents and providers when callers override the defaults", async () => { - const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-mock-auth-override-")); - cleanups.push(async () => { - await rm(stateDir, { recursive: true, force: true }); - }); + const stateDir = await tempDirs.makeTempDir("qa-mock-auth-override-"); const cfg = await testing.stageQaMockAuthProfiles({ cfg: {}, @@ -1564,12 +1512,8 @@ describe("buildQaRuntimeEnv", () => { }); it("preserves only sanitized gateway debug artifacts", async () => { - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-preserve-src-")); - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-preserve-repo-")); - cleanups.push(async () => { - await rm(tempRoot, { recursive: true, force: true }); - await rm(repoRoot, { recursive: true, force: true }); - }); + const tempRoot = await tempDirs.makeTempDir("qa-gateway-preserve-src-"); + const repoRoot = await tempDirs.makeTempDir("qa-gateway-preserve-repo-"); const stdoutLogPath = path.join(tempRoot, "gateway.stdout.log"); const stderrLogPath = path.join(tempRoot, "gateway.stderr.log"); @@ -1662,12 +1606,8 @@ describe("buildQaRuntimeEnv", () => { }); it("rejects preserved gateway artifacts that traverse symlinks", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-guard-repo-")); - const outsideRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-guard-outside-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - await rm(outsideRoot, { recursive: true, force: true }); - }); + const repoRoot = await tempDirs.makeTempDir("qa-gateway-guard-repo-"); + const outsideRoot = await tempDirs.makeTempDir("qa-gateway-guard-outside-"); await mkdir(path.join(repoRoot, ".artifacts"), { recursive: true }); await symlink(outsideRoot, path.join(repoRoot, ".artifacts", "qa-e2e"), "dir"); @@ -1680,12 +1620,8 @@ describe("buildQaRuntimeEnv", () => { }); it("cleans startup temp roots when they are not preserved", async () => { - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-cleanup-src-")); - const stagedRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-cleanup-stage-")); - cleanups.push(async () => { - await rm(tempRoot, { recursive: true, force: true }); - await rm(stagedRoot, { recursive: true, force: true }); - }); + const tempRoot = await tempDirs.makeTempDir("qa-gateway-cleanup-src-"); + const stagedRoot = await tempDirs.makeTempDir("qa-gateway-cleanup-stage-"); await writeFile(path.join(tempRoot, "openclaw.json"), "{}", "utf8"); await writeFile(path.join(stagedRoot, "marker.txt"), "x", "utf8"); @@ -1702,10 +1638,7 @@ describe("buildQaRuntimeEnv", () => { describe("resolveQaControlUiRoot", () => { it("returns the built control ui root when repo assets exist", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-control-ui-root-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); + const repoRoot = await tempDirs.makeTempDir("qa-control-ui-root-"); const controlUiRoot = path.join(repoRoot, "dist", "control-ui"); await mkdir(controlUiRoot, { recursive: true }); await writeFile(path.join(controlUiRoot, "index.html"), "", "utf8"); @@ -1714,10 +1647,7 @@ describe("resolveQaControlUiRoot", () => { }); it("returns undefined when control ui is disabled or not built", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-control-ui-root-missing-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); + const repoRoot = await tempDirs.makeTempDir("qa-control-ui-root-missing-"); expect(resolveQaControlUiRoot({ repoRoot })).toBeUndefined(); expect(resolveQaControlUiRoot({ repoRoot, controlUiEnabled: false })).toBeUndefined(); @@ -1726,28 +1656,16 @@ describe("resolveQaControlUiRoot", () => { describe("qa bundled plugin dir", () => { it("prefers a built bundled plugin when present", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-root-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - await mkdir(path.join(repoRoot, "dist", "extensions", "qa-channel"), { - recursive: true, - }); - await writeFile( + const repoRoot = await tempDirs.makeTempDir("qa-bundled-root-"); + await writeJsonFixture( path.join(repoRoot, "dist", "extensions", "qa-channel", "package.json"), - "{}", - "utf8", + {}, ); - await mkdir(path.join(repoRoot, "dist-runtime", "extensions", "qa-channel"), { - recursive: true, - }); - await writeFile( + await writeJsonFixture( path.join(repoRoot, "dist-runtime", "extensions", "qa-channel", "package.json"), - "{}", - "utf8", + {}, ); - await mkdir(path.join(repoRoot, "extensions", "qa-channel"), { recursive: true }); - await writeFile(path.join(repoRoot, "extensions", "qa-channel", "package.json"), "{}", "utf8"); + await writeJsonFixture(path.join(repoRoot, "extensions", "qa-channel", "package.json"), {}); expect( testing.resolveQaBundledPluginSourceDir({ @@ -1758,12 +1676,8 @@ describe("qa bundled plugin dir", () => { }); it("falls back to the source bundled plugin when no built copy exists", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-root-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - await mkdir(path.join(repoRoot, "extensions", "qa-channel"), { recursive: true }); - await writeFile(path.join(repoRoot, "extensions", "qa-channel", "package.json"), "{}", "utf8"); + const repoRoot = await tempDirs.makeTempDir("qa-bundled-source-root-"); + await writeJsonFixture(path.join(repoRoot, "extensions", "qa-channel", "package.json"), {}); expect( testing.resolveQaBundledPluginSourceDir({ @@ -1774,22 +1688,14 @@ describe("qa bundled plugin dir", () => { }); it("resolves bundled plugins by manifest id when the directory name differs", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-manifest-id-root-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - await mkdir(path.join(repoRoot, "dist", "extensions", "kimi-coding"), { - recursive: true, - }); - await writeFile( + const repoRoot = await tempDirs.makeTempDir("qa-bundled-manifest-id-root-"); + await writeJsonFixture( path.join(repoRoot, "dist", "extensions", "kimi-coding", "openclaw.plugin.json"), - JSON.stringify({ id: "kimi", providers: ["kimi"] }), - "utf8", + { id: "kimi", providers: ["kimi"] }, ); - await writeFile( + await writeJsonFixture( path.join(repoRoot, "dist", "extensions", "kimi-coding", "package.json"), - "{}", - "utf8", + {}, ); expect( @@ -1801,27 +1707,19 @@ describe("qa bundled plugin dir", () => { }); it("uses a source bundled plugin when the built copy is missing CLI metadata", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-cli-metadata-root-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - await mkdir(path.join(repoRoot, "dist", "extensions", "memory-core"), { recursive: true }); - await writeFile( + const repoRoot = await tempDirs.makeTempDir("qa-bundled-cli-metadata-root-"); + await writeJsonFixture( path.join(repoRoot, "dist", "extensions", "memory-core", "package.json"), - "{}", - "utf8", + {}, ); - await writeFile( + await writeJsonFixture( path.join(repoRoot, "dist", "extensions", "memory-core", "openclaw.plugin.json"), - JSON.stringify({ id: "memory-core", kind: "memory" }), - "utf8", + { id: "memory-core", kind: "memory" }, ); - await mkdir(path.join(repoRoot, "extensions", "memory-core"), { recursive: true }); - await writeFile(path.join(repoRoot, "extensions", "memory-core", "package.json"), "{}", "utf8"); - await writeFile( + await writeJsonFixture(path.join(repoRoot, "extensions", "memory-core", "package.json"), {}); + await writeJsonFixture( path.join(repoRoot, "extensions", "memory-core", "openclaw.plugin.json"), - JSON.stringify({ id: "memory-core", kind: "memory" }), - "utf8", + { id: "memory-core", kind: "memory" }, ); await writeFile( path.join(repoRoot, "extensions", "memory-core", "cli-metadata.ts"), @@ -1838,10 +1736,7 @@ describe("qa bundled plugin dir", () => { }); it("creates a scoped bundled plugin tree for allowed plugins plus always-allowed runtime facades", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-scope-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); + const repoRoot = await tempDirs.makeTempDir("qa-bundled-scope-"); await writeFile( path.join(repoRoot, "package.json"), JSON.stringify( @@ -1895,10 +1790,7 @@ describe("qa bundled plugin dir", () => { "utf8", ); await writeFile(path.join(repoRoot, "dist", "shared-chunk-abc123.js"), "export {};\n", "utf8"); - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-target-")); - cleanups.push(async () => { - await rm(tempRoot, { recursive: true, force: true }); - }); + const tempRoot = await tempDirs.makeTempDir("qa-bundled-target-"); const { bundledPluginsDir, stagedRoot } = await testing.createQaBundledPluginsDir({ repoRoot, @@ -1957,10 +1849,7 @@ describe("qa bundled plugin dir", () => { }); it("preserves dist-runtime-only root chunks when dist also exists", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-mixed-runtime-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); + const repoRoot = await tempDirs.makeTempDir("qa-bundled-mixed-runtime-"); await writeFile( path.join(repoRoot, "package.json"), JSON.stringify({ name: "openclaw", type: "module" }, null, 2), @@ -1990,10 +1879,7 @@ describe("qa bundled plugin dir", () => { ['import { marker } from "../../runtime-chunk.js";', "export { marker };", ""].join("\n"), "utf8", ); - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-mixed-target-")); - cleanups.push(async () => { - await rm(tempRoot, { recursive: true, force: true }); - }); + const tempRoot = await tempDirs.makeTempDir("qa-bundled-mixed-target-"); const { bundledPluginsDir } = await testing.createQaBundledPluginsDir({ repoRoot, @@ -2033,19 +1919,13 @@ describe("qa bundled plugin dir", () => { }); it("rejects invalid bundled plugin ids before staging paths are built", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-invalid-id-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); + const repoRoot = await tempDirs.makeTempDir("qa-bundled-invalid-id-"); await writeFile( path.join(repoRoot, "package.json"), JSON.stringify({ name: "openclaw", type: "module" }, null, 2), "utf8", ); - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-invalid-target-")); - cleanups.push(async () => { - await rm(tempRoot, { recursive: true, force: true }); - }); + const tempRoot = await tempDirs.makeTempDir("qa-bundled-invalid-target-"); await expect( testing.createQaBundledPluginsDir({ @@ -2075,14 +1955,8 @@ describe("qa bundled plugin dir", () => { }); it("stages source-only bundled plugins into a repo-like runtime root with node_modules", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-stage-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - const fakeDepStoreRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-store-")); - cleanups.push(async () => { - await rm(fakeDepStoreRoot, { recursive: true, force: true }); - }); + const repoRoot = await tempDirs.makeTempDir("qa-bundled-source-stage-"); + const fakeDepStoreRoot = await tempDirs.makeTempDir("qa-bundled-source-store-"); await writeFile( path.join(repoRoot, "package.json"), JSON.stringify( @@ -2136,10 +2010,7 @@ describe("qa bundled plugin dir", () => { ); await mkdir(path.join(repoRoot, "node_modules"), { recursive: true }); await symlink(fakeDepPackageDir, path.join(repoRoot, "node_modules", "fake-dep"), "dir"); - const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-target-")); - cleanups.push(async () => { - await rm(tempRoot, { recursive: true, force: true }); - }); + const tempRoot = await tempDirs.makeTempDir("qa-bundled-source-target-"); const { bundledPluginsDir, stagedRoot } = await testing.createQaBundledPluginsDir({ repoRoot, @@ -2175,19 +2046,14 @@ describe("qa bundled plugin dir", () => { }); it("maps cli backend provider ids to their owning bundled plugin ids", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-plugin-owner-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - await mkdir(path.join(repoRoot, "dist", "extensions", "openai"), { recursive: true }); - await writeFile( + const repoRoot = await tempDirs.makeTempDir("qa-plugin-owner-"); + await writeJsonFixture( path.join(repoRoot, "dist", "extensions", "openai", "openclaw.plugin.json"), - JSON.stringify({ + { id: "openai", providers: ["openai", "openai"], cliBackends: ["codex-cli"], - }), - "utf8", + }, ); await expect( @@ -2199,19 +2065,14 @@ describe("qa bundled plugin dir", () => { }); it("maps configured OpenAI Responses provider aliases to the OpenAI plugin", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-plugin-owner-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - await mkdir(path.join(repoRoot, "dist", "extensions", "openai"), { recursive: true }); - await writeFile( + const repoRoot = await tempDirs.makeTempDir("qa-plugin-owner-"); + await writeJsonFixture( path.join(repoRoot, "dist", "extensions", "openai", "openclaw.plugin.json"), - JSON.stringify({ + { id: "openai", providers: ["openai"], cliBackends: ["codex-cli"], - }), - "utf8", + }, ); await expect( @@ -2241,44 +2102,33 @@ describe("qa bundled plugin dir", () => { }); it("copies selected live provider configs from the host config", async () => { - const configPath = path.join( - await mkdtemp(path.join(os.tmpdir(), "qa-provider-config-")), - "openclaw.json", - ); - cleanups.push(async () => { - await rm(path.dirname(configPath), { recursive: true, force: true }); - }); - await writeFile( - configPath, - JSON.stringify({ - models: { - providers: { - "custom-openai": { - baseUrl: "https://api.example.test/v1", - api: "openai-responses", - models: [ - { - id: "model-a", - name: "model-a", - api: "openai-responses", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128_000, - maxTokens: 4096, - }, - ], - }, - ignored: { - baseUrl: "https://ignored.example.test/v1", - api: "openai-responses", - models: [], - }, + const configPath = await writeTempProviderConfig({ + models: { + providers: { + "custom-openai": { + baseUrl: "https://api.example.test/v1", + api: "openai-responses", + models: [ + { + id: "model-a", + name: "model-a", + api: "openai-responses", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 4096, + }, + ], + }, + ignored: { + baseUrl: "https://ignored.example.test/v1", + api: "openai-responses", + models: [], }, }, - }), - "utf8", - ); + }, + }); const overrides = await testing.readQaLiveProviderConfigOverrides({ providerIds: ["custom-openai"], @@ -2290,29 +2140,18 @@ describe("qa bundled plugin dir", () => { }); it("copies OpenAI auth-only live provider configs for default OpenAI runs", async () => { - const configPath = path.join( - await mkdtemp(path.join(os.tmpdir(), "qa-provider-config-")), - "openclaw.json", - ); - cleanups.push(async () => { - await rm(path.dirname(configPath), { recursive: true, force: true }); - }); - await writeFile( - configPath, - JSON.stringify({ - models: { - providers: { - openai: { - apiKey: { - source: "env", - id: "OPENCLAW_LIVE_CODEX_API_KEY", - }, + const configPath = await writeTempProviderConfig({ + models: { + providers: { + openai: { + apiKey: { + source: "env", + id: "OPENCLAW_LIVE_CODEX_API_KEY", }, }, }, - }), - "utf8", - ); + }, + }); const overrides = await testing.readQaLiveProviderConfigOverrides({ providerIds: ["openai"], @@ -2328,28 +2167,17 @@ describe("qa bundled plugin dir", () => { }); it("omits empty base URLs without dropping provider configs that inherit auth", async () => { - const configPath = path.join( - await mkdtemp(path.join(os.tmpdir(), "qa-provider-config-")), - "openclaw.json", - ); - cleanups.push(async () => { - await rm(path.dirname(configPath), { recursive: true, force: true }); - }); - await writeFile( - configPath, - JSON.stringify({ - models: { - providers: { - openai: { - baseUrl: "", - api: "openai-responses", - models: [], - }, + const configPath = await writeTempProviderConfig({ + models: { + providers: { + openai: { + baseUrl: "", + api: "openai-responses", + models: [], }, }, - }), - "utf8", - ); + }, + }); const overrides = await testing.readQaLiveProviderConfigOverrides({ providerIds: ["openai"], @@ -2361,31 +2189,20 @@ describe("qa bundled plugin dir", () => { }); it("does not copy OpenAI provider configs for custom OpenAI-compatible runs", async () => { - const configPath = path.join( - await mkdtemp(path.join(os.tmpdir(), "qa-provider-config-")), - "openclaw.json", - ); - cleanups.push(async () => { - await rm(path.dirname(configPath), { recursive: true, force: true }); - }); - await writeFile( - configPath, - JSON.stringify({ - models: { - providers: { - openai: { - baseUrl: "https://proxy.example.test/v1", - models: [], - apiKey: { - source: "env", - id: "OPENCLAW_LIVE_CODEX_API_KEY", - }, + const configPath = await writeTempProviderConfig({ + models: { + providers: { + openai: { + baseUrl: "https://proxy.example.test/v1", + models: [], + apiKey: { + source: "env", + id: "OPENCLAW_LIVE_CODEX_API_KEY", }, }, }, - }), - "utf8", - ); + }, + }); const overrides = await testing.readQaLiveProviderConfigOverrides({ providerIds: ["openai"], @@ -2396,29 +2213,16 @@ describe("qa bundled plugin dir", () => { }); it("raises the QA runtime host version to the highest allowed plugin floor", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-runtime-version-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - await writeFile( - path.join(repoRoot, "package.json"), - JSON.stringify({ version: "2026.4.7-1" }), - "utf8", - ); + const repoRoot = await tempDirs.makeTempDir("qa-runtime-version-"); + await writeJsonFixture(path.join(repoRoot, "package.json"), { version: "2026.4.7-1" }); const bundledRoot = path.join(repoRoot, "extensions"); - await mkdir(path.join(bundledRoot, "qa-channel"), { recursive: true }); - await writeFile( - path.join(bundledRoot, "qa-channel", "package.json"), - JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.8" } } }), - "utf8", - ); + await writeJsonFixture(path.join(bundledRoot, "qa-channel", "package.json"), { + openclaw: { install: { minHostVersion: ">=2026.4.8" } }, + }); - await mkdir(path.join(bundledRoot, "memory-core"), { recursive: true }); - await writeFile( - path.join(bundledRoot, "memory-core", "package.json"), - JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.7" } } }), - "utf8", - ); + await writeJsonFixture(path.join(bundledRoot, "memory-core", "package.json"), { + openclaw: { install: { minHostVersion: ">=2026.4.7" } }, + }); await expect( testing.resolveQaRuntimeHostVersion({ @@ -2429,28 +2233,15 @@ describe("qa bundled plugin dir", () => { }); it("includes always-allowed runtime facade plugins when raising the QA runtime host version", async () => { - const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-runtime-version-runtime-facade-")); - cleanups.push(async () => { - await rm(repoRoot, { recursive: true, force: true }); - }); - await writeFile( - path.join(repoRoot, "package.json"), - JSON.stringify({ version: "2026.4.7-1" }), - "utf8", - ); + const repoRoot = await tempDirs.makeTempDir("qa-runtime-version-runtime-facade-"); + await writeJsonFixture(path.join(repoRoot, "package.json"), { version: "2026.4.7-1" }); const bundledRoot = path.join(repoRoot, "extensions"); - await mkdir(path.join(bundledRoot, "qa-channel"), { recursive: true }); - await writeFile( - path.join(bundledRoot, "qa-channel", "package.json"), - JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.8" } } }), - "utf8", - ); - await mkdir(path.join(bundledRoot, "image-generation-core"), { recursive: true }); - await writeFile( - path.join(bundledRoot, "image-generation-core", "package.json"), - JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.9" } } }), - "utf8", - ); + await writeJsonFixture(path.join(bundledRoot, "qa-channel", "package.json"), { + openclaw: { install: { minHostVersion: ">=2026.4.8" } }, + }); + await writeJsonFixture(path.join(bundledRoot, "image-generation-core", "package.json"), { + openclaw: { install: { minHostVersion: ">=2026.4.9" } }, + }); await expect( testing.resolveQaRuntimeHostVersion({