import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { fetchTool } from "../fetch"; // Mock fetch globally const originalFetch = globalThis.fetch; let mockFetch: any; beforeEach(() => { mockFetch = mock(() => {}); globalThis.fetch = mockFetch; }); afterEach(() => { globalThis.fetch = originalFetch; mockFetch.mockClear(); }); describe("fetch tool", () => { test("should fetch simple text content", async () => { const mockContent = "This is plain text content"; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: "OK", url: "https://example.com/test.txt", headers: new Headers({ "content-type": "text/plain" }), text: () => Promise.resolve(mockContent), }); const result = await fetchTool.execute({ url: "https://example.com/test.txt", }); expect(result).toContain("Contents of https://example.com/test.txt:"); expect(result).toContain(mockContent); expect(result).toContain("Content type: text/plain"); }); test("should convert HTML to markdown", async () => { const mockHtml = ` Test Page

Main Title

This is a paragraph with emphasis.

Link text `; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: "OK", url: "https://example.com/test.html", headers: new Headers({ "content-type": "text/html" }), text: () => Promise.resolve(mockHtml), }); const result = await fetchTool.execute({ url: "https://example.com/test.html", }); expect(result).toContain("# Main Title"); expect(result).toContain("**paragraph**"); expect(result).toContain("_emphasis_"); expect(result).toContain("* First item"); expect(result).toContain("* Second item"); expect(result).toContain("[Link text](https://example.com)"); }); test("should return raw HTML when raw=true", async () => { const mockHtml = "

Raw HTML

"; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: "OK", url: "https://example.com/raw.html", headers: new Headers({ "content-type": "text/html" }), text: () => Promise.resolve(mockHtml), }); const result = await fetchTool.execute({ url: "https://example.com/raw.html", raw: true, }); expect(result).toContain(mockHtml); expect(result).not.toContain("# Raw HTML"); }); test("should handle content truncation", async () => { const longContent = "x".repeat(10000); mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: "OK", url: "https://example.com/long.txt", headers: new Headers({ "content-type": "text/plain" }), text: () => Promise.resolve(longContent), }); const result = await fetchTool.execute({ url: "https://example.com/long.txt", maxLength: 1000, }); expect(result).toContain(""); expect(result).toContain("Showing characters 0-1000 of 10000"); expect(result).toContain("Use startIndex=1000 to continue"); }); test("should handle pagination with startIndex", async () => { const content = "0123456789".repeat(100); // 1000 chars mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: "OK", url: "https://example.com/paginate.txt", headers: new Headers({ "content-type": "text/plain" }), text: () => Promise.resolve(content), }); const result = await fetchTool.execute({ url: "https://example.com/paginate.txt", startIndex: 500, maxLength: 300, }); expect(result).toContain("Showing characters 500-800 of 1000"); expect(result).toContain("200 characters remaining"); }); test("should handle startIndex beyond content length", async () => { const content = "Short content"; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: "OK", url: "https://example.com/short.txt", headers: new Headers({ "content-type": "text/plain" }), text: () => Promise.resolve(content), }); const result = await fetchTool.execute({ url: "https://example.com/short.txt", startIndex: 100, }); expect(result).toContain("No more content available"); expect(result).toContain("Start index 100 exceeds content length 13"); }); test("should return error message for HTTP errors", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: "Not Found", }); const result = await fetchTool.execute({ url: "https://example.com/notfound.txt", }); expect(result).toContain( "Failed to fetch https://example.com/notfound.txt - HTTP 404: Not Found" ); }); test("should return error message for network timeouts", async () => { mockFetch.mockRejectedValueOnce(new Error("AbortError")); const result = await fetchTool.execute({ url: "https://example.com/timeout.txt", timeout: 1000, }); expect(result).toContain( "Request timeout: Failed to fetch https://example.com/timeout.txt within 1000ms" ); }); test("should return error message for network errors", async () => { mockFetch.mockRejectedValueOnce(new Error("Network connection failed")); const result = await fetchTool.execute({ url: "https://example.com/network-error.txt", }); expect(result).toContain( "Network error: Network connection failed" ); }); test("should return error message for invalid URLs", async () => { const result = await fetchTool.execute({ url: "not-a-valid-url", }); expect(result).toContain("Invalid URL: not-a-valid-url"); }); test("should handle redirects", async () => { const content = "Redirected content"; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: "OK", url: "https://example.com/redirected.txt", // Final URL after redirect headers: new Headers({ "content-type": "text/plain" }), text: () => Promise.resolve(content), }); const result = await fetchTool.execute({ url: "https://example.com/redirect-me.txt", }); expect(result).toContain("Contents of https://example.com/redirected.txt:"); expect(result).toContain(content); }); test("should clean up HTML content properly", async () => { const htmlWithScripts = `

Clean Title

Clean content & entities <test>

`; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: "OK", url: "https://example.com/clean.html", headers: new Headers({ "content-type": "text/html" }), text: () => Promise.resolve(htmlWithScripts), }); const result = await fetchTool.execute({ url: "https://example.com/clean.html", }); expect(result).toContain("# Clean Title"); expect(result).toContain("Clean content & entities "); expect(result).not.toContain("alert"); expect(result).not.toContain("evil"); expect(result).not.toContain("