Spaces:
Paused
Paused
| 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" }); | |
| }); | |
| }); | |