test(qa): consolidate gateway child fixtures (#118508)

This commit is contained in:
Peter Steinberger
2026-08-02 22:37:55 -07:00
committed by GitHub
parent f153cf3a92
commit f6341f2fba
+147 -356
View File
@@ -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<void>> = [];
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<void> {
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"), "<html></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({