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 = `
`;
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(
"Plainjust 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 ``;
}).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);
});
});