perf(test): cut setup/import overhead in hot suites

This commit is contained in:
Peter Steinberger
2026-02-13 21:23:44 +00:00
parent 93dd51bce0
commit caebe70e9a
16 changed files with 428 additions and 426 deletions

View File

@@ -1,9 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as ssrf from "../../infra/net/ssrf.js";
import * as logger from "../../logger.js";
import { createWebFetchTool } from "./web-tools.js";
const lookupMock = vi.fn();
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
const baseToolConfig = {
config: {
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
},
} as const;
function makeHeaders(map: Record<string, string>): { get: (key: string) => string | null } {
return {
@@ -51,12 +57,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
// @ts-expect-error mock fetch
global.fetch = fetchSpy;
const { createWebFetchTool } = await import("./web-tools.js");
const tool = createWebFetchTool({
config: {
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
},
});
const tool = createWebFetchTool(baseToolConfig);
await tool?.execute?.("call", { url: "https://example.com/page" });
@@ -71,12 +72,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
// @ts-expect-error mock fetch
global.fetch = fetchSpy;
const { createWebFetchTool } = await import("./web-tools.js");
const tool = createWebFetchTool({
config: {
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
},
});
const tool = createWebFetchTool(baseToolConfig);
const result = await tool?.execute?.("call", { url: "https://example.com/cf" });
expect(result?.details).toMatchObject({
@@ -96,12 +92,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
// @ts-expect-error mock fetch
global.fetch = fetchSpy;
const { createWebFetchTool } = await import("./web-tools.js");
const tool = createWebFetchTool({
config: {
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
},
});
const tool = createWebFetchTool(baseToolConfig);
const result = await tool?.execute?.("call", { url: "https://example.com/html" });
expect(result?.details?.extractor).not.toBe("cf-markdown");
@@ -116,12 +107,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
// @ts-expect-error mock fetch
global.fetch = fetchSpy;
const { createWebFetchTool } = await import("./web-tools.js");
const tool = createWebFetchTool({
config: {
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
},
});
const tool = createWebFetchTool(baseToolConfig);
await tool?.execute?.("call", { url: "https://example.com/tokens/private?token=secret" });
@@ -142,12 +128,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
// @ts-expect-error mock fetch
global.fetch = fetchSpy;
const { createWebFetchTool } = await import("./web-tools.js");
const tool = createWebFetchTool({
config: {
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
},
});
const tool = createWebFetchTool(baseToolConfig);
const result = await tool?.execute?.("call", {
url: "https://example.com/text-mode",
@@ -169,12 +150,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
// @ts-expect-error mock fetch
global.fetch = fetchSpy;
const { createWebFetchTool } = await import("./web-tools.js");
const tool = createWebFetchTool({
config: {
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
},
});
const tool = createWebFetchTool(baseToolConfig);
await tool?.execute?.("call", { url: "https://example.com/no-tokens" });

View File

@@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({
}));
vi.mock("./pw-session.js", () => sessionMocks);
async function importModule() {
return await import("./pw-tools-core.js");
}
const mod = await import("./pw-tools-core.js");
describe("pw-tools-core", () => {
beforeEach(() => {
@@ -53,7 +50,6 @@ describe("pw-tools-core", () => {
currentRefLocator = { scrollIntoViewIfNeeded };
currentPage = {};
const mod = await importModule();
await mod.scrollIntoViewViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
@@ -70,7 +66,6 @@ describe("pw-tools-core", () => {
currentRefLocator = { scrollIntoViewIfNeeded };
currentPage = {};
const mod = await importModule();
await expect(
mod.scrollIntoViewViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
@@ -86,7 +81,6 @@ describe("pw-tools-core", () => {
currentRefLocator = { scrollIntoViewIfNeeded };
currentPage = {};
const mod = await importModule();
await expect(
mod.scrollIntoViewViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
@@ -102,7 +96,6 @@ describe("pw-tools-core", () => {
currentRefLocator = { click };
currentPage = {};
const mod = await importModule();
await expect(
mod.clickViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
@@ -118,7 +111,6 @@ describe("pw-tools-core", () => {
currentRefLocator = { click };
currentPage = {};
const mod = await importModule();
await expect(
mod.clickViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
@@ -136,7 +128,6 @@ describe("pw-tools-core", () => {
currentRefLocator = { click };
currentPage = {};
const mod = await importModule();
await expect(
mod.clickViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",

View File

@@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({
}));
vi.mock("./pw-session.js", () => sessionMocks);
async function importModule() {
return await import("./pw-tools-core.js");
}
const mod = await import("./pw-tools-core.js");
describe("pw-tools-core", () => {
beforeEach(() => {
@@ -75,7 +72,6 @@ describe("pw-tools-core", () => {
keyboard: { press: vi.fn(async () => {}) },
};
const mod = await importModule();
await mod.armFileUploadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
paths: ["/tmp/1"],
@@ -101,7 +97,6 @@ describe("pw-tools-core", () => {
waitForEvent,
};
const mod = await importModule();
await mod.armDialogViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
accept: true,
@@ -145,7 +140,6 @@ describe("pw-tools-core", () => {
getByText: vi.fn(() => ({ first: () => ({ waitFor: vi.fn() }) })),
};
const mod = await importModule();
await mod.waitForViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
selector: "#main",

View File

@@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({
}));
vi.mock("./pw-session.js", () => sessionMocks);
async function importModule() {
return await import("./pw-tools-core.js");
}
const mod = await import("./pw-tools-core.js");
describe("pw-tools-core", () => {
beforeEach(() => {
@@ -57,7 +54,6 @@ describe("pw-tools-core", () => {
screenshot: vi.fn(async () => Buffer.from("P")),
};
const mod = await importModule();
const res = await mod.takeScreenshotViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
@@ -78,7 +74,6 @@ describe("pw-tools-core", () => {
screenshot: vi.fn(async () => Buffer.from("P")),
};
const mod = await importModule();
const res = await mod.takeScreenshotViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
@@ -99,8 +94,6 @@ describe("pw-tools-core", () => {
screenshot: vi.fn(async () => Buffer.from("P")),
};
const mod = await importModule();
await expect(
mod.takeScreenshotViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
@@ -127,7 +120,6 @@ describe("pw-tools-core", () => {
keyboard: { press: vi.fn(async () => {}) },
};
const mod = await importModule();
await mod.armFileUploadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
@@ -151,7 +143,6 @@ describe("pw-tools-core", () => {
keyboard: { press },
};
const mod = await importModule();
await mod.armFileUploadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
paths: [],

View File

@@ -33,10 +33,7 @@ const tmpDirMocks = vi.hoisted(() => ({
resolvePreferredOpenClawTmpDir: vi.fn(() => "/tmp/openclaw"),
}));
vi.mock("../infra/tmp-openclaw-dir.js", () => tmpDirMocks);
async function importModule() {
return await import("./pw-tools-core.js");
}
const mod = await import("./pw-tools-core.js");
describe("pw-tools-core", () => {
beforeEach(() => {
@@ -75,7 +72,6 @@ describe("pw-tools-core", () => {
currentPage = { on, off };
const mod = await importModule();
const targetPath = path.resolve("/tmp/file.bin");
const p = mod.waitForDownloadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
@@ -113,7 +109,6 @@ describe("pw-tools-core", () => {
currentPage = { on, off };
const mod = await importModule();
const targetPath = path.resolve("/tmp/report.pdf");
const p = mod.downloadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
@@ -152,7 +147,6 @@ describe("pw-tools-core", () => {
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred");
currentPage = { on, off };
const mod = await importModule();
const p = mod.waitForDownloadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
@@ -194,7 +188,6 @@ describe("pw-tools-core", () => {
text: async () => '{"ok":true,"value":123}',
};
const mod = await importModule();
const p = mod.responseBodyViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
@@ -218,7 +211,6 @@ describe("pw-tools-core", () => {
currentRefLocator = { scrollIntoViewIfNeeded };
currentPage = {};
const mod = await importModule();
await mod.scrollIntoViewViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
@@ -232,7 +224,6 @@ describe("pw-tools-core", () => {
currentRefLocator = { scrollIntoViewIfNeeded: vi.fn(async () => {}) };
currentPage = {};
const mod = await importModule();
await expect(
mod.scrollIntoViewViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",

View File

@@ -154,6 +154,9 @@ vi.mock("./screenshot.js", () => ({
})),
}));
const { startBrowserControlServerFromConfig, stopBrowserControlServer } =
await import("./server.js");
async function getFreePort(): Promise<number> {
while (true) {
const port = await new Promise<number>((resolve, reject) => {
@@ -271,12 +274,10 @@ describe("browser control server", () => {
} else {
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
}
const { stopBrowserControlServer } = await import("./server.js");
await stopBrowserControlServer();
});
const startServerAndBase = async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());

View File

@@ -63,6 +63,9 @@ vi.mock("./server-context.js", async (importOriginal) => {
};
});
const { startBrowserControlServerFromConfig, stopBrowserControlServer } =
await import("./server.js");
async function getFreePort(): Promise<number> {
const probe = createServer();
await new Promise<void>((resolve, reject) => {
@@ -95,12 +98,10 @@ describe("browser control evaluate gating", () => {
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
}
const { stopBrowserControlServer } = await import("./server.js");
await stopBrowserControlServer();
});
it("blocks act:evaluate but still allows cookies/storage reads", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;

View File

@@ -153,6 +153,9 @@ vi.mock("./screenshot.js", () => ({
})),
}));
const { startBrowserControlServerFromConfig, stopBrowserControlServer } =
await import("./server.js");
async function getFreePort(): Promise<number> {
while (true) {
const port = await new Promise<number>((resolve, reject) => {
@@ -270,12 +273,10 @@ describe("browser control server", () => {
} else {
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
}
const { stopBrowserControlServer } = await import("./server.js");
await stopBrowserControlServer();
});
it("POST /tabs/open?profile=unknown returns 404", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
@@ -307,9 +308,6 @@ describe("profile CRUD endpoints", () => {
prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT;
process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2);
prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT;
process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2);
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
@@ -330,12 +328,10 @@ describe("profile CRUD endpoints", () => {
} else {
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
}
const { stopBrowserControlServer } = await import("./server.js");
await stopBrowserControlServer();
});
it("POST /profiles/create returns 400 for missing name", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
@@ -350,7 +346,6 @@ describe("profile CRUD endpoints", () => {
});
it("POST /profiles/create returns 400 for invalid name format", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
@@ -365,7 +360,6 @@ describe("profile CRUD endpoints", () => {
});
it("POST /profiles/create returns 409 for duplicate name", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
@@ -381,7 +375,6 @@ describe("profile CRUD endpoints", () => {
});
it("POST /profiles/create accepts cdpUrl for remote profiles", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
@@ -402,7 +395,6 @@ describe("profile CRUD endpoints", () => {
});
it("POST /profiles/create returns 400 for invalid cdpUrl", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
@@ -417,7 +409,6 @@ describe("profile CRUD endpoints", () => {
});
it("DELETE /profiles/:name returns 404 for non-existent profile", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
@@ -430,7 +421,6 @@ describe("profile CRUD endpoints", () => {
});
it("DELETE /profiles/:name returns 400 for default profile deletion", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
@@ -444,7 +434,6 @@ describe("profile CRUD endpoints", () => {
});
it("DELETE /profiles/:name returns 400 for invalid name format", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;

View File

@@ -59,9 +59,11 @@ vi.mock("../infra/exec-approvals.js", async () => {
};
});
const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
const execApprovals = await import("../infra/exec-approvals.js");
describe("exec approvals CLI", () => {
const createProgram = async () => {
const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
const createProgram = () => {
const program = new Command();
program.exitOverride();
registerExecApprovalsCli(program);
@@ -73,21 +75,21 @@ describe("exec approvals CLI", () => {
runtimeErrors.length = 0;
callGatewayFromCli.mockClear();
const localProgram = await createProgram();
const localProgram = createProgram();
await localProgram.parseAsync(["approvals", "get"], { from: "user" });
expect(callGatewayFromCli).not.toHaveBeenCalled();
expect(runtimeErrors).toHaveLength(0);
callGatewayFromCli.mockClear();
const gatewayProgram = await createProgram();
const gatewayProgram = createProgram();
await gatewayProgram.parseAsync(["approvals", "get", "--gateway"], { from: "user" });
expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.get", expect.anything(), {});
expect(runtimeErrors).toHaveLength(0);
callGatewayFromCli.mockClear();
const nodeProgram = await createProgram();
const nodeProgram = createProgram();
await nodeProgram.parseAsync(["approvals", "get", "--node", "macbook"], { from: "user" });
expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.node.get", expect.anything(), {
@@ -101,11 +103,9 @@ describe("exec approvals CLI", () => {
runtimeErrors.length = 0;
callGatewayFromCli.mockClear();
const execApprovals = await import("../infra/exec-approvals.js");
const saveExecApprovals = vi.mocked(execApprovals.saveExecApprovals);
saveExecApprovals.mockClear();
const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
const program = new Command();
program.exitOverride();
registerExecApprovalsCli(program);

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { UpdateRunResult } from "../infra/update-runner.js";
const confirm = vi.fn();
@@ -91,6 +91,23 @@ const { updateCommand, registerUpdateCli, updateStatusCommand, updateWizardComma
await import("./update-cli.js");
describe("update-cli", () => {
let fixtureRoot = "";
let fixtureCount = 0;
const createCaseDir = async (prefix: string) => {
const dir = path.join(fixtureRoot, `${prefix}-${fixtureCount++}`);
await fs.mkdir(dir, { recursive: true });
return dir;
};
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-tests-"));
});
afterAll(async () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
const baseSnapshot = {
valid: true,
config: {},
@@ -223,41 +240,37 @@ describe("update-cli", () => {
});
it("defaults to stable channel for package installs when unset", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
try {
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
"utf-8",
);
const tempDir = await createCaseDir("openclaw-update");
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
"utf-8",
);
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
installKind: "package",
packageManager: "npm",
deps: {
manager: "npm",
status: "ok",
lockfilePath: null,
markerPath: null,
},
});
vi.mocked(runGatewayUpdate).mockResolvedValue({
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
installKind: "package",
packageManager: "npm",
deps: {
manager: "npm",
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
lockfilePath: null,
markerPath: null,
},
});
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
await updateCommand({ yes: true });
await updateCommand({ yes: true });
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
expect(call?.channel).toBe("stable");
expect(call?.tag).toBe("latest");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
expect(call?.channel).toBe("stable");
expect(call?.tag).toBe("latest");
});
it("uses stored beta channel when configured", async () => {
@@ -279,75 +292,67 @@ describe("update-cli", () => {
});
it("falls back to latest when beta tag is older than release", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
try {
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
"utf-8",
);
const tempDir = await createCaseDir("openclaw-update");
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
"utf-8",
);
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
...baseSnapshot,
config: { update: { channel: "beta" } },
});
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
installKind: "package",
packageManager: "npm",
deps: {
manager: "npm",
status: "ok",
lockfilePath: null,
markerPath: null,
},
});
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "1.2.3-1",
});
vi.mocked(runGatewayUpdate).mockResolvedValue({
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
...baseSnapshot,
config: { update: { channel: "beta" } },
});
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
installKind: "package",
packageManager: "npm",
deps: {
manager: "npm",
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
lockfilePath: null,
markerPath: null,
},
});
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "1.2.3-1",
});
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
await updateCommand({});
await updateCommand({});
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
expect(call?.channel).toBe("beta");
expect(call?.tag).toBe("latest");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
expect(call?.channel).toBe("beta");
expect(call?.tag).toBe("latest");
});
it("honors --tag override", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
try {
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
"utf-8",
);
const tempDir = await createCaseDir("openclaw-update");
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
"utf-8",
);
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
await updateCommand({ tag: "next" });
await updateCommand({ tag: "next" });
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
expect(call?.tag).toBe("next");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
expect(call?.tag).toBe("next");
});
it("updateCommand outputs JSON when --json is set", async () => {
@@ -471,95 +476,87 @@ describe("update-cli", () => {
});
it("requires confirmation on downgrade when non-interactive", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
try {
setTty(false);
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
"utf-8",
);
const tempDir = await createCaseDir("openclaw-update");
setTty(false);
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
"utf-8",
);
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
installKind: "package",
packageManager: "npm",
deps: {
manager: "npm",
status: "ok",
lockfilePath: null,
markerPath: null,
},
});
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "0.0.1",
});
vi.mocked(runGatewayUpdate).mockResolvedValue({
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
installKind: "package",
packageManager: "npm",
deps: {
manager: "npm",
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
vi.mocked(defaultRuntime.error).mockClear();
vi.mocked(defaultRuntime.exit).mockClear();
lockfilePath: null,
markerPath: null,
},
});
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "0.0.1",
});
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
vi.mocked(defaultRuntime.error).mockClear();
vi.mocked(defaultRuntime.exit).mockClear();
await updateCommand({});
await updateCommand({});
expect(defaultRuntime.error).toHaveBeenCalledWith(
expect.stringContaining("Downgrade confirmation required."),
);
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
expect(defaultRuntime.error).toHaveBeenCalledWith(
expect.stringContaining("Downgrade confirmation required."),
);
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
});
it("allows downgrade with --yes in non-interactive mode", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
try {
setTty(false);
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
"utf-8",
);
const tempDir = await createCaseDir("openclaw-update");
setTty(false);
await fs.writeFile(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
"utf-8",
);
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
installKind: "package",
packageManager: "npm",
deps: {
manager: "npm",
status: "ok",
lockfilePath: null,
markerPath: null,
},
});
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "0.0.1",
});
vi.mocked(runGatewayUpdate).mockResolvedValue({
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
installKind: "package",
packageManager: "npm",
deps: {
manager: "npm",
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
vi.mocked(defaultRuntime.error).mockClear();
vi.mocked(defaultRuntime.exit).mockClear();
lockfilePath: null,
markerPath: null,
},
});
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "0.0.1",
});
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "npm",
steps: [],
durationMs: 100,
});
vi.mocked(defaultRuntime.error).mockClear();
vi.mocked(defaultRuntime.exit).mockClear();
await updateCommand({ yes: true });
await updateCommand({ yes: true });
expect(defaultRuntime.error).not.toHaveBeenCalledWith(
expect.stringContaining("Downgrade confirmation required."),
);
expect(runGatewayUpdate).toHaveBeenCalled();
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
expect(defaultRuntime.error).not.toHaveBeenCalledWith(
expect.stringContaining("Downgrade confirmation required."),
);
expect(runGatewayUpdate).toHaveBeenCalled();
});
it("updateWizardCommand requires a TTY", async () => {
@@ -576,7 +573,7 @@ describe("update-cli", () => {
});
it("updateWizardCommand offers dev checkout and forwards selections", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-wizard-"));
const tempDir = await createCaseDir("openclaw-update-wizard");
const previousGitDir = process.env.OPENCLAW_GIT_DIR;
try {
setTty(true);
@@ -608,7 +605,6 @@ describe("update-cli", () => {
expect(call?.channel).toBe("dev");
} finally {
process.env.OPENCLAW_GIT_DIR = previousGitDir;
await fs.rm(tempDir, { recursive: true, force: true });
}
});
});

View File

@@ -1,19 +1,53 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
import { loadConfig } from "./config.js";
import { withTempHome } from "./test-helpers.js";
type HomeEnvSnapshot = {
home: string | undefined;
userProfile: string | undefined;
homeDrive: string | undefined;
homePath: string | undefined;
stateDir: string | undefined;
};
function snapshotHomeEnv(): HomeEnvSnapshot {
return {
home: process.env.HOME,
userProfile: process.env.USERPROFILE,
homeDrive: process.env.HOMEDRIVE,
homePath: process.env.HOMEPATH,
stateDir: process.env.OPENCLAW_STATE_DIR,
};
}
function restoreHomeEnv(snapshot: HomeEnvSnapshot) {
const restoreKey = (key: string, value: string | undefined) => {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
};
restoreKey("HOME", snapshot.home);
restoreKey("USERPROFILE", snapshot.userProfile);
restoreKey("HOMEDRIVE", snapshot.homeDrive);
restoreKey("HOMEPATH", snapshot.homePath);
restoreKey("OPENCLAW_STATE_DIR", snapshot.stateDir);
}
describe("config identity defaults", () => {
let previousHome: string | undefined;
let fixtureRoot = "";
let fixtureCount = 0;
beforeEach(() => {
previousHome = process.env.HOME;
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-identity-"));
});
afterEach(() => {
process.env.HOME = previousHome;
afterAll(async () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
const writeAndLoadConfig = async (home: string, config: Record<string, unknown>) => {
@@ -27,6 +61,30 @@ describe("config identity defaults", () => {
return loadConfig();
};
const withTempHome = async <T>(fn: (home: string) => Promise<T>): Promise<T> => {
const home = path.join(fixtureRoot, `home-${fixtureCount++}`);
await fs.mkdir(path.join(home, ".openclaw"), { recursive: true });
const snapshot = snapshotHomeEnv();
process.env.HOME = home;
process.env.USERPROFILE = home;
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
if (process.platform === "win32") {
const match = home.match(/^([A-Za-z]:)(.*)$/);
if (match) {
process.env.HOMEDRIVE = match[1];
process.env.HOMEPATH = match[2] || "\\";
}
}
try {
return await fn(home);
} finally {
restoreHomeEnv(snapshot);
}
};
it("does not derive mention defaults and only sets ackReactionScope when identity is present", async () => {
await withTempHome(async (home) => {
const cfg = await writeAndLoadConfig(home, {

View File

@@ -1,8 +1,8 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { afterAll, describe, expect, it } from "vitest";
import { validateConfigObjectWithPlugins } from "./config.js";
import { withTempHome } from "./test-helpers.js";
async function writePluginFixture(params: {
dir: string;
@@ -31,145 +31,150 @@ async function writePluginFixture(params: {
}
describe("config plugin validation", () => {
const fixtureRoot = path.join(os.tmpdir(), "openclaw-config-plugin-validation");
let caseIndex = 0;
function createCaseHome() {
const home = path.join(fixtureRoot, `case-${caseIndex++}`);
return fs.mkdir(home, { recursive: true }).then(() => home);
}
const validateInHome = (home: string, raw: unknown) => {
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
return validateConfigObjectWithPlugins(raw);
};
afterAll(async () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
it("rejects missing plugin load paths", async () => {
await withTempHome(async (home) => {
const missingPath = path.join(home, "missing-plugin");
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, load: { paths: [missingPath] } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
const hasIssue = res.issues.some(
(issue) =>
issue.path === "plugins.load.paths" && issue.message.includes("plugin path not found"),
);
expect(hasIssue).toBe(true);
}
const home = await createCaseHome();
const missingPath = path.join(home, "missing-plugin");
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, load: { paths: [missingPath] } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
const hasIssue = res.issues.some(
(issue) =>
issue.path === "plugins.load.paths" && issue.message.includes("plugin path not found"),
);
expect(hasIssue).toBe(true);
}
});
it("rejects missing plugin ids in entries", async () => {
await withTempHome(async (home) => {
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues).toContainEqual({
path: "plugins.entries.missing-plugin",
message: "plugin not found: missing-plugin",
});
}
const home = await createCaseHome();
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues).toContainEqual({
path: "plugins.entries.missing-plugin",
message: "plugin not found: missing-plugin",
});
}
});
it("rejects missing plugin ids in allow/deny/slots", async () => {
await withTempHome(async (home) => {
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: false,
allow: ["missing-allow"],
deny: ["missing-deny"],
slots: { memory: "missing-slot" },
},
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues).toEqual(
expect.arrayContaining([
{ path: "plugins.allow", message: "plugin not found: missing-allow" },
{ path: "plugins.deny", message: "plugin not found: missing-deny" },
{ path: "plugins.slots.memory", message: "plugin not found: missing-slot" },
]),
);
}
const home = await createCaseHome();
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: false,
allow: ["missing-allow"],
deny: ["missing-deny"],
slots: { memory: "missing-slot" },
},
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues).toEqual(
expect.arrayContaining([
{ path: "plugins.allow", message: "plugin not found: missing-allow" },
{ path: "plugins.deny", message: "plugin not found: missing-deny" },
{ path: "plugins.slots.memory", message: "plugin not found: missing-slot" },
]),
);
}
});
it("surfaces plugin config diagnostics", async () => {
await withTempHome(async (home) => {
const pluginDir = path.join(home, "bad-plugin");
await writePluginFixture({
dir: pluginDir,
id: "bad-plugin",
schema: {
type: "object",
additionalProperties: false,
properties: {
value: { type: "boolean" },
},
required: ["value"],
const home = await createCaseHome();
const pluginDir = path.join(home, "bad-plugin");
await writePluginFixture({
dir: pluginDir,
id: "bad-plugin",
schema: {
type: "object",
additionalProperties: false,
properties: {
value: { type: "boolean" },
},
});
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: true,
load: { paths: [pluginDir] },
entries: { "bad-plugin": { config: { value: "nope" } } },
},
});
expect(res.ok).toBe(false);
if (!res.ok) {
const hasIssue = res.issues.some(
(issue) =>
issue.path === "plugins.entries.bad-plugin.config" &&
issue.message.includes("invalid config"),
);
expect(hasIssue).toBe(true);
}
required: ["value"],
},
});
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: true,
load: { paths: [pluginDir] },
entries: { "bad-plugin": { config: { value: "nope" } } },
},
});
expect(res.ok).toBe(false);
if (!res.ok) {
const hasIssue = res.issues.some(
(issue) =>
issue.path === "plugins.entries.bad-plugin.config" &&
issue.message.includes("invalid config"),
);
expect(hasIssue).toBe(true);
}
});
it("accepts known plugin ids", async () => {
await withTempHome(async (home) => {
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, entries: { discord: { enabled: true } } },
});
expect(res.ok).toBe(true);
const home = await createCaseHome();
const res = validateInHome(home, {
agents: { list: [{ id: "pi" }] },
plugins: { enabled: false, entries: { discord: { enabled: true } } },
});
expect(res.ok).toBe(true);
});
it("accepts plugin heartbeat targets", async () => {
await withTempHome(async (home) => {
const pluginDir = path.join(home, "bluebubbles-plugin");
await writePluginFixture({
dir: pluginDir,
id: "bluebubbles-plugin",
channels: ["bluebubbles"],
schema: { type: "object" },
});
const res = validateInHome(home, {
agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] },
plugins: { enabled: false, load: { paths: [pluginDir] } },
});
expect(res.ok).toBe(true);
const home = await createCaseHome();
const pluginDir = path.join(home, "bluebubbles-plugin");
await writePluginFixture({
dir: pluginDir,
id: "bluebubbles-plugin",
channels: ["bluebubbles"],
schema: { type: "object" },
});
const res = validateInHome(home, {
agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] },
plugins: { enabled: false, load: { paths: [pluginDir] } },
});
expect(res.ok).toBe(true);
});
it("rejects unknown heartbeat targets", async () => {
await withTempHome(async (home) => {
const res = validateInHome(home, {
agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues).toContainEqual({
path: "agents.defaults.heartbeat.target",
message: "unknown heartbeat target: not-a-channel",
});
}
const home = await createCaseHome();
const res = validateInHome(home, {
agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues).toContainEqual({
path: "agents.defaults.heartbeat.target",
message: "unknown heartbeat target: not-a-channel",
});
}
});
});

View File

@@ -4,28 +4,29 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import * as tar from "tar";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterAll, describe, expect, it, vi } from "vitest";
const tempDirs: string[] = [];
const fixtureRoot = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`);
let tempDirIndex = 0;
vi.mock("../process/exec.js", () => ({
runCommandWithTimeout: vi.fn(),
}));
function makeTempDir() {
const dir = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`);
const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`);
fs.mkdirSync(dir, { recursive: true });
tempDirs.push(dir);
return dir;
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// ignore cleanup failures
}
const { runCommandWithTimeout } = await import("../process/exec.js");
const { installHooksFromArchive, installHooksFromPath } = await import("./install.js");
afterAll(() => {
try {
fs.rmSync(fixtureRoot, { recursive: true, force: true });
} catch {
// ignore cleanup failures
}
});
@@ -61,7 +62,6 @@ describe("installHooksFromArchive", () => {
fs.writeFileSync(archivePath, buffer);
const hooksDir = path.join(stateDir, "hooks");
const { installHooksFromArchive } = await import("./install.js");
const result = await installHooksFromArchive({ archivePath, hooksDir });
expect(result.ok).toBe(true);
@@ -111,7 +111,6 @@ describe("installHooksFromArchive", () => {
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
const hooksDir = path.join(stateDir, "hooks");
const { installHooksFromArchive } = await import("./install.js");
const result = await installHooksFromArchive({ archivePath, hooksDir });
expect(result.ok).toBe(true);
@@ -160,7 +159,6 @@ describe("installHooksFromArchive", () => {
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
const hooksDir = path.join(stateDir, "hooks");
const { installHooksFromArchive } = await import("./install.js");
const result = await installHooksFromArchive({ archivePath, hooksDir });
expect(result.ok).toBe(false);
@@ -207,7 +205,6 @@ describe("installHooksFromArchive", () => {
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
const hooksDir = path.join(stateDir, "hooks");
const { installHooksFromArchive } = await import("./install.js");
const result = await installHooksFromArchive({ archivePath, hooksDir });
expect(result.ok).toBe(false);
@@ -253,11 +250,9 @@ describe("installHooksFromPath", () => {
"utf-8",
);
const { runCommandWithTimeout } = await import("../process/exec.js");
const run = vi.mocked(runCommandWithTimeout);
run.mockResolvedValue({ code: 0, stdout: "", stderr: "" });
const { installHooksFromPath } = await import("./install.js");
const res = await installHooksFromPath({
path: pkgDir,
hooksDir: path.join(stateDir, "hooks"),
@@ -301,7 +296,6 @@ describe("installHooksFromPath", () => {
fs.writeFileSync(path.join(hookDir, "handler.ts"), "export default async () => {};\n");
const hooksDir = path.join(stateDir, "hooks");
const { installHooksFromPath } = await import("./install.js");
const result = await installHooksFromPath({ path: hookDir, hooksDir });
expect(result.ok).toBe(true);

View File

@@ -2,19 +2,19 @@ import { randomUUID } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterAll, afterEach, describe, expect, it } from "vitest";
import { loadOpenClawPlugins } from "./loader.js";
type TempPlugin = { dir: string; file: string; id: string };
const tempDirs: string[] = [];
const fixtureRoot = path.join(os.tmpdir(), `openclaw-plugin-${randomUUID()}`);
let tempDirIndex = 0;
const prevBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
function makeTempDir() {
const dir = path.join(os.tmpdir(), `openclaw-plugin-${randomUUID()}`);
const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`);
fs.mkdirSync(dir, { recursive: true });
tempDirs.push(dir);
return dir;
}
@@ -44,13 +44,6 @@ function writePlugin(params: {
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// ignore cleanup failures
}
}
if (prevBundledDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
} else {
@@ -58,6 +51,14 @@ afterEach(() => {
}
});
afterAll(() => {
try {
fs.rmSync(fixtureRoot, { recursive: true, force: true });
} catch {
// ignore cleanup failures
}
});
describe("loadOpenClawPlugins", () => {
it("disables bundled plugins by default", () => {
const bundledDir = makeTempDir();

View File

@@ -51,6 +51,17 @@ function canConnect(port: number): Promise<boolean> {
});
}
async function waitForPortClosed(port: number, timeoutMs = 1_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() <= deadline) {
if (!(await canConnect(port))) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error("timeout waiting for port to close");
}
describe("attachChildProcessBridge", () => {
const children: Array<{ kill: (signal?: NodeJS.Signals) => boolean }> = [];
const detachments: Array<() => void> = [];
@@ -111,7 +122,7 @@ describe("attachChildProcessBridge", () => {
});
});
await new Promise((r) => setTimeout(r, 250));
await waitForPortClosed(port);
expect(await canConnect(port)).toBe(false);
}, 20_000);
});

View File

@@ -2,19 +2,16 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import sharp from "sharp";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as ssrf from "../infra/net/ssrf.js";
import { optimizeImageToPng } from "../media/image-ops.js";
import { loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } from "./media.js";
const tmpFiles: string[] = [];
let fixtureRoot = "";
let fixtureFileCount = 0;
async function writeTempFile(buffer: Buffer, ext: string): Promise<string> {
const file = path.join(
os.tmpdir(),
`openclaw-media-${Date.now()}-${Math.random().toString(16).slice(2)}${ext}`,
);
tmpFiles.push(file);
const file = path.join(fixtureRoot, `media-${fixtureFileCount++}${ext}`);
await fs.writeFile(file, buffer);
return file;
}
@@ -45,9 +42,15 @@ async function createLargeTestJpeg(): Promise<{ buffer: Buffer; file: string }>
return { buffer, file };
}
afterEach(async () => {
await Promise.all(tmpFiles.map((file) => fs.rm(file, { force: true })));
tmpFiles.length = 0;
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-test-"));
});
afterAll(async () => {
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
afterEach(() => {
vi.restoreAllMocks();
});