292 lines
8.7 KiB
TypeScript
292 lines
8.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const browserClientMocks = vi.hoisted(() => ({
|
|
browserCloseTab: vi.fn(async () => ({})),
|
|
browserFocusTab: vi.fn(async () => ({})),
|
|
browserOpenTab: vi.fn(async () => ({})),
|
|
browserProfiles: vi.fn(async () => []),
|
|
browserSnapshot: vi.fn(async () => ({
|
|
ok: true,
|
|
format: "ai",
|
|
targetId: "t1",
|
|
url: "https://example.com",
|
|
snapshot: "ok",
|
|
})),
|
|
browserStart: vi.fn(async () => ({})),
|
|
browserStatus: vi.fn(async () => ({
|
|
ok: true,
|
|
running: true,
|
|
pid: 1,
|
|
cdpPort: 18792,
|
|
cdpUrl: "http://127.0.0.1:18792",
|
|
})),
|
|
browserStop: vi.fn(async () => ({})),
|
|
browserTabs: vi.fn(async () => []),
|
|
}));
|
|
vi.mock("../../browser/client.js", () => browserClientMocks);
|
|
|
|
const browserConfigMocks = vi.hoisted(() => ({
|
|
resolveBrowserConfig: vi.fn(() => ({
|
|
enabled: true,
|
|
controlPort: 18791,
|
|
})),
|
|
}));
|
|
vi.mock("../../browser/config.js", () => browserConfigMocks);
|
|
|
|
const nodesUtilsMocks = vi.hoisted(() => ({
|
|
listNodes: vi.fn(async () => []),
|
|
}));
|
|
vi.mock("./nodes-utils.js", async () => {
|
|
const actual = await vi.importActual<typeof import("./nodes-utils.js")>("./nodes-utils.js");
|
|
return {
|
|
...actual,
|
|
listNodes: nodesUtilsMocks.listNodes,
|
|
};
|
|
});
|
|
|
|
const gatewayMocks = vi.hoisted(() => ({
|
|
callGatewayTool: vi.fn(async () => ({
|
|
ok: true,
|
|
payload: { result: { ok: true, running: true } },
|
|
})),
|
|
}));
|
|
vi.mock("./gateway.js", () => gatewayMocks);
|
|
|
|
const configMocks = vi.hoisted(() => ({
|
|
loadConfig: vi.fn(() => ({ browser: {} })),
|
|
}));
|
|
vi.mock("../../config/config.js", () => configMocks);
|
|
|
|
const toolCommonMocks = vi.hoisted(() => ({
|
|
imageResultFromFile: vi.fn(),
|
|
}));
|
|
vi.mock("./common.js", async () => {
|
|
const actual = await vi.importActual<typeof import("./common.js")>("./common.js");
|
|
return {
|
|
...actual,
|
|
imageResultFromFile: toolCommonMocks.imageResultFromFile,
|
|
};
|
|
});
|
|
|
|
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
|
|
import { createBrowserTool } from "./browser-tool.js";
|
|
|
|
describe("browser tool snapshot maxChars", () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
configMocks.loadConfig.mockReturnValue({ browser: {} });
|
|
nodesUtilsMocks.listNodes.mockResolvedValue([]);
|
|
});
|
|
|
|
it("applies the default ai snapshot limit", async () => {
|
|
const tool = createBrowserTool();
|
|
await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai" });
|
|
|
|
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
|
|
undefined,
|
|
expect.objectContaining({
|
|
format: "ai",
|
|
maxChars: DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("respects an explicit maxChars override", async () => {
|
|
const tool = createBrowserTool();
|
|
const override = 2_000;
|
|
await tool.execute?.(null, {
|
|
action: "snapshot",
|
|
snapshotFormat: "ai",
|
|
maxChars: override,
|
|
});
|
|
|
|
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
|
|
undefined,
|
|
expect.objectContaining({
|
|
maxChars: override,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("skips the default when maxChars is explicitly zero", async () => {
|
|
const tool = createBrowserTool();
|
|
await tool.execute?.(null, {
|
|
action: "snapshot",
|
|
snapshotFormat: "ai",
|
|
maxChars: 0,
|
|
});
|
|
|
|
expect(browserClientMocks.browserSnapshot).toHaveBeenCalled();
|
|
const [, opts] = browserClientMocks.browserSnapshot.mock.calls.at(-1) ?? [];
|
|
expect(Object.hasOwn(opts ?? {}, "maxChars")).toBe(false);
|
|
});
|
|
|
|
it("lists profiles", async () => {
|
|
const tool = createBrowserTool();
|
|
await tool.execute?.(null, { action: "profiles" });
|
|
|
|
expect(browserClientMocks.browserProfiles).toHaveBeenCalledWith(undefined);
|
|
});
|
|
|
|
it("passes refs mode through to browser snapshot", async () => {
|
|
const tool = createBrowserTool();
|
|
await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai", refs: "aria" });
|
|
|
|
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
|
|
undefined,
|
|
expect.objectContaining({
|
|
format: "ai",
|
|
refs: "aria",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("uses config snapshot defaults when mode is not provided", async () => {
|
|
configMocks.loadConfig.mockReturnValue({
|
|
browser: { snapshotDefaults: { mode: "efficient" } },
|
|
});
|
|
const tool = createBrowserTool();
|
|
await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai" });
|
|
|
|
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
|
|
undefined,
|
|
expect.objectContaining({
|
|
mode: "efficient",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not apply config snapshot defaults to aria snapshots", async () => {
|
|
configMocks.loadConfig.mockReturnValue({
|
|
browser: { snapshotDefaults: { mode: "efficient" } },
|
|
});
|
|
const tool = createBrowserTool();
|
|
await tool.execute?.(null, { action: "snapshot", snapshotFormat: "aria" });
|
|
|
|
expect(browserClientMocks.browserSnapshot).toHaveBeenCalled();
|
|
const [, opts] = browserClientMocks.browserSnapshot.mock.calls.at(-1) ?? [];
|
|
expect(opts?.mode).toBeUndefined();
|
|
});
|
|
|
|
it("defaults to host when using profile=chrome (even in sandboxed sessions)", async () => {
|
|
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
|
|
await tool.execute?.(null, { action: "snapshot", profile: "chrome", snapshotFormat: "ai" });
|
|
|
|
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
|
|
undefined,
|
|
expect.objectContaining({
|
|
profile: "chrome",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("routes to node proxy when target=node", async () => {
|
|
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
{
|
|
nodeId: "node-1",
|
|
displayName: "Browser Node",
|
|
connected: true,
|
|
caps: ["browser"],
|
|
commands: ["browser.proxy"],
|
|
},
|
|
]);
|
|
const tool = createBrowserTool();
|
|
await tool.execute?.(null, { action: "status", target: "node" });
|
|
|
|
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
|
"node.invoke",
|
|
{ timeoutMs: 20000 },
|
|
expect.objectContaining({
|
|
nodeId: "node-1",
|
|
command: "browser.proxy",
|
|
}),
|
|
);
|
|
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps sandbox bridge url when node proxy is available", async () => {
|
|
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
{
|
|
nodeId: "node-1",
|
|
displayName: "Browser Node",
|
|
connected: true,
|
|
caps: ["browser"],
|
|
commands: ["browser.proxy"],
|
|
},
|
|
]);
|
|
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
|
|
await tool.execute?.(null, { action: "status" });
|
|
|
|
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
|
|
"http://127.0.0.1:9999",
|
|
expect.objectContaining({ profile: undefined }),
|
|
);
|
|
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps chrome profile on host when node proxy is available", async () => {
|
|
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
{
|
|
nodeId: "node-1",
|
|
displayName: "Browser Node",
|
|
connected: true,
|
|
caps: ["browser"],
|
|
commands: ["browser.proxy"],
|
|
},
|
|
]);
|
|
const tool = createBrowserTool();
|
|
await tool.execute?.(null, { action: "status", profile: "chrome" });
|
|
|
|
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
|
|
undefined,
|
|
expect.objectContaining({ profile: "chrome" }),
|
|
);
|
|
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("browser tool snapshot labels", () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
configMocks.loadConfig.mockReturnValue({ browser: {} });
|
|
});
|
|
|
|
it("returns image + text when labels are requested", async () => {
|
|
const tool = createBrowserTool();
|
|
const imageResult = {
|
|
content: [
|
|
{ type: "text", text: "label text" },
|
|
{ type: "image", data: "base64", mimeType: "image/png" },
|
|
],
|
|
details: { path: "/tmp/snap.png" },
|
|
};
|
|
|
|
toolCommonMocks.imageResultFromFile.mockResolvedValueOnce(imageResult);
|
|
browserClientMocks.browserSnapshot.mockResolvedValueOnce({
|
|
ok: true,
|
|
format: "ai",
|
|
targetId: "t1",
|
|
url: "https://example.com",
|
|
snapshot: "label text",
|
|
imagePath: "/tmp/snap.png",
|
|
});
|
|
|
|
const result = await tool.execute?.(null, {
|
|
action: "snapshot",
|
|
snapshotFormat: "ai",
|
|
labels: true,
|
|
});
|
|
|
|
expect(toolCommonMocks.imageResultFromFile).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
path: "/tmp/snap.png",
|
|
extraText: "label text",
|
|
}),
|
|
);
|
|
expect(result).toEqual(imageResult);
|
|
expect(result?.content).toHaveLength(2);
|
|
expect(result?.content?.[0]).toMatchObject({ type: "text", text: "label text" });
|
|
expect(result?.content?.[1]).toMatchObject({ type: "image" });
|
|
});
|
|
});
|