diff --git a/src/agents/tools/browser-tool.test.ts b/src/agents/tools/browser-tool.test.ts new file mode 100644 index 000000000..8f34447c5 --- /dev/null +++ b/src/agents/tools/browser-tool.test.ts @@ -0,0 +1,92 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const browserClientMocks = vi.hoisted(() => ({ + browserCloseTab: vi.fn(async () => ({})), + browserFocusTab: vi.fn(async () => ({})), + browserOpenTab: 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, + controlUrl: "http://127.0.0.1:18791", + controlHost: "127.0.0.1", + controlPort: 18791, + cdpProtocol: "http", + cdpHost: "127.0.0.1", + cdpIsLoopback: true, + color: "#FF0000", + headless: true, + noSandbox: false, + attachOnly: false, + defaultProfile: "clawd", + profiles: { + clawd: { + cdpPort: 18792, + color: "#FF0000", + }, + }, + })), +})); +vi.mock("../../browser/config.js", () => browserConfigMocks); + +vi.mock("../../config/config.js", () => ({ + loadConfig: vi.fn(() => ({ browser: {} })), +})); + +import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js"; +import { createBrowserTool } from "./browser-tool.js"; + +describe("browser tool snapshot maxChars", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("applies the default ai snapshot limit", async () => { + const tool = createBrowserTool(); + await tool.execute?.(null, { action: "snapshot", format: "ai" }); + + expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( + "http://127.0.0.1:18791", + 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", + format: "ai", + maxChars: override, + }); + + expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( + "http://127.0.0.1:18791", + expect.objectContaining({ + maxChars: override, + }), + ); + }); +}); diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts index 3b33bbac4..2fcc0ad2b 100644 --- a/src/agents/tools/browser-tool.ts +++ b/src/agents/tools/browser-tool.ts @@ -20,6 +20,7 @@ import { browserScreenshotAction, } from "../../browser/client-actions.js"; import { resolveBrowserConfig } from "../../browser/config.js"; +import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js"; import { loadConfig } from "../../config/config.js"; import { type AnyAgentTool, @@ -44,8 +45,6 @@ const BROWSER_ACT_KINDS = [ type BrowserActKind = (typeof BROWSER_ACT_KINDS)[number]; -const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000; - // NOTE: Using a flattened object schema instead of Type.Union([Type.Object(...), ...]) // because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema. // The discriminator (kind) determines which properties are relevant; runtime validates. diff --git a/src/browser/constants.ts b/src/browser/constants.ts index 041a3e6ce..a5c3052d1 100644 --- a/src/browser/constants.ts +++ b/src/browser/constants.ts @@ -2,3 +2,4 @@ export const DEFAULT_CLAWD_BROWSER_ENABLED = true; export const DEFAULT_CLAWD_BROWSER_CONTROL_URL = "http://127.0.0.1:18791"; export const DEFAULT_CLAWD_BROWSER_COLOR = "#FF4500"; export const DEFAULT_CLAWD_BROWSER_PROFILE_NAME = "clawd"; +export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000; diff --git a/src/browser/routes/agent.ts b/src/browser/routes/agent.ts index 91b4d5c42..3439937ec 100644 --- a/src/browser/routes/agent.ts +++ b/src/browser/routes/agent.ts @@ -7,6 +7,7 @@ import type express from "express"; import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js"; import { captureScreenshot, snapshotAria } from "../cdp.js"; import type { BrowserFormField } from "../client-actions-core.js"; +import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../constants.js"; import { DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES, DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE, @@ -1205,6 +1206,8 @@ export function registerBrowserAgentRoutes( maxCharsRaw > 0 ? Math.floor(maxCharsRaw) : undefined; + const resolvedMaxChars = + format === "ai" ? (maxChars ?? DEFAULT_AI_SNAPSHOT_MAX_CHARS) : undefined; const interactive = toBoolean(req.query.interactive); const compact = toBoolean(req.query.compact); const depth = toNumber(req.query.depth); @@ -1239,7 +1242,9 @@ export function registerBrowserAgentRoutes( .snapshotAiViaPlaywright({ cdpUrl: profileCtx.profile.cdpUrl, targetId: tab.targetId, - ...(maxChars ? { maxChars } : {}), + ...(typeof resolvedMaxChars === "number" + ? { maxChars: resolvedMaxChars } + : {}), }) .catch(async (err) => { // Public-API fallback when Playwright's private _snapshotForAI is missing. diff --git a/src/browser/server.test.ts b/src/browser/server.test.ts index 47e644b1e..85b2ecf15 100644 --- a/src/browser/server.test.ts +++ b/src/browser/server.test.ts @@ -2,6 +2,7 @@ import { type AddressInfo, createServer } from "node:net"; import { fetch as realFetch } from "undici"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./constants.js"; let testPort = 0; let cdpBaseUrl = ""; @@ -329,6 +330,7 @@ describe("browser control server", () => { expect(pwMocks.snapshotAiViaPlaywright).toHaveBeenCalledWith({ cdpUrl: cdpBaseUrl, targetId: "abcd1234", + maxChars: DEFAULT_AI_SNAPSHOT_MAX_CHARS, }); const nav = (await realFetch(`${base}/navigate`, {