test: cover the Ollama web tools, mark trimmed snippets, new icon #2

Merged
adrianbonpin merged 1 commits from feat/web-tools-coverage into prod 2026-10-03 20:36:38 +00:00
3 changed files with 253 additions and 1 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+246
View File
@@ -0,0 +1,246 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TOOL_DEFINITIONS, WEB_TOOL_NAMES, executeTool, toolsFor } from "../lib/tools";
/**
* Direct coverage for the tool layer.
*
* `agent.test.ts` exercises the loop end to end but only with `get_datetime`.
* These tests pin the contract of the two Ollama web tools specifically: the
* request they put on the wire, how results are rendered for the model, and how
* failures are reported.
*/
const fetchMock = vi.fn();
const context = { host: "https://ollama.com", apiKey: "test-key" };
function jsonResponse(payload: unknown, status = 200): Response {
return new Response(JSON.stringify(payload), {
status,
headers: { "Content-Type": "application/json" },
});
}
function request(index = 0): { url: string; init: RequestInit } {
const call = fetchMock.mock.calls[index] as [string, RequestInit];
return { url: call[0], init: call[1] };
}
function requestBody(index = 0): Record<string, unknown> {
return JSON.parse(request(index).init.body as string) as Record<string, unknown>;
}
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<string, string>).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();
});
});
+7 -1
View File
@@ -208,7 +208,13 @@ async function runWebSearch(args: Record<string, unknown>, 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");