From b2914a7ce74f1eed3ebaeb87c2a6a4da0d79bc6f Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 4 Oct 2026 04:36:27 +0800 Subject: [PATCH] test: cover the Ollama web tools and mark trimmed snippets Both web tools were already implemented and wired, but the suite only exercised get_datetime, so neither had any direct coverage. Adds tools.test.ts pinning the contract for both: - web_search: request shape (endpoint, bearer token, body), max_results clamped into 1..10, empty result sets, and how results render - web_fetch: request shape, page content, title fallback, truncation - failure paths for both, including a rejected API key - the webEnabled filter, so the local tools survive and the web tools go Also marks a trimmed search snippet with "[truncated]" rather than silently dropping the tail, which is what web_fetch already did. Also replaces the extension icon with a white tile and a black bubble. Endpoint paths were checked against the live Ollama API: /api/chat, /api/web_search and /api/web_fetch all return 401 unauthenticated (so the route exists and demands a key), and /api/tags returns 405 to a POST, which also confirms the path. No authenticated call was made. Test count: 77 -> 96. --- assets/extension-icon.png | Bin 22477 -> 16589 bytes src/__tests__/tools.test.ts | 246 ++++++++++++++++++++++++++++++++++++ src/lib/tools.ts | 8 +- 3 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/tools.test.ts diff --git a/assets/extension-icon.png b/assets/extension-icon.png index 78c06aa3a75d12ec297786847935d80d9374e5bb..2ff9065939497a99dcd70b8e88ba6ed52d1066c2 100644 GIT binary patch literal 16589 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 S96;*@(CW9Z7hJOn`F{YiXIaPq literal 22477 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 fMh { + return JSON.parse(request(index).init.body as string) as Record; +} + +beforeEach(() => { + fetchMock.mockReset(); + vi.stubGlobal("fetch", fetchMock); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("tool registration", () => { + it("exposes all four tools to the model", () => { + const names = TOOL_DEFINITIONS.map((tool) => tool.function.name); + + expect(names).toEqual(["get_datetime", "get_location", "web_search", "web_fetch"]); + }); + + it("marks both web tools as cloud-dependent", () => { + expect(WEB_TOOL_NAMES).toEqual(new Set(["web_search", "web_fetch"])); + }); + + it("offers the web tools only when the user has them enabled", () => { + const enabled = toolsFor({ webEnabled: true }).map((tool) => tool.function.name); + const disabled = toolsFor({ webEnabled: false }).map((tool) => tool.function.name); + + expect(enabled).toContain("web_search"); + expect(enabled).toContain("web_fetch"); + + expect(disabled).not.toContain("web_search"); + expect(disabled).not.toContain("web_fetch"); + // Local tools must survive the filter. + expect(disabled).toEqual(["get_datetime", "get_location"]); + }); + + it("gives every tool a description the model can act on", () => { + for (const tool of TOOL_DEFINITIONS) { + expect(tool.type).toBe("function"); + expect(tool.function.description.length).toBeGreaterThan(20); + expect(tool.function.parameters).toMatchObject({ type: "object" }); + } + }); +}); + +describe("web_search", () => { + it("posts the query to Ollama's web search endpoint", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse({ results: [{ title: "Vicinae", url: "https://vicinae.com", content: "A launcher." }] }), + ); + + const result = await executeTool("web_search", { query: "vicinae launcher", max_results: 3 }, context); + + expect(result.isError).toBe(false); + expect(request().url).toBe("https://ollama.com/api/web_search"); + expect(request().init.method).toBe("POST"); + expect((request().init.headers as Record).Authorization).toBe("Bearer test-key"); + expect(requestBody()).toEqual({ query: "vicinae launcher", max_results: 3 }); + }); + + it("renders results as numbered title, url, and snippet", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse({ + results: [ + { title: "First", url: "https://first.example", content: "First body" }, + { title: "Second", url: "https://second.example", content: "Second body" }, + ], + }), + ); + + const result = await executeTool("web_search", { query: "x" }, context); + + expect(result.content).toContain('Web results for "x"'); + expect(result.content).toContain("1. First"); + expect(result.content).toContain("https://first.example"); + expect(result.content).toContain("2. Second"); + }); + + it("omits max_results when the model does not ask for it", async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ results: [] })); + + await executeTool("web_search", { query: "x" }, context); + + expect(requestBody()).toEqual({ query: "x" }); + }); + + it("clamps max_results into the range the API accepts", async () => { + fetchMock.mockResolvedValue(jsonResponse({ results: [] })); + + await executeTool("web_search", { query: "x", max_results: 99 }, context); + expect(requestBody(0).max_results).toBe(10); + + await executeTool("web_search", { query: "x", max_results: 0 }, context); + expect(requestBody(1).max_results).toBe(1); + }); + + it("reports an empty result set without treating it as a failure", async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ results: [] })); + + const result = await executeTool("web_search", { query: "obscure" }, context); + + expect(result.isError).toBe(false); + expect(result.content).toContain("No web results"); + }); + + it("marks a snippet that had to be trimmed", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse({ results: [{ title: "Long", url: "https://long.example", content: "x".repeat(5000) }] }), + ); + + const result = await executeTool("web_search", { query: "x" }, context); + + expect(result.content).toContain("[truncated]"); + expect(result.content.length).toBeLessThan(2000); + }); + + it("rejects a call with no query without touching the network", async () => { + const result = await executeTool("web_search", {}, context); + + expect(result.isError).toBe(true); + expect(result.content).toContain("query"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("turns a rejected key into an actionable message", async () => { + fetchMock.mockResolvedValueOnce(new Response("unauthorized", { status: 401 })); + + const result = await executeTool("web_search", { query: "x" }, context); + + expect(result.isError).toBe(true); + expect(result.content).toContain("API key"); + expect(result.content).toContain("Ollama Cloud"); + }); +}); + +describe("web_fetch", () => { + it("posts the url and returns the page content", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse({ + title: "Example", + content: "# Example\n\nBody text.", + links: ["https://a.example", "https://b.example"], + }), + ); + + const result = await executeTool("web_fetch", { url: "https://example.com" }, context); + + expect(result.isError).toBe(false); + expect(request().url).toBe("https://ollama.com/api/web_fetch"); + expect(requestBody()).toEqual({ url: "https://example.com" }); + + expect(result.content).toContain("Title: Example"); + expect(result.content).toContain("Body text."); + expect(result.content).toContain("https://a.example"); + }); + + it("truncates an enormous page and says so", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse({ title: "Huge", content: "y".repeat(20_000), links: [] }), + ); + + const result = await executeTool("web_fetch", { url: "https://huge.example" }, context); + + expect(result.content).toContain("[content truncated]"); + expect(result.content.length).toBeLessThan(9000); + }); + + it("falls back to the url when the page reports no title", async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ content: "body", links: [] })); + + const result = await executeTool("web_fetch", { url: "https://untitled.example" }, context); + + expect(result.content).toContain("Title: https://untitled.example"); + }); + + it("rejects a call with no url without touching the network", async () => { + const result = await executeTool("web_fetch", {}, context); + + expect(result.isError).toBe(true); + expect(result.content).toContain("url"); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); + +describe("local tools", () => { + it("uses the injected clock instead of the wall clock", async () => { + const result = await executeTool("get_datetime", {}, { + ...context, + now: new Date("2026-01-02T03:04:05Z"), + }); + + expect(result.isError).toBe(false); + expect(result.content).toContain("2026-01-02T03:04:05.000Z"); + expect(result.content).toContain("Unix timestamp: 1767323045"); + expect(result.content).toContain("Timezone:"); + // Deterministic across machines, unlike the rendered local date. + expect(result.content).toContain("Current date and time:"); + }); + + it("describes location as a region rather than an address", async () => { + const result = await executeTool("get_location", {}, context); + + expect(result.isError).toBe(false); + expect(result.content).toContain("Timezone:"); + expect(result.content).toContain("Locale:"); + expect(result.content).toContain("not a precise address"); + }); +}); + +describe("dispatch", () => { + it("reports an unknown tool instead of throwing", async () => { + const result = await executeTool("delete_everything", {}, context); + + expect(result.isError).toBe(true); + expect(result.content).toContain("Unknown tool"); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/tools.ts b/src/lib/tools.ts index a79b6c4..5a00bdd 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -208,7 +208,13 @@ async function runWebSearch(args: Record, context: ToolContext) const rendered = results .map((result, index) => { - const snippet = result.content.replace(/\s+/g, " ").trim().slice(0, MAX_SNIPPET_CHARS); + const collapsed = result.content.replace(/\s+/g, " ").trim(); + // Mark the cut rather than silently dropping the tail: the model should + // know a snippet was trimmed, and this matches what web_fetch does. + const snippet = + collapsed.length > MAX_SNIPPET_CHARS + ? `${collapsed.slice(0, MAX_SNIPPET_CHARS)} …[truncated]` + : collapsed; return `${index + 1}. ${result.title}\n ${result.url}\n ${snippet}`; }) .join("\n\n"); -- 2.54.0