mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 23:02:02 +03: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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as ssrf from "../../infra/net/ssrf.js";
|
import * as ssrf from "../../infra/net/ssrf.js";
|
||||||
import * as logger from "../../logger.js";
|
import * as logger from "../../logger.js";
|
||||||
|
import { createWebFetchTool } from "./web-tools.js";
|
||||||
|
|
||||||
const lookupMock = vi.fn();
|
const lookupMock = vi.fn();
|
||||||
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
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 } {
|
function makeHeaders(map: Record<string, string>): { get: (key: string) => string | null } {
|
||||||
return {
|
return {
|
||||||
@@ -51,12 +57,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
|||||||
// @ts-expect-error mock fetch
|
// @ts-expect-error mock fetch
|
||||||
global.fetch = fetchSpy;
|
global.fetch = fetchSpy;
|
||||||
|
|
||||||
const { createWebFetchTool } = await import("./web-tools.js");
|
const tool = createWebFetchTool(baseToolConfig);
|
||||||
const tool = createWebFetchTool({
|
|
||||||
config: {
|
|
||||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tool?.execute?.("call", { url: "https://example.com/page" });
|
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
|
// @ts-expect-error mock fetch
|
||||||
global.fetch = fetchSpy;
|
global.fetch = fetchSpy;
|
||||||
|
|
||||||
const { createWebFetchTool } = await import("./web-tools.js");
|
const tool = createWebFetchTool(baseToolConfig);
|
||||||
const tool = createWebFetchTool({
|
|
||||||
config: {
|
|
||||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", { url: "https://example.com/cf" });
|
const result = await tool?.execute?.("call", { url: "https://example.com/cf" });
|
||||||
expect(result?.details).toMatchObject({
|
expect(result?.details).toMatchObject({
|
||||||
@@ -96,12 +92,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
|||||||
// @ts-expect-error mock fetch
|
// @ts-expect-error mock fetch
|
||||||
global.fetch = fetchSpy;
|
global.fetch = fetchSpy;
|
||||||
|
|
||||||
const { createWebFetchTool } = await import("./web-tools.js");
|
const tool = createWebFetchTool(baseToolConfig);
|
||||||
const tool = createWebFetchTool({
|
|
||||||
config: {
|
|
||||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", { url: "https://example.com/html" });
|
const result = await tool?.execute?.("call", { url: "https://example.com/html" });
|
||||||
expect(result?.details?.extractor).not.toBe("cf-markdown");
|
expect(result?.details?.extractor).not.toBe("cf-markdown");
|
||||||
@@ -116,12 +107,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
|||||||
// @ts-expect-error mock fetch
|
// @ts-expect-error mock fetch
|
||||||
global.fetch = fetchSpy;
|
global.fetch = fetchSpy;
|
||||||
|
|
||||||
const { createWebFetchTool } = await import("./web-tools.js");
|
const tool = createWebFetchTool(baseToolConfig);
|
||||||
const tool = createWebFetchTool({
|
|
||||||
config: {
|
|
||||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tool?.execute?.("call", { url: "https://example.com/tokens/private?token=secret" });
|
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
|
// @ts-expect-error mock fetch
|
||||||
global.fetch = fetchSpy;
|
global.fetch = fetchSpy;
|
||||||
|
|
||||||
const { createWebFetchTool } = await import("./web-tools.js");
|
const tool = createWebFetchTool(baseToolConfig);
|
||||||
const tool = createWebFetchTool({
|
|
||||||
config: {
|
|
||||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", {
|
const result = await tool?.execute?.("call", {
|
||||||
url: "https://example.com/text-mode",
|
url: "https://example.com/text-mode",
|
||||||
@@ -169,12 +150,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
|||||||
// @ts-expect-error mock fetch
|
// @ts-expect-error mock fetch
|
||||||
global.fetch = fetchSpy;
|
global.fetch = fetchSpy;
|
||||||
|
|
||||||
const { createWebFetchTool } = await import("./web-tools.js");
|
const tool = createWebFetchTool(baseToolConfig);
|
||||||
const tool = createWebFetchTool({
|
|
||||||
config: {
|
|
||||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tool?.execute?.("call", { url: "https://example.com/no-tokens" });
|
await tool?.execute?.("call", { url: "https://example.com/no-tokens" });
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./pw-session.js", () => sessionMocks);
|
vi.mock("./pw-session.js", () => sessionMocks);
|
||||||
|
const mod = await import("./pw-tools-core.js");
|
||||||
async function importModule() {
|
|
||||||
return await import("./pw-tools-core.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("pw-tools-core", () => {
|
describe("pw-tools-core", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -53,7 +50,6 @@ describe("pw-tools-core", () => {
|
|||||||
currentRefLocator = { scrollIntoViewIfNeeded };
|
currentRefLocator = { scrollIntoViewIfNeeded };
|
||||||
currentPage = {};
|
currentPage = {};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await mod.scrollIntoViewViaPlaywright({
|
await mod.scrollIntoViewViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
targetId: "T1",
|
targetId: "T1",
|
||||||
@@ -70,7 +66,6 @@ describe("pw-tools-core", () => {
|
|||||||
currentRefLocator = { scrollIntoViewIfNeeded };
|
currentRefLocator = { scrollIntoViewIfNeeded };
|
||||||
currentPage = {};
|
currentPage = {};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await expect(
|
await expect(
|
||||||
mod.scrollIntoViewViaPlaywright({
|
mod.scrollIntoViewViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
@@ -86,7 +81,6 @@ describe("pw-tools-core", () => {
|
|||||||
currentRefLocator = { scrollIntoViewIfNeeded };
|
currentRefLocator = { scrollIntoViewIfNeeded };
|
||||||
currentPage = {};
|
currentPage = {};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await expect(
|
await expect(
|
||||||
mod.scrollIntoViewViaPlaywright({
|
mod.scrollIntoViewViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
@@ -102,7 +96,6 @@ describe("pw-tools-core", () => {
|
|||||||
currentRefLocator = { click };
|
currentRefLocator = { click };
|
||||||
currentPage = {};
|
currentPage = {};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await expect(
|
await expect(
|
||||||
mod.clickViaPlaywright({
|
mod.clickViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
@@ -118,7 +111,6 @@ describe("pw-tools-core", () => {
|
|||||||
currentRefLocator = { click };
|
currentRefLocator = { click };
|
||||||
currentPage = {};
|
currentPage = {};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await expect(
|
await expect(
|
||||||
mod.clickViaPlaywright({
|
mod.clickViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
@@ -136,7 +128,6 @@ describe("pw-tools-core", () => {
|
|||||||
currentRefLocator = { click };
|
currentRefLocator = { click };
|
||||||
currentPage = {};
|
currentPage = {};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await expect(
|
await expect(
|
||||||
mod.clickViaPlaywright({
|
mod.clickViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./pw-session.js", () => sessionMocks);
|
vi.mock("./pw-session.js", () => sessionMocks);
|
||||||
|
const mod = await import("./pw-tools-core.js");
|
||||||
async function importModule() {
|
|
||||||
return await import("./pw-tools-core.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("pw-tools-core", () => {
|
describe("pw-tools-core", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -75,7 +72,6 @@ describe("pw-tools-core", () => {
|
|||||||
keyboard: { press: vi.fn(async () => {}) },
|
keyboard: { press: vi.fn(async () => {}) },
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await mod.armFileUploadViaPlaywright({
|
await mod.armFileUploadViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
paths: ["/tmp/1"],
|
paths: ["/tmp/1"],
|
||||||
@@ -101,7 +97,6 @@ describe("pw-tools-core", () => {
|
|||||||
waitForEvent,
|
waitForEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await mod.armDialogViaPlaywright({
|
await mod.armDialogViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
accept: true,
|
accept: true,
|
||||||
@@ -145,7 +140,6 @@ describe("pw-tools-core", () => {
|
|||||||
getByText: vi.fn(() => ({ first: () => ({ waitFor: vi.fn() }) })),
|
getByText: vi.fn(() => ({ first: () => ({ waitFor: vi.fn() }) })),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await mod.waitForViaPlaywright({
|
await mod.waitForViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
selector: "#main",
|
selector: "#main",
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./pw-session.js", () => sessionMocks);
|
vi.mock("./pw-session.js", () => sessionMocks);
|
||||||
|
const mod = await import("./pw-tools-core.js");
|
||||||
async function importModule() {
|
|
||||||
return await import("./pw-tools-core.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("pw-tools-core", () => {
|
describe("pw-tools-core", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -57,7 +54,6 @@ describe("pw-tools-core", () => {
|
|||||||
screenshot: vi.fn(async () => Buffer.from("P")),
|
screenshot: vi.fn(async () => Buffer.from("P")),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
const res = await mod.takeScreenshotViaPlaywright({
|
const res = await mod.takeScreenshotViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
targetId: "T1",
|
targetId: "T1",
|
||||||
@@ -78,7 +74,6 @@ describe("pw-tools-core", () => {
|
|||||||
screenshot: vi.fn(async () => Buffer.from("P")),
|
screenshot: vi.fn(async () => Buffer.from("P")),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
const res = await mod.takeScreenshotViaPlaywright({
|
const res = await mod.takeScreenshotViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
targetId: "T1",
|
targetId: "T1",
|
||||||
@@ -99,8 +94,6 @@ describe("pw-tools-core", () => {
|
|||||||
screenshot: vi.fn(async () => Buffer.from("P")),
|
screenshot: vi.fn(async () => Buffer.from("P")),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
mod.takeScreenshotViaPlaywright({
|
mod.takeScreenshotViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
@@ -127,7 +120,6 @@ describe("pw-tools-core", () => {
|
|||||||
keyboard: { press: vi.fn(async () => {}) },
|
keyboard: { press: vi.fn(async () => {}) },
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await mod.armFileUploadViaPlaywright({
|
await mod.armFileUploadViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
targetId: "T1",
|
targetId: "T1",
|
||||||
@@ -151,7 +143,6 @@ describe("pw-tools-core", () => {
|
|||||||
keyboard: { press },
|
keyboard: { press },
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await mod.armFileUploadViaPlaywright({
|
await mod.armFileUploadViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
paths: [],
|
paths: [],
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ const tmpDirMocks = vi.hoisted(() => ({
|
|||||||
resolvePreferredOpenClawTmpDir: vi.fn(() => "/tmp/openclaw"),
|
resolvePreferredOpenClawTmpDir: vi.fn(() => "/tmp/openclaw"),
|
||||||
}));
|
}));
|
||||||
vi.mock("../infra/tmp-openclaw-dir.js", () => tmpDirMocks);
|
vi.mock("../infra/tmp-openclaw-dir.js", () => tmpDirMocks);
|
||||||
|
const mod = await import("./pw-tools-core.js");
|
||||||
async function importModule() {
|
|
||||||
return await import("./pw-tools-core.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("pw-tools-core", () => {
|
describe("pw-tools-core", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -75,7 +72,6 @@ describe("pw-tools-core", () => {
|
|||||||
|
|
||||||
currentPage = { on, off };
|
currentPage = { on, off };
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
const targetPath = path.resolve("/tmp/file.bin");
|
const targetPath = path.resolve("/tmp/file.bin");
|
||||||
const p = mod.waitForDownloadViaPlaywright({
|
const p = mod.waitForDownloadViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
@@ -113,7 +109,6 @@ describe("pw-tools-core", () => {
|
|||||||
|
|
||||||
currentPage = { on, off };
|
currentPage = { on, off };
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
const targetPath = path.resolve("/tmp/report.pdf");
|
const targetPath = path.resolve("/tmp/report.pdf");
|
||||||
const p = mod.downloadViaPlaywright({
|
const p = mod.downloadViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
@@ -152,7 +147,6 @@ describe("pw-tools-core", () => {
|
|||||||
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred");
|
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred");
|
||||||
currentPage = { on, off };
|
currentPage = { on, off };
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
const p = mod.waitForDownloadViaPlaywright({
|
const p = mod.waitForDownloadViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
targetId: "T1",
|
targetId: "T1",
|
||||||
@@ -194,7 +188,6 @@ describe("pw-tools-core", () => {
|
|||||||
text: async () => '{"ok":true,"value":123}',
|
text: async () => '{"ok":true,"value":123}',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
const p = mod.responseBodyViaPlaywright({
|
const p = mod.responseBodyViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
targetId: "T1",
|
targetId: "T1",
|
||||||
@@ -218,7 +211,6 @@ describe("pw-tools-core", () => {
|
|||||||
currentRefLocator = { scrollIntoViewIfNeeded };
|
currentRefLocator = { scrollIntoViewIfNeeded };
|
||||||
currentPage = {};
|
currentPage = {};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await mod.scrollIntoViewViaPlaywright({
|
await mod.scrollIntoViewViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
targetId: "T1",
|
targetId: "T1",
|
||||||
@@ -232,7 +224,6 @@ describe("pw-tools-core", () => {
|
|||||||
currentRefLocator = { scrollIntoViewIfNeeded: vi.fn(async () => {}) };
|
currentRefLocator = { scrollIntoViewIfNeeded: vi.fn(async () => {}) };
|
||||||
currentPage = {};
|
currentPage = {};
|
||||||
|
|
||||||
const mod = await importModule();
|
|
||||||
await expect(
|
await expect(
|
||||||
mod.scrollIntoViewViaPlaywright({
|
mod.scrollIntoViewViaPlaywright({
|
||||||
cdpUrl: "http://127.0.0.1:18792",
|
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> {
|
async function getFreePort(): Promise<number> {
|
||||||
while (true) {
|
while (true) {
|
||||||
const port = await new Promise<number>((resolve, reject) => {
|
const port = await new Promise<number>((resolve, reject) => {
|
||||||
@@ -271,12 +274,10 @@ describe("browser control server", () => {
|
|||||||
} else {
|
} else {
|
||||||
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
||||||
}
|
}
|
||||||
const { stopBrowserControlServer } = await import("./server.js");
|
|
||||||
await stopBrowserControlServer();
|
await stopBrowserControlServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
const startServerAndBase = async () => {
|
const startServerAndBase = async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
const base = `http://127.0.0.1:${testPort}`;
|
||||||
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
|
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> {
|
async function getFreePort(): Promise<number> {
|
||||||
const probe = createServer();
|
const probe = createServer();
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -95,12 +98,10 @@ describe("browser control evaluate gating", () => {
|
|||||||
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stopBrowserControlServer } = await import("./server.js");
|
|
||||||
await stopBrowserControlServer();
|
await stopBrowserControlServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks act:evaluate but still allows cookies/storage reads", async () => {
|
it("blocks act:evaluate but still allows cookies/storage reads", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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> {
|
async function getFreePort(): Promise<number> {
|
||||||
while (true) {
|
while (true) {
|
||||||
const port = await new Promise<number>((resolve, reject) => {
|
const port = await new Promise<number>((resolve, reject) => {
|
||||||
@@ -270,12 +273,10 @@ describe("browser control server", () => {
|
|||||||
} else {
|
} else {
|
||||||
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
||||||
}
|
}
|
||||||
const { stopBrowserControlServer } = await import("./server.js");
|
|
||||||
await stopBrowserControlServer();
|
await stopBrowserControlServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /tabs/open?profile=unknown returns 404", async () => {
|
it("POST /tabs/open?profile=unknown returns 404", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
const base = `http://127.0.0.1:${testPort}`;
|
||||||
|
|
||||||
@@ -307,9 +308,6 @@ describe("profile CRUD endpoints", () => {
|
|||||||
prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT;
|
prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT;
|
||||||
process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2);
|
process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2);
|
||||||
|
|
||||||
prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT;
|
|
||||||
process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2);
|
|
||||||
|
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
vi.fn(async (url: string) => {
|
vi.fn(async (url: string) => {
|
||||||
@@ -330,12 +328,10 @@ describe("profile CRUD endpoints", () => {
|
|||||||
} else {
|
} else {
|
||||||
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
||||||
}
|
}
|
||||||
const { stopBrowserControlServer } = await import("./server.js");
|
|
||||||
await stopBrowserControlServer();
|
await stopBrowserControlServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("POST /profiles/create returns 400 for missing name", async () => {
|
it("POST /profiles/create returns 400 for missing name", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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 () => {
|
it("POST /profiles/create returns 400 for invalid name format", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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 () => {
|
it("POST /profiles/create returns 409 for duplicate name", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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 () => {
|
it("POST /profiles/create accepts cdpUrl for remote profiles", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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 () => {
|
it("POST /profiles/create returns 400 for invalid cdpUrl", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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 () => {
|
it("DELETE /profiles/:name returns 404 for non-existent profile", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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 () => {
|
it("DELETE /profiles/:name returns 400 for default profile deletion", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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 () => {
|
it("DELETE /profiles/:name returns 400 for invalid name format", async () => {
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
await startBrowserControlServerFromConfig();
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
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", () => {
|
describe("exec approvals CLI", () => {
|
||||||
const createProgram = async () => {
|
const createProgram = () => {
|
||||||
const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
program.exitOverride();
|
program.exitOverride();
|
||||||
registerExecApprovalsCli(program);
|
registerExecApprovalsCli(program);
|
||||||
@@ -73,21 +75,21 @@ describe("exec approvals CLI", () => {
|
|||||||
runtimeErrors.length = 0;
|
runtimeErrors.length = 0;
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const localProgram = await createProgram();
|
const localProgram = createProgram();
|
||||||
await localProgram.parseAsync(["approvals", "get"], { from: "user" });
|
await localProgram.parseAsync(["approvals", "get"], { from: "user" });
|
||||||
|
|
||||||
expect(callGatewayFromCli).not.toHaveBeenCalled();
|
expect(callGatewayFromCli).not.toHaveBeenCalled();
|
||||||
expect(runtimeErrors).toHaveLength(0);
|
expect(runtimeErrors).toHaveLength(0);
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const gatewayProgram = await createProgram();
|
const gatewayProgram = createProgram();
|
||||||
await gatewayProgram.parseAsync(["approvals", "get", "--gateway"], { from: "user" });
|
await gatewayProgram.parseAsync(["approvals", "get", "--gateway"], { from: "user" });
|
||||||
|
|
||||||
expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.get", expect.anything(), {});
|
expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.get", expect.anything(), {});
|
||||||
expect(runtimeErrors).toHaveLength(0);
|
expect(runtimeErrors).toHaveLength(0);
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const nodeProgram = await createProgram();
|
const nodeProgram = createProgram();
|
||||||
await nodeProgram.parseAsync(["approvals", "get", "--node", "macbook"], { from: "user" });
|
await nodeProgram.parseAsync(["approvals", "get", "--node", "macbook"], { from: "user" });
|
||||||
|
|
||||||
expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.node.get", expect.anything(), {
|
expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.node.get", expect.anything(), {
|
||||||
@@ -101,11 +103,9 @@ describe("exec approvals CLI", () => {
|
|||||||
runtimeErrors.length = 0;
|
runtimeErrors.length = 0;
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const execApprovals = await import("../infra/exec-approvals.js");
|
|
||||||
const saveExecApprovals = vi.mocked(execApprovals.saveExecApprovals);
|
const saveExecApprovals = vi.mocked(execApprovals.saveExecApprovals);
|
||||||
saveExecApprovals.mockClear();
|
saveExecApprovals.mockClear();
|
||||||
|
|
||||||
const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
program.exitOverride();
|
program.exitOverride();
|
||||||
registerExecApprovalsCli(program);
|
registerExecApprovalsCli(program);
|
||||||
|
|||||||
+167
-171
@@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
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";
|
import type { UpdateRunResult } from "../infra/update-runner.js";
|
||||||
|
|
||||||
const confirm = vi.fn();
|
const confirm = vi.fn();
|
||||||
@@ -91,6 +91,23 @@ const { updateCommand, registerUpdateCli, updateStatusCommand, updateWizardComma
|
|||||||
await import("./update-cli.js");
|
await import("./update-cli.js");
|
||||||
|
|
||||||
describe("update-cli", () => {
|
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 = {
|
const baseSnapshot = {
|
||||||
valid: true,
|
valid: true,
|
||||||
config: {},
|
config: {},
|
||||||
@@ -223,41 +240,37 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to stable channel for package installs when unset", async () => {
|
it("defaults to stable channel for package installs when unset", async () => {
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
|
const tempDir = await createCaseDir("openclaw-update");
|
||||||
try {
|
await fs.writeFile(
|
||||||
await fs.writeFile(
|
path.join(tempDir, "package.json"),
|
||||||
path.join(tempDir, "package.json"),
|
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
|
||||||
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
|
"utf-8",
|
||||||
"utf-8",
|
);
|
||||||
);
|
|
||||||
|
|
||||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
root: tempDir,
|
root: tempDir,
|
||||||
installKind: "package",
|
installKind: "package",
|
||||||
packageManager: "npm",
|
packageManager: "npm",
|
||||||
deps: {
|
deps: {
|
||||||
manager: "npm",
|
manager: "npm",
|
||||||
status: "ok",
|
|
||||||
lockfilePath: null,
|
|
||||||
markerPath: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "npm",
|
lockfilePath: null,
|
||||||
steps: [],
|
markerPath: null,
|
||||||
durationMs: 100,
|
},
|
||||||
});
|
});
|
||||||
|
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];
|
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
|
||||||
expect(call?.channel).toBe("stable");
|
expect(call?.channel).toBe("stable");
|
||||||
expect(call?.tag).toBe("latest");
|
expect(call?.tag).toBe("latest");
|
||||||
} finally {
|
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses stored beta channel when configured", async () => {
|
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 () => {
|
it("falls back to latest when beta tag is older than release", async () => {
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
|
const tempDir = await createCaseDir("openclaw-update");
|
||||||
try {
|
await fs.writeFile(
|
||||||
await fs.writeFile(
|
path.join(tempDir, "package.json"),
|
||||||
path.join(tempDir, "package.json"),
|
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
|
||||||
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
|
"utf-8",
|
||||||
"utf-8",
|
);
|
||||||
);
|
|
||||||
|
|
||||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
||||||
...baseSnapshot,
|
...baseSnapshot,
|
||||||
config: { update: { channel: "beta" } },
|
config: { update: { channel: "beta" } },
|
||||||
});
|
});
|
||||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
root: tempDir,
|
root: tempDir,
|
||||||
installKind: "package",
|
installKind: "package",
|
||||||
packageManager: "npm",
|
packageManager: "npm",
|
||||||
deps: {
|
deps: {
|
||||||
manager: "npm",
|
manager: "npm",
|
||||||
status: "ok",
|
|
||||||
lockfilePath: null,
|
|
||||||
markerPath: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
|
||||||
tag: "latest",
|
|
||||||
version: "1.2.3-1",
|
|
||||||
});
|
|
||||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "npm",
|
lockfilePath: null,
|
||||||
steps: [],
|
markerPath: null,
|
||||||
durationMs: 100,
|
},
|
||||||
});
|
});
|
||||||
|
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];
|
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
|
||||||
expect(call?.channel).toBe("beta");
|
expect(call?.channel).toBe("beta");
|
||||||
expect(call?.tag).toBe("latest");
|
expect(call?.tag).toBe("latest");
|
||||||
} finally {
|
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("honors --tag override", async () => {
|
it("honors --tag override", async () => {
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
|
const tempDir = await createCaseDir("openclaw-update");
|
||||||
try {
|
await fs.writeFile(
|
||||||
await fs.writeFile(
|
path.join(tempDir, "package.json"),
|
||||||
path.join(tempDir, "package.json"),
|
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
|
||||||
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
|
"utf-8",
|
||||||
"utf-8",
|
);
|
||||||
);
|
|
||||||
|
|
||||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "npm",
|
mode: "npm",
|
||||||
steps: [],
|
steps: [],
|
||||||
durationMs: 100,
|
durationMs: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateCommand({ tag: "next" });
|
await updateCommand({ tag: "next" });
|
||||||
|
|
||||||
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
|
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
|
||||||
expect(call?.tag).toBe("next");
|
expect(call?.tag).toBe("next");
|
||||||
} finally {
|
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updateCommand outputs JSON when --json is set", async () => {
|
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 () => {
|
it("requires confirmation on downgrade when non-interactive", async () => {
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
|
const tempDir = await createCaseDir("openclaw-update");
|
||||||
try {
|
setTty(false);
|
||||||
setTty(false);
|
await fs.writeFile(
|
||||||
await fs.writeFile(
|
path.join(tempDir, "package.json"),
|
||||||
path.join(tempDir, "package.json"),
|
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
||||||
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
"utf-8",
|
||||||
"utf-8",
|
);
|
||||||
);
|
|
||||||
|
|
||||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
root: tempDir,
|
root: tempDir,
|
||||||
installKind: "package",
|
installKind: "package",
|
||||||
packageManager: "npm",
|
packageManager: "npm",
|
||||||
deps: {
|
deps: {
|
||||||
manager: "npm",
|
manager: "npm",
|
||||||
status: "ok",
|
|
||||||
lockfilePath: null,
|
|
||||||
markerPath: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
|
||||||
tag: "latest",
|
|
||||||
version: "0.0.1",
|
|
||||||
});
|
|
||||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "npm",
|
lockfilePath: null,
|
||||||
steps: [],
|
markerPath: null,
|
||||||
durationMs: 100,
|
},
|
||||||
});
|
});
|
||||||
vi.mocked(defaultRuntime.error).mockClear();
|
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||||
vi.mocked(defaultRuntime.exit).mockClear();
|
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(defaultRuntime.error).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Downgrade confirmation required."),
|
expect.stringContaining("Downgrade confirmation required."),
|
||||||
);
|
);
|
||||||
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
|
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
|
||||||
} finally {
|
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows downgrade with --yes in non-interactive mode", async () => {
|
it("allows downgrade with --yes in non-interactive mode", async () => {
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-"));
|
const tempDir = await createCaseDir("openclaw-update");
|
||||||
try {
|
setTty(false);
|
||||||
setTty(false);
|
await fs.writeFile(
|
||||||
await fs.writeFile(
|
path.join(tempDir, "package.json"),
|
||||||
path.join(tempDir, "package.json"),
|
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
||||||
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
"utf-8",
|
||||||
"utf-8",
|
);
|
||||||
);
|
|
||||||
|
|
||||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
root: tempDir,
|
root: tempDir,
|
||||||
installKind: "package",
|
installKind: "package",
|
||||||
packageManager: "npm",
|
packageManager: "npm",
|
||||||
deps: {
|
deps: {
|
||||||
manager: "npm",
|
manager: "npm",
|
||||||
status: "ok",
|
|
||||||
lockfilePath: null,
|
|
||||||
markerPath: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
|
||||||
tag: "latest",
|
|
||||||
version: "0.0.1",
|
|
||||||
});
|
|
||||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "npm",
|
lockfilePath: null,
|
||||||
steps: [],
|
markerPath: null,
|
||||||
durationMs: 100,
|
},
|
||||||
});
|
});
|
||||||
vi.mocked(defaultRuntime.error).mockClear();
|
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||||
vi.mocked(defaultRuntime.exit).mockClear();
|
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(defaultRuntime.error).not.toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Downgrade confirmation required."),
|
expect.stringContaining("Downgrade confirmation required."),
|
||||||
);
|
);
|
||||||
expect(runGatewayUpdate).toHaveBeenCalled();
|
expect(runGatewayUpdate).toHaveBeenCalled();
|
||||||
} finally {
|
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updateWizardCommand requires a TTY", async () => {
|
it("updateWizardCommand requires a TTY", async () => {
|
||||||
@@ -576,7 +573,7 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateWizardCommand offers dev checkout and forwards selections", async () => {
|
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;
|
const previousGitDir = process.env.OPENCLAW_GIT_DIR;
|
||||||
try {
|
try {
|
||||||
setTty(true);
|
setTty(true);
|
||||||
@@ -608,7 +605,6 @@ describe("update-cli", () => {
|
|||||||
expect(call?.channel).toBe("dev");
|
expect(call?.channel).toBe("dev");
|
||||||
} finally {
|
} finally {
|
||||||
process.env.OPENCLAW_GIT_DIR = previousGitDir;
|
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 fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
import path from "node:path";
|
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 { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
|
||||||
import { loadConfig } from "./config.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", () => {
|
describe("config identity defaults", () => {
|
||||||
let previousHome: string | undefined;
|
let fixtureRoot = "";
|
||||||
|
let fixtureCount = 0;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeAll(async () => {
|
||||||
previousHome = process.env.HOME;
|
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-identity-"));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterAll(async () => {
|
||||||
process.env.HOME = previousHome;
|
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
const writeAndLoadConfig = async (home: string, config: Record<string, unknown>) => {
|
const writeAndLoadConfig = async (home: string, config: Record<string, unknown>) => {
|
||||||
@@ -27,6 +61,30 @@ describe("config identity defaults", () => {
|
|||||||
return loadConfig();
|
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 () => {
|
it("does not derive mention defaults and only sets ackReactionScope when identity is present", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = await writeAndLoadConfig(home, {
|
const cfg = await writeAndLoadConfig(home, {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { afterAll, describe, expect, it } from "vitest";
|
||||||
import { validateConfigObjectWithPlugins } from "./config.js";
|
import { validateConfigObjectWithPlugins } from "./config.js";
|
||||||
import { withTempHome } from "./test-helpers.js";
|
|
||||||
|
|
||||||
async function writePluginFixture(params: {
|
async function writePluginFixture(params: {
|
||||||
dir: string;
|
dir: string;
|
||||||
@@ -31,145 +31,150 @@ async function writePluginFixture(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("config plugin validation", () => {
|
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) => {
|
const validateInHome = (home: string, raw: unknown) => {
|
||||||
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
|
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
|
||||||
return validateConfigObjectWithPlugins(raw);
|
return validateConfigObjectWithPlugins(raw);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects missing plugin load paths", async () => {
|
it("rejects missing plugin load paths", async () => {
|
||||||
await withTempHome(async (home) => {
|
const home = await createCaseHome();
|
||||||
const missingPath = path.join(home, "missing-plugin");
|
const missingPath = path.join(home, "missing-plugin");
|
||||||
const res = validateInHome(home, {
|
const res = validateInHome(home, {
|
||||||
agents: { list: [{ id: "pi" }] },
|
agents: { list: [{ id: "pi" }] },
|
||||||
plugins: { enabled: false, load: { paths: [missingPath] } },
|
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
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 () => {
|
it("rejects missing plugin ids in entries", async () => {
|
||||||
await withTempHome(async (home) => {
|
const home = await createCaseHome();
|
||||||
const res = validateInHome(home, {
|
const res = validateInHome(home, {
|
||||||
agents: { list: [{ id: "pi" }] },
|
agents: { list: [{ id: "pi" }] },
|
||||||
plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } },
|
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
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 () => {
|
it("rejects missing plugin ids in allow/deny/slots", async () => {
|
||||||
await withTempHome(async (home) => {
|
const home = await createCaseHome();
|
||||||
const res = validateInHome(home, {
|
const res = validateInHome(home, {
|
||||||
agents: { list: [{ id: "pi" }] },
|
agents: { list: [{ id: "pi" }] },
|
||||||
plugins: {
|
plugins: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
allow: ["missing-allow"],
|
allow: ["missing-allow"],
|
||||||
deny: ["missing-deny"],
|
deny: ["missing-deny"],
|
||||||
slots: { memory: "missing-slot" },
|
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" },
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
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 () => {
|
it("surfaces plugin config diagnostics", async () => {
|
||||||
await withTempHome(async (home) => {
|
const home = await createCaseHome();
|
||||||
const pluginDir = path.join(home, "bad-plugin");
|
const pluginDir = path.join(home, "bad-plugin");
|
||||||
await writePluginFixture({
|
await writePluginFixture({
|
||||||
dir: pluginDir,
|
dir: pluginDir,
|
||||||
id: "bad-plugin",
|
id: "bad-plugin",
|
||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "boolean" },
|
value: { type: "boolean" },
|
||||||
},
|
|
||||||
required: ["value"],
|
|
||||||
},
|
},
|
||||||
});
|
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
it("accepts known plugin ids", async () => {
|
||||||
await withTempHome(async (home) => {
|
const home = await createCaseHome();
|
||||||
const res = validateInHome(home, {
|
const res = validateInHome(home, {
|
||||||
agents: { list: [{ id: "pi" }] },
|
agents: { list: [{ id: "pi" }] },
|
||||||
plugins: { enabled: false, entries: { discord: { enabled: true } } },
|
plugins: { enabled: false, entries: { discord: { enabled: true } } },
|
||||||
});
|
|
||||||
expect(res.ok).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts plugin heartbeat targets", async () => {
|
it("accepts plugin heartbeat targets", async () => {
|
||||||
await withTempHome(async (home) => {
|
const home = await createCaseHome();
|
||||||
const pluginDir = path.join(home, "bluebubbles-plugin");
|
const pluginDir = path.join(home, "bluebubbles-plugin");
|
||||||
await writePluginFixture({
|
await writePluginFixture({
|
||||||
dir: pluginDir,
|
dir: pluginDir,
|
||||||
id: "bluebubbles-plugin",
|
id: "bluebubbles-plugin",
|
||||||
channels: ["bluebubbles"],
|
channels: ["bluebubbles"],
|
||||||
schema: { type: "object" },
|
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 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 () => {
|
it("rejects unknown heartbeat targets", async () => {
|
||||||
await withTempHome(async (home) => {
|
const home = await createCaseHome();
|
||||||
const res = validateInHome(home, {
|
const res = validateInHome(home, {
|
||||||
agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] },
|
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
expect(res.issues).toContainEqual({
|
||||||
|
path: "agents.defaults.heartbeat.target",
|
||||||
|
message: "unknown heartbeat target: not-a-channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-18
@@ -4,28 +4,29 @@ import fs from "node:fs";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import * as tar from "tar";
|
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", () => ({
|
vi.mock("../process/exec.js", () => ({
|
||||||
runCommandWithTimeout: vi.fn(),
|
runCommandWithTimeout: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function makeTempDir() {
|
function makeTempDir() {
|
||||||
const dir = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`);
|
const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`);
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
tempDirs.push(dir);
|
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
const { runCommandWithTimeout } = await import("../process/exec.js");
|
||||||
for (const dir of tempDirs.splice(0)) {
|
const { installHooksFromArchive, installHooksFromPath } = await import("./install.js");
|
||||||
try {
|
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
afterAll(() => {
|
||||||
} catch {
|
try {
|
||||||
// ignore cleanup failures
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||||
}
|
} catch {
|
||||||
|
// ignore cleanup failures
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +62,6 @@ describe("installHooksFromArchive", () => {
|
|||||||
fs.writeFileSync(archivePath, buffer);
|
fs.writeFileSync(archivePath, buffer);
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const { installHooksFromArchive } = await import("./install.js");
|
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
@@ -111,7 +111,6 @@ describe("installHooksFromArchive", () => {
|
|||||||
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const { installHooksFromArchive } = await import("./install.js");
|
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
@@ -160,7 +159,6 @@ describe("installHooksFromArchive", () => {
|
|||||||
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const { installHooksFromArchive } = await import("./install.js");
|
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
|
|
||||||
expect(result.ok).toBe(false);
|
expect(result.ok).toBe(false);
|
||||||
@@ -207,7 +205,6 @@ describe("installHooksFromArchive", () => {
|
|||||||
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const { installHooksFromArchive } = await import("./install.js");
|
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
|
|
||||||
expect(result.ok).toBe(false);
|
expect(result.ok).toBe(false);
|
||||||
@@ -253,11 +250,9 @@ describe("installHooksFromPath", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const { runCommandWithTimeout } = await import("../process/exec.js");
|
|
||||||
const run = vi.mocked(runCommandWithTimeout);
|
const run = vi.mocked(runCommandWithTimeout);
|
||||||
run.mockResolvedValue({ code: 0, stdout: "", stderr: "" });
|
run.mockResolvedValue({ code: 0, stdout: "", stderr: "" });
|
||||||
|
|
||||||
const { installHooksFromPath } = await import("./install.js");
|
|
||||||
const res = await installHooksFromPath({
|
const res = await installHooksFromPath({
|
||||||
path: pkgDir,
|
path: pkgDir,
|
||||||
hooksDir: path.join(stateDir, "hooks"),
|
hooksDir: path.join(stateDir, "hooks"),
|
||||||
@@ -301,7 +296,6 @@ describe("installHooksFromPath", () => {
|
|||||||
fs.writeFileSync(path.join(hookDir, "handler.ts"), "export default async () => {};\n");
|
fs.writeFileSync(path.join(hookDir, "handler.ts"), "export default async () => {};\n");
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const { installHooksFromPath } = await import("./install.js");
|
|
||||||
const result = await installHooksFromPath({ path: hookDir, hooksDir });
|
const result = await installHooksFromPath({ path: hookDir, hooksDir });
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
|
|||||||
+12
-11
@@ -2,19 +2,19 @@ import { randomUUID } from "node:crypto";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
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";
|
import { loadOpenClawPlugins } from "./loader.js";
|
||||||
|
|
||||||
type TempPlugin = { dir: string; file: string; id: string };
|
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 prevBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||||
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
|
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
|
||||||
|
|
||||||
function makeTempDir() {
|
function makeTempDir() {
|
||||||
const dir = path.join(os.tmpdir(), `openclaw-plugin-${randomUUID()}`);
|
const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`);
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
tempDirs.push(dir);
|
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,13 +44,6 @@ function writePlugin(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
for (const dir of tempDirs.splice(0)) {
|
|
||||||
try {
|
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
|
||||||
} catch {
|
|
||||||
// ignore cleanup failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (prevBundledDir === undefined) {
|
if (prevBundledDir === undefined) {
|
||||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||||
} else {
|
} else {
|
||||||
@@ -58,6 +51,14 @@ afterEach(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
try {
|
||||||
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup failures
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe("loadOpenClawPlugins", () => {
|
describe("loadOpenClawPlugins", () => {
|
||||||
it("disables bundled plugins by default", () => {
|
it("disables bundled plugins by default", () => {
|
||||||
const bundledDir = makeTempDir();
|
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", () => {
|
describe("attachChildProcessBridge", () => {
|
||||||
const children: Array<{ kill: (signal?: NodeJS.Signals) => boolean }> = [];
|
const children: Array<{ kill: (signal?: NodeJS.Signals) => boolean }> = [];
|
||||||
const detachments: Array<() => void> = [];
|
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);
|
expect(await canConnect(port)).toBe(false);
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-10
@@ -2,19 +2,16 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import sharp from "sharp";
|
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 * as ssrf from "../infra/net/ssrf.js";
|
||||||
import { optimizeImageToPng } from "../media/image-ops.js";
|
import { optimizeImageToPng } from "../media/image-ops.js";
|
||||||
import { loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } from "./media.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> {
|
async function writeTempFile(buffer: Buffer, ext: string): Promise<string> {
|
||||||
const file = path.join(
|
const file = path.join(fixtureRoot, `media-${fixtureFileCount++}${ext}`);
|
||||||
os.tmpdir(),
|
|
||||||
`openclaw-media-${Date.now()}-${Math.random().toString(16).slice(2)}${ext}`,
|
|
||||||
);
|
|
||||||
tmpFiles.push(file);
|
|
||||||
await fs.writeFile(file, buffer);
|
await fs.writeFile(file, buffer);
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
@@ -45,9 +42,15 @@ async function createLargeTestJpeg(): Promise<{ buffer: Buffer; file: string }>
|
|||||||
return { buffer, file };
|
return { buffer, file };
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
beforeAll(async () => {
|
||||||
await Promise.all(tmpFiles.map((file) => fs.rm(file, { force: true })));
|
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-test-"));
|
||||||
tmpFiles.length = 0;
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user