mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
perf(test): cut setup/import overhead in hot suites
This commit is contained in:
@@ -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" });
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user