diff --git a/extensions/anthropic/src/anthropic-native-search-wrapper.test.ts b/extensions/anthropic/src/anthropic-native-search-wrapper.test.ts new file mode 100644 index 00000000000..06978c4befc --- /dev/null +++ b/extensions/anthropic/src/anthropic-native-search-wrapper.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi } from "vitest"; +import { createAnthropicNativeSearchStreamWrapper } from "./anthropic-native-search-wrapper.js"; + +describe("createAnthropicNativeSearchStreamWrapper", () => { + it("injects web_search server tool into anthropic-messages payloads", () => { + const capturedPayloads: any[] = []; + const fakeStream = vi.fn((_model: any, _context: any, options: any) => { + // Simulate calling onPayload + if (options?.onPayload) { + const payload = { tools: [{ type: "function", name: "other_tool" }] }; + options.onPayload(payload); + capturedPayloads.push(payload); + } + return { type: "stream", events: [] } as any; + }); + + const wrapped = createAnthropicNativeSearchStreamWrapper(fakeStream as any); + wrapped({ api: "anthropic-messages" } as any, {} as any, {} as any); + + expect(fakeStream).toHaveBeenCalledOnce(); + expect(capturedPayloads[0].tools).toHaveLength(2); + expect(capturedPayloads[0].tools[1].type).toMatch(/^web_search_/); + expect(capturedPayloads[0].tools[1].name).toBe("web_search"); + }); + + it("does not inject if web_search server tool already present", () => { + const capturedPayloads: any[] = []; + const fakeStream = vi.fn((_model: any, _context: any, options: any) => { + if (options?.onPayload) { + const payload = { tools: [{ type: "web_search_20260209", name: "web_search" }] }; + options.onPayload(payload); + capturedPayloads.push(payload); + } + return { type: "stream", events: [] } as any; + }); + + const wrapped = createAnthropicNativeSearchStreamWrapper(fakeStream as any); + wrapped({ api: "anthropic-messages" } as any, {} as any, {} as any); + + expect(capturedPayloads[0].tools).toHaveLength(1); + }); + + it("passes through non-anthropic API calls unchanged", () => { + const fakeStream = vi.fn(() => ({ type: "stream" }) as any); + const wrapped = createAnthropicNativeSearchStreamWrapper(fakeStream as any); + + wrapped({ api: "openai-chat" } as any, {} as any, { onPayload: vi.fn() } as any); + + // onPayload should be the original, not our wrapper + const passedOptions = fakeStream.mock.calls[0][2]; + expect(passedOptions.onPayload).toBeDefined(); + }); +}); diff --git a/extensions/anthropic/src/anthropic-web-search.test.ts b/extensions/anthropic/src/anthropic-web-search.test.ts new file mode 100644 index 00000000000..0268bb6d6b5 --- /dev/null +++ b/extensions/anthropic/src/anthropic-web-search.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it } from "vitest"; +import { __testing, buildAnthropicWebSearchServerTool } from "./anthropic-web-search-provider.js"; + +const { resolveAnthropicWebSearchConfig, resolveToolVersion, DEFAULT_TOOL_VERSION } = __testing; + +describe("resolveAnthropicWebSearchConfig", () => { + it("returns empty object when no config", () => { + expect(resolveAnthropicWebSearchConfig(undefined)).toEqual({}); + expect(resolveAnthropicWebSearchConfig({})).toEqual({}); + }); + + it("returns empty object for non-object anthropic value", () => { + expect(resolveAnthropicWebSearchConfig({ anthropic: "invalid" } as any)).toEqual({}); + expect(resolveAnthropicWebSearchConfig({ anthropic: [] } as any)).toEqual({}); + }); + + it("passes through anthropic config object", () => { + const config = { anthropic: { maxUses: 5, allowedDomains: ["example.com"] } }; + expect(resolveAnthropicWebSearchConfig(config as any)).toEqual({ + maxUses: 5, + allowedDomains: ["example.com"], + }); + }); +}); + +describe("resolveToolVersion", () => { + it("defaults to latest version", () => { + expect(resolveToolVersion({})).toBe(DEFAULT_TOOL_VERSION); + }); + + it("accepts valid versions", () => { + expect(resolveToolVersion({ toolVersion: "web_search_20250305" })).toBe("web_search_20250305"); + expect(resolveToolVersion({ toolVersion: "web_search_20260209" })).toBe("web_search_20260209"); + }); + + it("falls back to default for invalid versions", () => { + expect(resolveToolVersion({ toolVersion: "invalid" })).toBe(DEFAULT_TOOL_VERSION); + expect(resolveToolVersion({ toolVersion: " " })).toBe(DEFAULT_TOOL_VERSION); + }); +}); + +describe("buildAnthropicWebSearchServerTool", () => { + it("builds minimal tool with defaults", () => { + const tool = buildAnthropicWebSearchServerTool(); + expect(tool).toEqual({ + type: DEFAULT_TOOL_VERSION, + name: "web_search", + }); + }); + + it("includes allowed_domains when configured", () => { + const tool = buildAnthropicWebSearchServerTool({ + anthropic: { allowedDomains: ["example.com", "docs.dev"] }, + } as any); + expect(tool.allowed_domains).toEqual(["example.com", "docs.dev"]); + }); + + it("includes blocked_domains when configured", () => { + const tool = buildAnthropicWebSearchServerTool({ + anthropic: { blockedDomains: ["spam.com"] }, + } as any); + expect(tool.blocked_domains).toEqual(["spam.com"]); + }); + + it("includes max_uses when positive", () => { + const tool = buildAnthropicWebSearchServerTool({ + anthropic: { maxUses: 3 }, + } as any); + expect(tool.max_uses).toBe(3); + }); + + it("excludes max_uses when zero or negative", () => { + expect( + buildAnthropicWebSearchServerTool({ anthropic: { maxUses: 0 } } as any).max_uses, + ).toBeUndefined(); + expect( + buildAnthropicWebSearchServerTool({ anthropic: { maxUses: -1 } } as any).max_uses, + ).toBeUndefined(); + }); + + it("includes user_location when configured", () => { + const tool = buildAnthropicWebSearchServerTool({ + anthropic: { + userLocation: { + type: "approximate", + city: "Denver", + region: "Colorado", + country: "US", + timezone: "America/Denver", + }, + }, + } as any); + expect(tool.user_location).toEqual({ + type: "approximate", + city: "Denver", + region: "Colorado", + country: "US", + timezone: "America/Denver", + }); + }); + + it("omits user_location when empty", () => { + const tool = buildAnthropicWebSearchServerTool({ + anthropic: { userLocation: {} }, + } as any); + expect(tool.user_location).toBeUndefined(); + }); + + it("respects custom tool version", () => { + const tool = buildAnthropicWebSearchServerTool({ + anthropic: { toolVersion: "web_search_20250305" }, + } as any); + expect(tool.type).toBe("web_search_20250305"); + }); + + it("omits empty allowed_domains array", () => { + const tool = buildAnthropicWebSearchServerTool({ + anthropic: { allowedDomains: [] }, + } as any); + expect(tool.allowed_domains).toBeUndefined(); + }); +});