feat(FN-3758): align web fetch test coverage and documentation
Aligns web fetch tool test coverage across multiple test suites and updates the agents documentation, with a patch changeset for `@runfusion/fusion`. Fusion-Task-Id: FN-3758
This commit is contained in:
44
packages/engine/src/__tests__/agent-tools-web-fetch.test.ts
Normal file
44
packages/engine/src/__tests__/agent-tools-web-fetch.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createWebFetchTool } from "../agent-tools.js";
|
||||
|
||||
describe("createWebFetchTool", () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("returns fetched content with metadata", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://example.com/final",
|
||||
headers: new Headers({ "content-type": "text/plain" }),
|
||||
text: async () => "hello world",
|
||||
} as Response);
|
||||
|
||||
const tool = createWebFetchTool();
|
||||
const result = await tool.execute("id", { url: "https://example.com" } as any, undefined, undefined, {} as any);
|
||||
const text = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
|
||||
|
||||
expect((result as any).isError).toBeUndefined();
|
||||
expect(text).toContain("URL: https://example.com/final");
|
||||
expect(text).toContain("Status: 200");
|
||||
expect(text).toContain("hello world");
|
||||
});
|
||||
|
||||
it("returns blocked-host error", async () => {
|
||||
const tool = createWebFetchTool();
|
||||
const result = await tool.execute("id", { url: "http://127.0.0.1" } as any, undefined, undefined, {} as any);
|
||||
const text = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
|
||||
|
||||
expect((result as any).isError).toBe(true);
|
||||
expect(text).toContain("blocked-host");
|
||||
});
|
||||
|
||||
it("requires url parameter", async () => {
|
||||
const tool = createWebFetchTool();
|
||||
await expect(tool.execute("id", {} as any, undefined, undefined, {} as any)).resolves.toMatchObject({ isError: true });
|
||||
});
|
||||
});
|
||||
@@ -1920,8 +1920,8 @@ describe("executeHeartbeat", () => {
|
||||
expect(callArgs.systemPrompt).toContain("fn_task_document_write");
|
||||
expect(callArgs.tools).toBe("coding");
|
||||
// Tools: fn_task_create, fn_task_log, fn_task_document_write, fn_task_document_read, fn_list_agents, fn_delegate_task,
|
||||
// fn_get_agent_config, fn_update_agent_config, fn_read_evaluations, fn_update_identity, fn_memory_search, fn_memory_get, fn_memory_append, fn_heartbeat_done
|
||||
expect(callArgs.customTools).toHaveLength(14);
|
||||
// fn_get_agent_config, fn_update_agent_config, fn_read_evaluations, fn_update_identity, fn_web_fetch, fn_memory_search, fn_memory_get, fn_memory_append, fn_heartbeat_done
|
||||
expect(callArgs.customTools).toHaveLength(15);
|
||||
expect(callArgs.customTools![0]!.name).toBe("fn_task_create");
|
||||
expect(callArgs.customTools![1]!.name).toBe("fn_task_log");
|
||||
expect(callArgs.customTools![2]!.name).toBe("fn_task_document_write");
|
||||
@@ -1932,11 +1932,12 @@ describe("executeHeartbeat", () => {
|
||||
expect(callArgs.customTools![7]!.name).toBe("fn_update_agent_config");
|
||||
expect(callArgs.customTools![8]!.name).toBe("fn_read_evaluations");
|
||||
expect(callArgs.customTools![9]!.name).toBe("fn_update_identity");
|
||||
expect(callArgs.customTools![10]!.name).toBe("fn_memory_search");
|
||||
expect(callArgs.customTools![11]!.name).toBe("fn_memory_get");
|
||||
expect(callArgs.customTools![12]!.name).toBe("fn_memory_append");
|
||||
expect(callArgs.customTools![10]!.name).toBe("fn_web_fetch");
|
||||
expect(callArgs.customTools![11]!.name).toBe("fn_memory_search");
|
||||
expect(callArgs.customTools![12]!.name).toBe("fn_memory_get");
|
||||
expect(callArgs.customTools![13]!.name).toBe("fn_memory_append");
|
||||
// fn_heartbeat_done is last (terminal tool)
|
||||
expect(callArgs.customTools![13]!.name).toBe("fn_heartbeat_done");
|
||||
expect(callArgs.customTools![14]!.name).toBe("fn_heartbeat_done");
|
||||
});
|
||||
|
||||
it("loads workspace memory into system prompt and identity snapshot when inline memory is empty", async () => {
|
||||
|
||||
96
packages/engine/src/__tests__/web-fetch.test.ts
Normal file
96
packages/engine/src/__tests__/web-fetch.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { dnsResolver, fetchWebContent, WebFetchError } from "../web-fetch.js";
|
||||
|
||||
describe("web-fetch", () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("extracts html content", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://example.com/final",
|
||||
headers: new Headers({ "content-type": "text/html" }),
|
||||
text: async () => "<html><head><title>Hello</title><meta name='description' content='Desc'></head><body><main><h1>Title</h1><p>Body</p></main></body></html>",
|
||||
} as Response);
|
||||
|
||||
const result = await fetchWebContent("https://example.com");
|
||||
expect(result.content).toContain("Title Body");
|
||||
expect(result.title).toBe("Hello");
|
||||
expect(result.description).toBe("Desc");
|
||||
expect(result.finalUrl).toBe("https://example.com/final");
|
||||
});
|
||||
|
||||
it("pretty prints json", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://example.com",
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
text: async () => '{"a":1}',
|
||||
} as Response);
|
||||
|
||||
const result = await fetchWebContent("https://example.com");
|
||||
expect(result.content).toContain('"a": 1');
|
||||
});
|
||||
|
||||
it("passes through plain text", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://example.com",
|
||||
headers: new Headers({ "content-type": "text/plain" }),
|
||||
text: async () => "hello world",
|
||||
} as Response);
|
||||
|
||||
const result = await fetchWebContent("https://example.com");
|
||||
expect(result.content).toBe("hello world");
|
||||
});
|
||||
|
||||
it("maps timeout", async () => {
|
||||
global.fetch = vi.fn().mockImplementation(async (_url, init: RequestInit) => {
|
||||
await new Promise((_, reject) => init.signal?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError"))));
|
||||
return { ok: true, status: 200, url: "https://example.com", headers: new Headers(), text: async () => "" } as Response;
|
||||
});
|
||||
await expect(fetchWebContent("https://example.com", { timeoutMs: 1 })).rejects.toMatchObject({ code: "timeout" });
|
||||
});
|
||||
|
||||
it("truncates content to max bytes", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://example.com",
|
||||
headers: new Headers({ "content-type": "text/plain" }),
|
||||
text: async () => "x".repeat(100),
|
||||
} as Response);
|
||||
|
||||
const result = await fetchWebContent("https://example.com", { maxBytes: 10 });
|
||||
expect(result.content).toHaveLength(10);
|
||||
expect(result.truncated).toBe(true);
|
||||
expect(result.bytesRead).toBe(100);
|
||||
});
|
||||
|
||||
it("maps non-ok response to http-error", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404 } as Response);
|
||||
await expect(fetchWebContent("https://example.com")).rejects.toMatchObject({ code: "http-error" });
|
||||
});
|
||||
|
||||
it.each(["file:///tmp/test", "ftp://example.com", "data:text/plain,hello"])("blocks unsupported scheme %s", async (url) => {
|
||||
await expect(fetchWebContent(url)).rejects.toMatchObject({ code: "blocked-scheme" });
|
||||
});
|
||||
|
||||
it.each(["http://127.0.0.1", "http://10.0.0.5", "http://[::1]", "http://169.254.0.1"])("blocks private literal host %s", async (url) => {
|
||||
await expect(fetchWebContent(url)).rejects.toMatchObject({ code: "blocked-host" });
|
||||
});
|
||||
|
||||
it("blocks dns-resolved private host", async () => {
|
||||
vi.spyOn(dnsResolver, "lookup").mockResolvedValue([{ address: "10.0.0.1", family: 4 }] as unknown as Awaited<ReturnType<typeof dnsResolver.lookup>>);
|
||||
const pending = fetchWebContent("https://internal.example.com");
|
||||
await expect(pending).rejects.toBeInstanceOf(WebFetchError);
|
||||
await expect(pending).rejects.toMatchObject({ code: "blocked-host" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user