import type { AdapterContext } from "@rakazo/adapter-kit"; import { describe, expect, it } from "vitest"; import { FakeWebProvider } from "./fake-web.js"; import { extractReadableText, KeylessHttpWebProvider, parseDuckDuckGoResults, } from "./keyless-http-web.js"; import { clampMaxChars, clampMaxResults } from "./web-limits.js"; import { createWebProvider, resolveWebProviderKind } from "./web-provider-factory.js"; import { webFetchFromTool, webSearchFromTool } from "./web-tools.js"; const ctx: AdapterContext = { operationId: "1", traceId: "1", spaceId: "w", userId: "u", signal: new AbortController().signal, }; const publicResolver = async () => [{ address: "203.0.113.10", family: 4 as const }]; describe("web provider factory", () => { it("defaults to keyless-http", () => { expect(resolveWebProviderKind({})).toBe("keyless-http"); expect(createWebProvider().describe().id).toBe("keyless-http"); expect(createWebProvider("fake").describe().id).toBe("fake"); }); }); describe("keyless HTTP web provider", () => { it("searches via the HTML backend without a vendor API key", async () => { const html = `
Alpha Snippet A
Beta
Snippet B
`; const fetchMock: typeof fetch = async () => new Response(html, { status: 200, headers: { "content-type": "text/html" } }); const provider = new KeylessHttpWebProvider({ fetch: fetchMock, resolveHostname: publicResolver, }); const results = await provider.search({ query: "alpha", maxResults: 5 }, ctx); expect(results).toEqual([ { title: "Alpha", url: "https://example.test/a", snippet: "Snippet A" }, { title: "Beta", url: "https://example.test/b", snippet: "Snippet B" }, ]); }); it("fetches readable page text with truncation", async () => { const long = "word ".repeat(100); const html = `Doc

${long}

`; const fetchMock: typeof fetch = async () => new Response(html, { status: 200, headers: { "content-type": "text/html" } }); const provider = new KeylessHttpWebProvider({ fetch: fetchMock, resolveHostname: publicResolver, }); const result = await provider.fetch({ url: "https://example.test/doc", maxChars: 120 }, ctx); expect(result.title).toBeTruthy(); expect(result.url).toBe("https://example.test/doc"); expect(result.truncated).toBe(true); expect(result.text.endsWith("[Content truncated]")).toBe(true); expect(result.text.length).toBeLessThanOrEqual(120 + "\n\n[Content truncated]".length); }); it("falls back to HTML strip when Readability finds nothing", () => { const extracted = extractReadableText( "Plain
just text here
", "https://example.test/", ); expect(extracted.title).toBe("Plain"); expect(extracted.text).toContain("just text here"); }); it("parses HTML search results with a cap", () => { const html = Array.from({ length: 12 }, (_, i) => { return `
T${i}
S${i}
`; }).join(""); expect(parseDuckDuckGoResults(html, 3)).toHaveLength(3); }); }); describe("web tool apply with fake provider", () => { it("returns injected search and fetch results", async () => { const fake = new FakeWebProvider(); fake.searchHits = [ { title: "One", url: "https://example.test/1", snippet: "s1" }, { title: "Two", url: "https://example.test/2", snippet: "s2" }, ]; fake.fetchResult = { url: "https://example.test/1", title: "One", text: "Body text", truncated: false, }; await expect(webSearchFromTool(fake, ctx, { query: "q", maxResults: 1 })).resolves.toEqual({ results: [{ title: "One", url: "https://example.test/1", snippet: "s1" }], }); expect(fake.lastSearch?.query).toBe("q"); await expect( webFetchFromTool(fake, ctx, { url: "https://example.test/1", maxChars: 100 }), ).resolves.toEqual({ url: "https://example.test/1", title: "One", text: "Body text", truncated: false, }); }); it("surfaces provider errors as tool errors", async () => { const fake = new FakeWebProvider(); fake.searchError = new Error("boom"); await expect(webSearchFromTool(fake, ctx, { query: "q" })).resolves.toEqual({ error: "boom", }); }); }); describe("web tool clamps", () => { it("clamps maxResults and maxChars", () => { expect(clampMaxResults(undefined)).toBe(5); expect(clampMaxResults(100)).toBe(10); expect(clampMaxResults(0)).toBe(1); expect(clampMaxChars(undefined)).toBe(8_000); expect(clampMaxChars(10)).toBe(100); expect(clampMaxChars(999_999)).toBe(50_000); }); });