mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 01:08:28 +00:00
perf(test): trim browser and models suite overhead
This commit is contained in:
@@ -1,511 +0,0 @@
|
|||||||
import { type AddressInfo, createServer } from "node:net";
|
|
||||||
import { fetch as realFetch } from "undici";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
let testPort = 0;
|
|
||||||
let _cdpBaseUrl = "";
|
|
||||||
let reachable = false;
|
|
||||||
let cfgAttachOnly = false;
|
|
||||||
let createTargetId: string | null = null;
|
|
||||||
let prevGatewayPort: string | undefined;
|
|
||||||
|
|
||||||
const cdpMocks = vi.hoisted(() => ({
|
|
||||||
createTargetViaCdp: vi.fn(async () => {
|
|
||||||
throw new Error("cdp disabled");
|
|
||||||
}),
|
|
||||||
snapshotAria: vi.fn(async () => ({
|
|
||||||
nodes: [{ ref: "1", role: "link", name: "x", depth: 0 }],
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const pwMocks = vi.hoisted(() => ({
|
|
||||||
armDialogViaPlaywright: vi.fn(async () => {}),
|
|
||||||
armFileUploadViaPlaywright: vi.fn(async () => {}),
|
|
||||||
clickViaPlaywright: vi.fn(async () => {}),
|
|
||||||
closePageViaPlaywright: vi.fn(async () => {}),
|
|
||||||
closePlaywrightBrowserConnection: vi.fn(async () => {}),
|
|
||||||
downloadViaPlaywright: vi.fn(async () => ({
|
|
||||||
url: "https://example.com/report.pdf",
|
|
||||||
suggestedFilename: "report.pdf",
|
|
||||||
path: "/tmp/report.pdf",
|
|
||||||
})),
|
|
||||||
dragViaPlaywright: vi.fn(async () => {}),
|
|
||||||
evaluateViaPlaywright: vi.fn(async () => "ok"),
|
|
||||||
fillFormViaPlaywright: vi.fn(async () => {}),
|
|
||||||
getConsoleMessagesViaPlaywright: vi.fn(async () => []),
|
|
||||||
hoverViaPlaywright: vi.fn(async () => {}),
|
|
||||||
scrollIntoViewViaPlaywright: vi.fn(async () => {}),
|
|
||||||
navigateViaPlaywright: vi.fn(async () => ({ url: "https://example.com" })),
|
|
||||||
pdfViaPlaywright: vi.fn(async () => ({ buffer: Buffer.from("pdf") })),
|
|
||||||
pressKeyViaPlaywright: vi.fn(async () => {}),
|
|
||||||
responseBodyViaPlaywright: vi.fn(async () => ({
|
|
||||||
url: "https://example.com/api/data",
|
|
||||||
status: 200,
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
body: '{"ok":true}',
|
|
||||||
})),
|
|
||||||
resizeViewportViaPlaywright: vi.fn(async () => {}),
|
|
||||||
selectOptionViaPlaywright: vi.fn(async () => {}),
|
|
||||||
setInputFilesViaPlaywright: vi.fn(async () => {}),
|
|
||||||
snapshotAiViaPlaywright: vi.fn(async () => ({ snapshot: "ok" })),
|
|
||||||
takeScreenshotViaPlaywright: vi.fn(async () => ({
|
|
||||||
buffer: Buffer.from("png"),
|
|
||||||
})),
|
|
||||||
typeViaPlaywright: vi.fn(async () => {}),
|
|
||||||
waitForDownloadViaPlaywright: vi.fn(async () => ({
|
|
||||||
url: "https://example.com/report.pdf",
|
|
||||||
suggestedFilename: "report.pdf",
|
|
||||||
path: "/tmp/report.pdf",
|
|
||||||
})),
|
|
||||||
waitForViaPlaywright: vi.fn(async () => {}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
function makeProc(pid = 123) {
|
|
||||||
const handlers = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
||||||
return {
|
|
||||||
pid,
|
|
||||||
killed: false,
|
|
||||||
exitCode: null as number | null,
|
|
||||||
on: (event: string, cb: (...args: unknown[]) => void) => {
|
|
||||||
handlers.set(event, [...(handlers.get(event) ?? []), cb]);
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
emitExit: () => {
|
|
||||||
for (const cb of handlers.get("exit") ?? []) {
|
|
||||||
cb(0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
kill: () => {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const proc = makeProc();
|
|
||||||
|
|
||||||
vi.mock("../config/config.js", async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
loadConfig: () => ({
|
|
||||||
browser: {
|
|
||||||
enabled: true,
|
|
||||||
color: "#FF4500",
|
|
||||||
attachOnly: cfgAttachOnly,
|
|
||||||
headless: true,
|
|
||||||
defaultProfile: "openclaw",
|
|
||||||
profiles: {
|
|
||||||
openclaw: { cdpPort: testPort + 1, color: "#FF4500" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
writeConfigFile: vi.fn(async () => {}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>);
|
|
||||||
vi.mock("./chrome.js", () => ({
|
|
||||||
isChromeCdpReady: vi.fn(async () => reachable),
|
|
||||||
isChromeReachable: vi.fn(async () => reachable),
|
|
||||||
launchOpenClawChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => {
|
|
||||||
launchCalls.push({ port: profile.cdpPort });
|
|
||||||
reachable = true;
|
|
||||||
return {
|
|
||||||
pid: 123,
|
|
||||||
exe: { kind: "chrome", path: "/fake/chrome" },
|
|
||||||
userDataDir: "/tmp/openclaw",
|
|
||||||
cdpPort: profile.cdpPort,
|
|
||||||
startedAt: Date.now(),
|
|
||||||
proc,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
resolveOpenClawUserDataDir: vi.fn(() => "/tmp/openclaw"),
|
|
||||||
stopOpenClawChrome: vi.fn(async () => {
|
|
||||||
reachable = false;
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("./cdp.js", () => ({
|
|
||||||
createTargetViaCdp: cdpMocks.createTargetViaCdp,
|
|
||||||
normalizeCdpWsUrl: vi.fn((wsUrl: string) => wsUrl),
|
|
||||||
snapshotAria: cdpMocks.snapshotAria,
|
|
||||||
getHeadersWithAuth: vi.fn(() => ({})),
|
|
||||||
appendCdpPath: vi.fn((cdpUrl: string, path: string) => {
|
|
||||||
const base = cdpUrl.replace(/\/$/, "");
|
|
||||||
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
||||||
return `${base}${suffix}`;
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("./pw-ai.js", () => pwMocks);
|
|
||||||
|
|
||||||
vi.mock("../media/store.js", () => ({
|
|
||||||
ensureMediaDir: vi.fn(async () => {}),
|
|
||||||
saveMediaBuffer: vi.fn(async () => ({ path: "/tmp/fake.png" })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("./screenshot.js", () => ({
|
|
||||||
DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES: 128,
|
|
||||||
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE: 64,
|
|
||||||
normalizeBrowserScreenshot: vi.fn(async (buf: Buffer) => ({
|
|
||||||
buffer: buf,
|
|
||||||
contentType: "image/png",
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
async function getFreePort(): Promise<number> {
|
|
||||||
while (true) {
|
|
||||||
const port = await new Promise<number>((resolve, reject) => {
|
|
||||||
const s = createServer();
|
|
||||||
s.once("error", reject);
|
|
||||||
s.listen(0, "127.0.0.1", () => {
|
|
||||||
const assigned = (s.address() as AddressInfo).port;
|
|
||||||
s.close((err) => (err ? reject(err) : resolve(assigned)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (port < 65535) {
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeResponse(
|
|
||||||
body: unknown,
|
|
||||||
init?: { ok?: boolean; status?: number; text?: string },
|
|
||||||
): Response {
|
|
||||||
const ok = init?.ok ?? true;
|
|
||||||
const status = init?.status ?? 200;
|
|
||||||
const text = init?.text ?? "";
|
|
||||||
return {
|
|
||||||
ok,
|
|
||||||
status,
|
|
||||||
json: async () => body,
|
|
||||||
text: async () => text,
|
|
||||||
} as unknown as Response;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("browser control server", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
reachable = false;
|
|
||||||
cfgAttachOnly = false;
|
|
||||||
createTargetId = null;
|
|
||||||
|
|
||||||
cdpMocks.createTargetViaCdp.mockImplementation(async () => {
|
|
||||||
if (createTargetId) {
|
|
||||||
return { targetId: createTargetId };
|
|
||||||
}
|
|
||||||
throw new Error("cdp disabled");
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const fn of Object.values(pwMocks)) {
|
|
||||||
fn.mockClear();
|
|
||||||
}
|
|
||||||
for (const fn of Object.values(cdpMocks)) {
|
|
||||||
fn.mockClear();
|
|
||||||
}
|
|
||||||
|
|
||||||
testPort = await getFreePort();
|
|
||||||
_cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`;
|
|
||||||
prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT;
|
|
||||||
process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2);
|
|
||||||
|
|
||||||
// Minimal CDP JSON endpoints used by the server.
|
|
||||||
let putNewCalls = 0;
|
|
||||||
vi.stubGlobal(
|
|
||||||
"fetch",
|
|
||||||
vi.fn(async (url: string, init?: RequestInit) => {
|
|
||||||
const u = String(url);
|
|
||||||
if (u.includes("/json/list")) {
|
|
||||||
if (!reachable) {
|
|
||||||
return makeResponse([]);
|
|
||||||
}
|
|
||||||
return makeResponse([
|
|
||||||
{
|
|
||||||
id: "abcd1234",
|
|
||||||
title: "Tab",
|
|
||||||
url: "https://example.com",
|
|
||||||
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/abcd1234",
|
|
||||||
type: "page",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "abce9999",
|
|
||||||
title: "Other",
|
|
||||||
url: "https://other",
|
|
||||||
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/abce9999",
|
|
||||||
type: "page",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if (u.includes("/json/new?")) {
|
|
||||||
if (init?.method === "PUT") {
|
|
||||||
putNewCalls += 1;
|
|
||||||
if (putNewCalls === 1) {
|
|
||||||
return makeResponse({}, { ok: false, status: 405, text: "" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return makeResponse({
|
|
||||||
id: "newtab1",
|
|
||||||
title: "",
|
|
||||||
url: "about:blank",
|
|
||||||
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/newtab1",
|
|
||||||
type: "page",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (u.includes("/json/activate/")) {
|
|
||||||
return makeResponse("ok");
|
|
||||||
}
|
|
||||||
if (u.includes("/json/close/")) {
|
|
||||||
return makeResponse("ok");
|
|
||||||
}
|
|
||||||
return makeResponse({}, { ok: false, status: 500, text: "unexpected" });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
vi.unstubAllGlobals();
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
if (prevGatewayPort === undefined) {
|
|
||||||
delete process.env.OPENCLAW_GATEWAY_PORT;
|
|
||||||
} else {
|
|
||||||
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
|
||||||
}
|
|
||||||
const { stopBrowserControlServer } = await import("./server.js");
|
|
||||||
await stopBrowserControlServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("covers additional endpoint branches", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
const tabsWhenStopped = (await realFetch(`${base}/tabs`).then((r) => r.json())) as {
|
|
||||||
running: boolean;
|
|
||||||
tabs: unknown[];
|
|
||||||
};
|
|
||||||
expect(tabsWhenStopped.running).toBe(false);
|
|
||||||
expect(Array.isArray(tabsWhenStopped.tabs)).toBe(true);
|
|
||||||
|
|
||||||
const focusStopped = await realFetch(`${base}/tabs/focus`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ targetId: "abcd" }),
|
|
||||||
});
|
|
||||||
expect(focusStopped.status).toBe(409);
|
|
||||||
|
|
||||||
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
|
|
||||||
|
|
||||||
const focusMissing = await realFetch(`${base}/tabs/focus`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ targetId: "zzz" }),
|
|
||||||
});
|
|
||||||
expect(focusMissing.status).toBe(404);
|
|
||||||
|
|
||||||
const delAmbiguous = await realFetch(`${base}/tabs/abc`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
expect(delAmbiguous.status).toBe(409);
|
|
||||||
|
|
||||||
const snapAmbiguous = await realFetch(`${base}/snapshot?format=aria&targetId=abc`);
|
|
||||||
expect(snapAmbiguous.status).toBe(409);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("backward compatibility (profile parameter)", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
reachable = false;
|
|
||||||
cfgAttachOnly = false;
|
|
||||||
createTargetId = null;
|
|
||||||
|
|
||||||
for (const fn of Object.values(pwMocks)) {
|
|
||||||
fn.mockClear();
|
|
||||||
}
|
|
||||||
for (const fn of Object.values(cdpMocks)) {
|
|
||||||
fn.mockClear();
|
|
||||||
}
|
|
||||||
|
|
||||||
testPort = await getFreePort();
|
|
||||||
_cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`;
|
|
||||||
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) => {
|
|
||||||
const u = String(url);
|
|
||||||
if (u.includes("/json/list")) {
|
|
||||||
if (!reachable) {
|
|
||||||
return makeResponse([]);
|
|
||||||
}
|
|
||||||
return makeResponse([
|
|
||||||
{
|
|
||||||
id: "abcd1234",
|
|
||||||
title: "Tab",
|
|
||||||
url: "https://example.com",
|
|
||||||
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/abcd1234",
|
|
||||||
type: "page",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if (u.includes("/json/new?")) {
|
|
||||||
return makeResponse({
|
|
||||||
id: "newtab1",
|
|
||||||
title: "",
|
|
||||||
url: "about:blank",
|
|
||||||
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/newtab1",
|
|
||||||
type: "page",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (u.includes("/json/activate/")) {
|
|
||||||
return makeResponse("ok");
|
|
||||||
}
|
|
||||||
if (u.includes("/json/close/")) {
|
|
||||||
return makeResponse("ok");
|
|
||||||
}
|
|
||||||
return makeResponse({}, { ok: false, status: 500, text: "unexpected" });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
vi.unstubAllGlobals();
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
if (prevGatewayPort === undefined) {
|
|
||||||
delete process.env.OPENCLAW_GATEWAY_PORT;
|
|
||||||
} else {
|
|
||||||
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
|
|
||||||
}
|
|
||||||
const { stopBrowserControlServer } = await import("./server.js");
|
|
||||||
await stopBrowserControlServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("GET / without profile uses default profile", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
const status = (await realFetch(`${base}/`).then((r) => r.json())) as {
|
|
||||||
running: boolean;
|
|
||||||
profile?: string;
|
|
||||||
};
|
|
||||||
expect(status.running).toBe(false);
|
|
||||||
// Should use default profile (openclaw)
|
|
||||||
expect(status.profile).toBe("openclaw");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("POST /start without profile uses default profile", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
const result = (await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json())) as {
|
|
||||||
ok: boolean;
|
|
||||||
profile?: string;
|
|
||||||
};
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
expect(result.profile).toBe("openclaw");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("POST /stop without profile uses default profile", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
await realFetch(`${base}/start`, { method: "POST" });
|
|
||||||
|
|
||||||
const result = (await realFetch(`${base}/stop`, { method: "POST" }).then((r) => r.json())) as {
|
|
||||||
ok: boolean;
|
|
||||||
profile?: string;
|
|
||||||
};
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
expect(result.profile).toBe("openclaw");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("GET /tabs without profile uses default profile", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
await realFetch(`${base}/start`, { method: "POST" });
|
|
||||||
|
|
||||||
const result = (await realFetch(`${base}/tabs`).then((r) => r.json())) as {
|
|
||||||
running: boolean;
|
|
||||||
tabs: unknown[];
|
|
||||||
};
|
|
||||||
expect(result.running).toBe(true);
|
|
||||||
expect(Array.isArray(result.tabs)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("POST /tabs/open without profile uses default profile", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
await realFetch(`${base}/start`, { method: "POST" });
|
|
||||||
|
|
||||||
const result = (await realFetch(`${base}/tabs/open`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ url: "https://example.com" }),
|
|
||||||
}).then((r) => r.json())) as { targetId?: string };
|
|
||||||
expect(result.targetId).toBe("newtab1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("GET /profiles returns list of profiles", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
const result = (await realFetch(`${base}/profiles`).then((r) => r.json())) as {
|
|
||||||
profiles: Array<{ name: string }>;
|
|
||||||
};
|
|
||||||
expect(Array.isArray(result.profiles)).toBe(true);
|
|
||||||
// Should at least have the default openclaw profile
|
|
||||||
expect(result.profiles.some((p) => p.name === "openclaw")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("GET /tabs?profile=openclaw returns tabs for specified profile", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
await realFetch(`${base}/start`, { method: "POST" });
|
|
||||||
|
|
||||||
const result = (await realFetch(`${base}/tabs?profile=openclaw`).then((r) => r.json())) as {
|
|
||||||
running: boolean;
|
|
||||||
tabs: unknown[];
|
|
||||||
};
|
|
||||||
expect(result.running).toBe(true);
|
|
||||||
expect(Array.isArray(result.tabs)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("POST /tabs/open?profile=openclaw opens tab in specified profile", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
await realFetch(`${base}/start`, { method: "POST" });
|
|
||||||
|
|
||||||
const result = (await realFetch(`${base}/tabs/open?profile=openclaw`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ url: "https://example.com" }),
|
|
||||||
}).then((r) => r.json())) as { targetId?: string };
|
|
||||||
expect(result.targetId).toBe("newtab1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("GET /tabs?profile=unknown returns 404", async () => {
|
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
|
||||||
await startBrowserControlServerFromConfig();
|
|
||||||
const base = `http://127.0.0.1:${testPort}`;
|
|
||||||
|
|
||||||
const result = await realFetch(`${base}/tabs?profile=unknown`);
|
|
||||||
expect(result.status).toBe(404);
|
|
||||||
const body = (await result.json()) as { error: string };
|
|
||||||
expect(body.error).toContain("not found");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -457,6 +457,111 @@ describe("browser control server", () => {
|
|||||||
expect(started.error ?? "").toMatch(/attachOnly/i);
|
expect(started.error ?? "").toMatch(/attachOnly/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("covers additional endpoint branches", async () => {
|
||||||
|
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
||||||
|
await startBrowserControlServerFromConfig();
|
||||||
|
const base = `http://127.0.0.1:${testPort}`;
|
||||||
|
|
||||||
|
const tabsWhenStopped = (await realFetch(`${base}/tabs`).then((r) => r.json())) as {
|
||||||
|
running: boolean;
|
||||||
|
tabs: unknown[];
|
||||||
|
};
|
||||||
|
expect(tabsWhenStopped.running).toBe(false);
|
||||||
|
expect(Array.isArray(tabsWhenStopped.tabs)).toBe(true);
|
||||||
|
|
||||||
|
const focusStopped = await realFetch(`${base}/tabs/focus`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ targetId: "abcd" }),
|
||||||
|
});
|
||||||
|
expect(focusStopped.status).toBe(409);
|
||||||
|
|
||||||
|
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
|
||||||
|
|
||||||
|
const focusMissing = await realFetch(`${base}/tabs/focus`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ targetId: "zzz" }),
|
||||||
|
});
|
||||||
|
expect(focusMissing.status).toBe(404);
|
||||||
|
|
||||||
|
const delAmbiguous = await realFetch(`${base}/tabs/abc`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
expect(delAmbiguous.status).toBe(409);
|
||||||
|
|
||||||
|
const snapAmbiguous = await realFetch(`${base}/snapshot?format=aria&targetId=abc`);
|
||||||
|
expect(snapAmbiguous.status).toBe(409);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles backward-compatible profile routes", async () => {
|
||||||
|
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
||||||
|
await startBrowserControlServerFromConfig();
|
||||||
|
const base = `http://127.0.0.1:${testPort}`;
|
||||||
|
|
||||||
|
const status = (await realFetch(`${base}/`).then((r) => r.json())) as {
|
||||||
|
running: boolean;
|
||||||
|
profile?: string;
|
||||||
|
};
|
||||||
|
expect(status.running).toBe(false);
|
||||||
|
expect(status.profile).toBe("openclaw");
|
||||||
|
|
||||||
|
const started = (await realFetch(`${base}/start`, { method: "POST" }).then((r) =>
|
||||||
|
r.json(),
|
||||||
|
)) as { ok: boolean; profile?: string };
|
||||||
|
expect(started.ok).toBe(true);
|
||||||
|
expect(started.profile).toBe("openclaw");
|
||||||
|
|
||||||
|
const stopped = (await realFetch(`${base}/stop`, { method: "POST" }).then((r) => r.json())) as {
|
||||||
|
ok: boolean;
|
||||||
|
profile?: string;
|
||||||
|
};
|
||||||
|
expect(stopped.ok).toBe(true);
|
||||||
|
expect(stopped.profile).toBe("openclaw");
|
||||||
|
|
||||||
|
await realFetch(`${base}/start`, { method: "POST" });
|
||||||
|
|
||||||
|
const tabsDefault = (await realFetch(`${base}/tabs`).then((r) => r.json())) as {
|
||||||
|
running: boolean;
|
||||||
|
tabs: unknown[];
|
||||||
|
};
|
||||||
|
expect(tabsDefault.running).toBe(true);
|
||||||
|
expect(Array.isArray(tabsDefault.tabs)).toBe(true);
|
||||||
|
|
||||||
|
const openDefault = (await realFetch(`${base}/tabs/open`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ url: "https://example.com" }),
|
||||||
|
}).then((r) => r.json())) as { targetId?: string };
|
||||||
|
expect(openDefault.targetId).toBe("newtab1");
|
||||||
|
|
||||||
|
const profiles = (await realFetch(`${base}/profiles`).then((r) => r.json())) as {
|
||||||
|
profiles: Array<{ name: string }>;
|
||||||
|
};
|
||||||
|
expect(profiles.profiles.some((profile) => profile.name === "openclaw")).toBe(true);
|
||||||
|
|
||||||
|
const tabsByProfile = (await realFetch(`${base}/tabs?profile=openclaw`).then((r) =>
|
||||||
|
r.json(),
|
||||||
|
)) as {
|
||||||
|
running: boolean;
|
||||||
|
tabs: unknown[];
|
||||||
|
};
|
||||||
|
expect(tabsByProfile.running).toBe(true);
|
||||||
|
expect(Array.isArray(tabsByProfile.tabs)).toBe(true);
|
||||||
|
|
||||||
|
const openByProfile = (await realFetch(`${base}/tabs/open?profile=openclaw`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ url: "https://example.com" }),
|
||||||
|
}).then((r) => r.json())) as { targetId?: string };
|
||||||
|
expect(openByProfile.targetId).toBe("newtab1");
|
||||||
|
|
||||||
|
const unknownProfile = await realFetch(`${base}/tabs?profile=unknown`);
|
||||||
|
expect(unknownProfile.status).toBe(404);
|
||||||
|
const unknownPayload = (await unknownProfile.json()) as { error: string };
|
||||||
|
expect(unknownPayload.error).toContain("not found");
|
||||||
|
});
|
||||||
|
|
||||||
it("allows attachOnly servers to ensure reachability via callback", async () => {
|
it("allows attachOnly servers to ensure reachability via callback", async () => {
|
||||||
cfgAttachOnly = true;
|
cfgAttachOnly = true;
|
||||||
reachable = false;
|
reachable = false;
|
||||||
|
|||||||
@@ -101,33 +101,6 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("models list/status", () => {
|
describe("models list/status", () => {
|
||||||
it("models status resolves z.ai alias to canonical zai", async () => {
|
|
||||||
loadConfig.mockReturnValue({
|
|
||||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
|
||||||
});
|
|
||||||
const runtime = makeRuntime();
|
|
||||||
|
|
||||||
const { modelsStatusCommand } = await import("./models/list.js");
|
|
||||||
await modelsStatusCommand({ json: true }, runtime);
|
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
|
||||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0]));
|
|
||||||
expect(payload.resolvedDefault).toBe("zai/glm-4.7");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("models status plain outputs canonical zai model", async () => {
|
|
||||||
loadConfig.mockReturnValue({
|
|
||||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
|
||||||
});
|
|
||||||
const runtime = makeRuntime();
|
|
||||||
|
|
||||||
const { modelsStatusCommand } = await import("./models/list.js");
|
|
||||||
await modelsStatusCommand({ plain: true }, runtime);
|
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
|
||||||
expect(runtime.log.mock.calls[0]?.[0]).toBe("zai/glm-4.7");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("models list outputs canonical zai key for configured z.ai model", async () => {
|
it("models list outputs canonical zai key for configured z.ai model", async () => {
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
agents: { defaults: { model: "z.ai/glm-4.7" } },
|
||||||
@@ -146,7 +119,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = [model];
|
modelRegistryState.models = [model];
|
||||||
modelRegistryState.available = [model];
|
modelRegistryState.available = [model];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -172,7 +145,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = [model];
|
modelRegistryState.models = [model];
|
||||||
modelRegistryState.available = [model];
|
modelRegistryState.available = [model];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ plain: true }, runtime);
|
await modelsListCommand({ plain: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -207,7 +180,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = models;
|
modelRegistryState.models = models;
|
||||||
modelRegistryState.available = models;
|
modelRegistryState.available = models;
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ all: true, provider: "z.ai", json: true }, runtime);
|
await modelsListCommand({ all: true, provider: "z.ai", json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -244,7 +217,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = models;
|
modelRegistryState.models = models;
|
||||||
modelRegistryState.available = models;
|
modelRegistryState.available = models;
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ all: true, provider: "Z.AI", json: true }, runtime);
|
await modelsListCommand({ all: true, provider: "Z.AI", json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -281,7 +254,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = models;
|
modelRegistryState.models = models;
|
||||||
modelRegistryState.available = models;
|
modelRegistryState.available = models;
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ all: true, provider: "z-ai", json: true }, runtime);
|
await modelsListCommand({ all: true, provider: "z-ai", json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -308,7 +281,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = [model];
|
modelRegistryState.models = [model];
|
||||||
modelRegistryState.available = [];
|
modelRegistryState.available = [];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ all: true, json: true }, runtime);
|
await modelsListCommand({ all: true, json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -345,7 +318,7 @@ describe("models list/status", () => {
|
|||||||
];
|
];
|
||||||
modelRegistryState.available = [];
|
modelRegistryState.available = [];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -385,7 +358,7 @@ describe("models list/status", () => {
|
|||||||
];
|
];
|
||||||
modelRegistryState.available = [];
|
modelRegistryState.available = [];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -424,7 +397,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = [template];
|
modelRegistryState.models = [template];
|
||||||
modelRegistryState.available = [template];
|
modelRegistryState.available = [template];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -462,7 +435,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = [template];
|
modelRegistryState.models = [template];
|
||||||
modelRegistryState.available = [template];
|
modelRegistryState.available = [template];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -505,7 +478,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = [template];
|
modelRegistryState.models = [template];
|
||||||
modelRegistryState.available = [];
|
modelRegistryState.available = [];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -554,7 +527,7 @@ describe("models list/status", () => {
|
|||||||
];
|
];
|
||||||
modelRegistryState.available = [];
|
modelRegistryState.available = [];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.error).toHaveBeenCalledTimes(1);
|
expect(runtime.error).toHaveBeenCalledTimes(1);
|
||||||
@@ -601,7 +574,7 @@ describe("models list/status", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.error).toHaveBeenCalledTimes(1);
|
expect(runtime.error).toHaveBeenCalledTimes(1);
|
||||||
@@ -651,7 +624,7 @@ describe("models list/status", () => {
|
|||||||
];
|
];
|
||||||
modelRegistryState.available = [];
|
modelRegistryState.available = [];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.error).toHaveBeenCalledTimes(1);
|
expect(runtime.error).toHaveBeenCalledTimes(1);
|
||||||
@@ -682,7 +655,7 @@ describe("models list/status", () => {
|
|||||||
});
|
});
|
||||||
const runtime = makeRuntime();
|
const runtime = makeRuntime();
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.error).toHaveBeenCalledTimes(1);
|
expect(runtime.error).toHaveBeenCalledTimes(1);
|
||||||
@@ -717,7 +690,7 @@ describe("models list/status", () => {
|
|||||||
modelRegistryState.models = [];
|
modelRegistryState.models = [];
|
||||||
modelRegistryState.available = [];
|
modelRegistryState.available = [];
|
||||||
|
|
||||||
const { modelsListCommand } = await import("./models/list.js");
|
const { modelsListCommand } = await import("./models/list.list-command.js");
|
||||||
await modelsListCommand({ json: true }, runtime);
|
await modelsListCommand({ json: true }, runtime);
|
||||||
|
|
||||||
expect(runtime.error).toHaveBeenCalledTimes(1);
|
expect(runtime.error).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { modelsStatusCommand } from "./list.js";
|
import { modelsStatusCommand } from "./list.status-command.js";
|
||||||
|
|
||||||
const runtime = {
|
const runtime = {
|
||||||
log: vi.fn(),
|
log: vi.fn(),
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ describe("web media loading", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to JPEG when PNG alpha cannot fit under cap", async () => {
|
it("falls back to JPEG when PNG alpha cannot fit under cap", async () => {
|
||||||
const sizes = [256, 320, 448];
|
const sizes = [224, 256, 320];
|
||||||
let pngBuffer: Buffer | null = null;
|
let pngBuffer: Buffer | null = null;
|
||||||
let smallestPng: Awaited<ReturnType<typeof optimizeImageToPng>> | null = null;
|
let smallestPng: Awaited<ReturnType<typeof optimizeImageToPng>> | null = null;
|
||||||
let jpegOptimized: Awaited<ReturnType<typeof optimizeImageToJpeg>> | null = null;
|
let jpegOptimized: Awaited<ReturnType<typeof optimizeImageToJpeg>> | null = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user