diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eebf93fc..85ac61fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia. ### Fixes +- Browser: add tests for snapshot labels/efficient query params and labeled image responses. - Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06. - Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06. - Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06. diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index fb8266735..6cbd667fc 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -80,14 +80,9 @@ function resolveProviderToolPolicy(params: { const normalizedProvider = normalizeProviderKey(provider); const rawModelId = params.modelId?.trim().toLowerCase(); const fullModelId = - rawModelId && !rawModelId.includes("/") - ? `${normalizedProvider}/${rawModelId}` - : rawModelId; + rawModelId && !rawModelId.includes("/") ? `${normalizedProvider}/${rawModelId}` : rawModelId; - const candidates = [ - ...(fullModelId ? [fullModelId] : []), - normalizedProvider, - ]; + const candidates = [...(fullModelId ? [fullModelId] : []), normalizedProvider]; for (const key of candidates) { const match = lookup.get(key); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 6ff841a8c..9ddfdf159 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -125,8 +125,7 @@ export function createClawdbotCodingTools(options?: { }); const profilePolicy = resolveToolProfilePolicy(profile); const providerProfilePolicy = resolveToolProfilePolicy(providerProfile); - const scopeKey = - options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); + const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); const subagentPolicy = isSubagentSessionKey(options?.sessionKey) && options?.sessionKey ? resolveSubagentToolPolicy(options.config) @@ -240,9 +239,7 @@ export function createClawdbotCodingTools(options?: { hasRepliedRef: options?.hasRepliedRef, }), ]; - const toolsFiltered = profilePolicy - ? filterToolsByPolicy(tools, profilePolicy) - : tools; + const toolsFiltered = profilePolicy ? filterToolsByPolicy(tools, profilePolicy) : tools; const providerProfileFiltered = providerProfilePolicy ? filterToolsByPolicy(toolsFiltered, providerProfilePolicy) : toolsFiltered; diff --git a/src/agents/tools/browser-tool.test.ts b/src/agents/tools/browser-tool.test.ts index dc3439bfc..542d7de8c 100644 --- a/src/agents/tools/browser-tool.test.ts +++ b/src/agents/tools/browser-tool.test.ts @@ -52,6 +52,17 @@ vi.mock("../../config/config.js", () => ({ loadConfig: vi.fn(() => ({ browser: {} })), })); +const toolCommonMocks = vi.hoisted(() => ({ + imageResultFromFile: vi.fn(), +})); +vi.mock("./common.js", async () => { + const actual = await vi.importActual("./common.js"); + return { + ...actual, + imageResultFromFile: toolCommonMocks.imageResultFromFile, + }; +}); + import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js"; import { createBrowserTool } from "./browser-tool.js"; @@ -103,3 +114,47 @@ describe("browser tool snapshot maxChars", () => { expect(Object.hasOwn(opts ?? {}, "maxChars")).toBe(false); }); }); + +describe("browser tool snapshot labels", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + 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", + format: "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" }); + }); +}); diff --git a/src/browser/client.test.ts b/src/browser/client.test.ts index 4f53dc3ec..f85f3756c 100644 --- a/src/browser/client.test.ts +++ b/src/browser/client.test.ts @@ -49,6 +49,40 @@ describe("browser client", () => { ).rejects.toThrow(/409: conflict/i); }); + it("adds labels + efficient mode query params to snapshots", async () => { + const calls: string[] = []; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string) => { + calls.push(url); + return { + ok: true, + json: async () => ({ + ok: true, + format: "ai", + targetId: "t1", + url: "https://x", + snapshot: "ok", + }), + } as unknown as Response; + }), + ); + + await expect( + browserSnapshot("http://127.0.0.1:18791", { + format: "ai", + labels: true, + mode: "efficient", + }), + ).resolves.toMatchObject({ ok: true, format: "ai" }); + + const snapshotCall = calls.find((url) => url.includes("/snapshot?")); + expect(snapshotCall).toBeTruthy(); + const parsed = new URL(snapshotCall as string); + expect(parsed.searchParams.get("labels")).toBe("1"); + expect(parsed.searchParams.get("mode")).toBe("efficient"); + }); + it("uses the expected endpoints + methods for common calls", async () => { const calls: Array<{ url: string; init?: RequestInit }> = [];