fix(browser): limit ai snapshot size

test(browser): cover ai snapshot limit
This commit is contained in:
Gabriel Trigo
2026-01-12 18:05:10 +00:00
committed by Peter Steinberger
parent 542c8020ec
commit 79a6506593
5 changed files with 102 additions and 3 deletions

View File

@@ -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,
}),
);
});
});

View File

@@ -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.

View File

@@ -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;

View File

@@ -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.

View File

@@ -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`, {