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:
Fusion
2026-05-08 18:47:58 -07:00
committed by gsxdsm
parent eed16abacb
commit a79e91d3aa
18 changed files with 633 additions and 72 deletions

View File

@@ -0,0 +1,43 @@
import { describe, expect, it, vi } from "vitest";
const fetchWebContentMock = vi.hoisted(() => vi.fn());
vi.mock("@fusion/engine", () => ({
fetchWebContent: fetchWebContentMock,
}));
import kbExtension from "../extension.js";
describe("extension fn_web_fetch", () => {
it("registers and executes fn_web_fetch", async () => {
const tools = new Map<string, any>();
const api = {
registerTool(def: any) {
tools.set(def.name, def);
},
registerCommand: vi.fn(),
registerShortcut: vi.fn(),
registerFlag: vi.fn(),
on: vi.fn(),
} as any;
fetchWebContentMock.mockResolvedValue({
finalUrl: "https://example.com/final",
status: 200,
contentType: "text/plain",
title: "Example",
content: "hello world",
truncated: false,
bytesRead: 11,
});
kbExtension(api);
const tool = tools.get("fn_web_fetch");
expect(tool).toBeTruthy();
const result = await tool.execute("id", { url: "https://example.com" }, undefined, undefined, { cwd: process.cwd() });
expect(fetchWebContentMock).toHaveBeenCalledWith("https://example.com", { timeoutMs: undefined, maxBytes: undefined });
expect(result.content[0].text).toContain("https://example.com/final");
expect(result.details.status).toBe(200);
});
});

View File

@@ -25,6 +25,7 @@ import {
isGhAvailable,
runGhJsonAsync,
} from "@fusion/core/gh-cli";
import { fetchWebContent } from "@fusion/engine";
import { resolve, basename, extname, join } from "node:path";
import { readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
@@ -1363,6 +1364,54 @@ export default function kbExtension(pi: ExtensionAPI) {
},
});
pi.registerTool({
name: "fn_web_fetch",
label: "fn: Web Fetch",
description: "Lightweight URL fetch (no JS rendering). Use agent-browser skill for JS-heavy pages.",
parameters: Type.Object({
url: Type.String({ description: "URL to fetch (http/https)" }),
prompt: Type.Optional(Type.String({ description: "Optional extraction hint for downstream summarization" })),
timeoutMs: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 30000)" })),
maxBytes: Type.Optional(Type.Number({ description: "Max bytes to return (default: 512000)" })),
}),
promptSnippet: "Fetch and extract readable text from a webpage URL",
promptGuidelines: [
"Use for lightweight GET requests where JS rendering is not required.",
"For JS-rendered pages or complex browsing flows, use agent-browser skill instead.",
],
async execute(_toolCallId, params) {
const result = await fetchWebContent(params.url, {
timeoutMs: params.timeoutMs,
maxBytes: params.maxBytes,
});
return {
content: [{
type: "text",
text: [
`URL: ${result.finalUrl}`,
`Status: ${result.status}`,
`Content-Type: ${result.contentType}`,
params.prompt ? `Prompt: ${params.prompt}` : undefined,
result.title ? `Title: ${result.title}` : undefined,
"",
result.content,
result.truncated ? "\n[truncated to maxBytes]" : "",
].filter(Boolean).join("\n"),
}],
details: {
finalUrl: result.finalUrl,
status: result.status,
contentType: result.contentType,
title: result.title,
truncated: result.truncated,
bytesRead: result.bytesRead,
promptSnippet: params.prompt ? params.prompt.slice(0, 200) : undefined,
promptGuidelines: "Lightweight fetch only; for JS-rendered pages, use agent-browser skill.",
},
};
},
});
// ── Research Tools ──────────────────────────────────────────────
pi.registerTool({