import { describe, expect, test } from "bun:test" import path from "path" import { Instance } from "../../src/project/instance" import { WebFetchTool } from "../../src/tool/webfetch" import { SessionID, MessageID } from "../../src/session/schema" const projectRoot = path.join(import.meta.dir, "../..") const ctx = { sessionID: SessionID.make("ses_test"), messageID: MessageID.make("message"), callID: "", agent: "build", abort: AbortSignal.any([]), messages: [], metadata: () => {}, ask: async () => {}, } type TimerID = ReturnType async function withFetch( mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise, fn: () => Promise, ) { const originalFetch = globalThis.fetch globalThis.fetch = mockFetch as unknown as typeof fetch try { await fn() } finally { globalThis.fetch = originalFetch } } async function withTimers(fn: (state: { ids: TimerID[]; cleared: TimerID[] }) => Promise) { const set = globalThis.setTimeout const clear = globalThis.clearTimeout const ids: TimerID[] = [] const cleared: TimerID[] = [] globalThis.setTimeout = ((...args: Parameters) => { const id = set(...args) ids.push(id) return id }) as typeof setTimeout globalThis.clearTimeout = ((id?: TimerID) => { if (id !== undefined) cleared.push(id) return clear(id) }) as typeof clearTimeout try { await fn({ ids, cleared }) } finally { ids.forEach(clear) globalThis.setTimeout = set globalThis.clearTimeout = clear } } describe("tool.webfetch", () => { test("returns image responses as file attachments", async () => { const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]) await withFetch( async () => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }), async () => { await Instance.provide({ directory: projectRoot, fn: async () => { const webfetch = await WebFetchTool.init() const result = await webfetch.execute({ url: "https://example.com/image.png", format: "markdown" }, ctx) expect(result.output).toBe("Image fetched successfully") expect(result.attachments).toBeDefined() expect(result.attachments?.length).toBe(1) expect(result.attachments?.[0].type).toBe("file") expect(result.attachments?.[0].mime).toBe("image/png") expect(result.attachments?.[0].url.startsWith("data:image/png;base64,")).toBe(true) expect(result.attachments?.[0]).not.toHaveProperty("id") expect(result.attachments?.[0]).not.toHaveProperty("sessionID") expect(result.attachments?.[0]).not.toHaveProperty("messageID") }, }) }, ) }) test("keeps svg as text output", async () => { const svg = 'hello' await withFetch( async () => new Response(svg, { status: 200, headers: { "content-type": "image/svg+xml; charset=UTF-8" }, }), async () => { await Instance.provide({ directory: projectRoot, fn: async () => { const webfetch = await WebFetchTool.init() const result = await webfetch.execute({ url: "https://example.com/image.svg", format: "html" }, ctx) expect(result.output).toContain(" { await withFetch( async () => new Response("hello from webfetch", { status: 200, headers: { "content-type": "text/plain; charset=utf-8" }, }), async () => { await Instance.provide({ directory: projectRoot, fn: async () => { const webfetch = await WebFetchTool.init() const result = await webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx) expect(result.output).toBe("hello from webfetch") expect(result.attachments).toBeUndefined() }, }) }, ) }) test("clears timeout when fetch rejects", async () => { await withTimers(async ({ ids, cleared }) => { await withFetch( async () => { throw new Error("boom") }, async () => { await Instance.provide({ directory: projectRoot, fn: async () => { const webfetch = await WebFetchTool.init() await expect( webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx), ).rejects.toThrow("boom") }, }) }, ) expect(ids).toHaveLength(1) expect(cleared).toContain(ids[0]) }) }) })