{"version":3,"file":"fetch-enforcement.test.d.ts","sourceRoot":"","sources":["../../../src/core/web-research/fetch-enforcement.test.ts"],"names":[],"mappings":"","sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { DeepResearchEngine } from \"./research.js\";\nimport { WebSearchProviderRegistry } from \"./search.js\";\nimport type { WebFetchRequest, WebFetchResponse, WebSearchProvider, WebSearchResult } from \"./types.js\";\n\n/**\n * Fetch-enforcement policy:\n * - deep_research collects evidence only through the secure fetcher (web_fetch).\n * - shell/curl output is never imported as evidence by the research engine.\n * - a failed fetch cannot become a directly supported claim.\n */\n\nconst RESULTS: WebSearchResult[] = [\n\t{\n\t\ttitle: \"Primary source\",\n\t\turl: \"https://docs.example.com/a\",\n\t\tsnippet: \"base damage 225\",\n\t\tprovider: \"searxng\",\n\t\tengine: \"bing\",\n\t\trank: 1,\n\t},\n\t{\n\t\ttitle: \"Second source\",\n\t\turl: \"https://agency.gov/b\",\n\t\tsnippet: \"base damage 225\",\n\t\tprovider: \"searxng\",\n\t\tengine: \"brave\",\n\t\trank: 2,\n\t},\n\t{\n\t\ttitle: \"Inaccessible source\",\n\t\turl: \"https://blocked.example.net/c\",\n\t\tsnippet: \"base damage 999\",\n\t\tprovider: \"searxng\",\n\t\tengine: \"brave\",\n\t\trank: 3,\n\t},\n];\n\nfunction provider(): WebSearchProvider {\n\treturn {\n\t\tid: \"searxng\",\n\t\tcapabilities: { freshness: true, language: true, region: false, categories: true, safeSearch: true },\n\t\tsearch: async (request) => ({\n\t\t\tprovider: \"searxng\",\n\t\t\tquery: request.query,\n\t\t\tresults: RESULTS,\n\t\t\trawResultCount: RESULTS.length,\n\t\t\tdeduplicatedCount: 0,\n\t\t\tdurationMs: 1,\n\t\t}),\n\t\thealthCheck: async () => ({ provider: \"searxng\", status: \"healthy\" }),\n\t};\n}\n\nfunction makeEngine(fetchImpl: (request: WebFetchRequest) => Promise<WebFetchResponse>) {\n\tconst registry = new WebSearchProviderRegistry([provider(), { ...provider(), id: \"duckduckgo-lite\" as const }]);\n\treturn new DeepResearchEngine(\n\t\tregistry,\n\t\t\"searxng\",\n\t\t{ fetch: fetchImpl },\n\t\t{\n\t\t\tmaxQueries: 2,\n\t\t\tmaxSources: 3,\n\t\t\tmaxBytes: 10_000,\n\t\t\tmaxBrowserRenders: 0,\n\t\t\tmaxElapsedMs: 5000,\n\t\t\tmaxParallelFetches: 2,\n\t\t},\n\t);\n}\n\ndescribe(\"fetch enforcement in deep_research\", () => {\n\tit(\"collects evidence only through the secure fetcher, never shell output\", async () => {\n\t\tconst fetchedUrls: string[] = [];\n\t\tconst engine = makeEngine((request) => {\n\t\t\tfetchedUrls.push(request.url);\n\t\t\tconst id = request.url.includes(\"gov\") ? \"web-b\" : \"web-a\";\n\t\t\tconst content = \"base damage 225\";\n\t\t\treturn Promise.resolve({\n\t\t\t\tevidence: {\n\t\t\t\t\tevidenceId: id,\n\t\t\t\t\tsourceType: \"web\",\n\t\t\t\t\trequestedUrl: request.url,\n\t\t\t\t\tfinalUrl: request.url,\n\t\t\t\t\tcanonicalUrl: request.url,\n\t\t\t\t\ttitle: id,\n\t\t\t\t\tretrievedAt: \"2026-08-02T00:00:00.000Z\",\n\t\t\t\t\tcontentType: \"text/html\",\n\t\t\t\t\textractor: \"readability\",\n\t\t\t\t\tcontentSha256: id.padEnd(64, \"0\"),\n\t\t\t\t\tcompleteContentLocation: `session:tool-result:${id}`,\n\t\t\t\t\tcompleteContent: content,\n\t\t\t\t\trelevantPassages: [{ id: `p-${id}`, text: content, startLine: 1, endLine: 1 }],\n\t\t\t\t\toutboundLinks: [],\n\t\t\t\t\tbytesDownloaded: 50,\n\t\t\t\t\tbytesExtracted: content.length,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t\tuntrusted: true,\n\t\t\t\t},\n\t\t\t\tcontent,\n\t\t\t\trendered: false,\n\t\t\t\tdurationMs: 1,\n\t\t\t});\n\t\t});\n\t\tconst result = await engine.run({ objective: \"Find base damage\", maxSources: 2 });\n\t\t// Every selected/evidence source passed through the fetcher.\n\t\texpect(fetchedUrls.length).toBeGreaterThan(0);\n\t\texpect(fetchedUrls.every((url) => url.startsWith(\"https://\"))).toBe(true);\n\t\texpect(result.bundle.evidence.every((e) => e.sourceType === \"web\")).toBe(true);\n\t});\n\n\tit(\"does not convert a failed fetch into a directly supported claim\", async () => {\n\t\tconst engine = makeEngine(() => Promise.reject(new Error(\"SOURCE_BLOCKED\")));\n\t\tconst result = await engine.run({ objective: \"Find base damage\", maxSources: 2 });\n\t\texpect(result.partial).toBe(true);\n\t\texpect(result.events.some((event) => event.type === \"SOURCE_REJECTED\")).toBe(true);\n\t\t// The blocked source's claimed 999 must not appear as a direct claim.\n\t\texpect(result.bundle.claims.some((claim) => claim.text.includes(\"999\"))).toBe(false);\n\t\texpect(result.bundle.contradictions.length).toBe(0);\n\t});\n});\n"]}