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

@@ -33,7 +33,7 @@ Mission → Milestone → Slice → Feature → Task
- **Agent tools** — `fn_agent_stop`, `fn_agent_start`, `fn_list_agents`, `fn_delegate_task`, `fn_agent_show`, `fn_agent_org_chart`
- **Skills tools** — `fn_skills_search`, `fn_skills_install`
- **Insight tools** — `fn_insight_list`, `fn_insight_show`, `fn_insight_run_list`, `fn_insight_run_show`
- **Other tools** — `fn_research_run`, `fn_research_list`, `fn_research_get`, `fn_research_cancel`, `fn_research_retry`
- **Other tools** — `fn_web_fetch`, `fn_research_run`, `fn_research_list`, `fn_research_get`, `fn_research_cancel`, `fn_research_retry`
<!-- END: tool-categories -->
- **Dashboard** — Use `/fn` command to start/stop the dashboard

View File

@@ -18,6 +18,7 @@ These tools are **not** part of the user-invokable extension surface. They are i
| `fn_memory_search` | triage, executor, heartbeat | Search project memory plus per-agent layered memory snippets | `query` (string), `limit?` (number) |
| `fn_memory_get` | triage, executor, heartbeat | Read a bounded memory file window (including bounded per-agent layered paths) | `path` (string), `startLine?` (number), `lineCount?` (number) |
| `fn_memory_append` | executor, heartbeat (when writable backend enabled) | Append memory notes with explicit scope: `scope="agent"` for private operating context, `scope="project"` for workspace-wide durable knowledge | `scope?` (`project` \| `agent`), `layer` (`long-term` \| `daily`), `content` (string) |
| `fn_web_fetch` | executor, step-session, heartbeat | Lightweight HTTP fetch with HTML→text extraction, timeout/size caps, and SSRF guard (no JS rendering) | `url` (string), `prompt?` (string), `timeoutMs?` (number), `maxBytes?` (number) |
| `fn_research_run` | triage, executor | Start a bounded research run (optionally wait for completion) and return structured findings metadata | `query` (string), `wait_for_completion?` (boolean), `max_wait_ms?` (number) |
| `fn_research_list` | triage, executor | List recent research runs with status/summary metadata | `status?` (`pending` \| `running` \| `completed` \| `failed` \| `cancelled`), `limit?` (number) |
| `fn_research_get` | triage, executor | Read one research run's structured findings/citations payload | `id` (string) |

View File

@@ -364,6 +364,17 @@ Show a single insight-generation run by ID.
## Other Tools
### fn_web_fetch
Lightweight URL fetch (no JS rendering). Use agent-browser skill for JS-heavy pages. URL to fetch (http/https) Optional extraction hint for downstream summarization Timeout in milliseconds (default: 30000) Max bytes to return (default: 512000)
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `url` | string | ✓ | URL to fetch (http/https) |
| `prompt` | string | — | Optional extraction hint for downstream summarization |
| `timeoutMs` | number | — | Timeout in milliseconds (default: 30000) |
| `maxBytes` | number | — | Max bytes to return (default: 512000) |
### fn_research_run
Start a bounded research run and optionally wait for findings.

View File

@@ -29,6 +29,7 @@ All skill/extension tool invocations in this catalog use the public `fn_*` names
| `fn_task_import_github_issue` | Import a specific GitHub issue as a Fusion task. Fetches the issue by number and creates a single task in the planning column with the issue title and body. |
| `fn_task_browse_github_issues` | List open GitHub issues from a repository to browse before importing. Returns issue numbers, titles, and URLs for selection. Use with fn_task_import_github_issue to import specific issues by number. |
| `fn_task_plan` | Create a task via AI-guided planning mode — interactive conversation to refine your idea into a well-specified task. |
| `fn_web_fetch` | Lightweight URL fetch (no JS rendering). Use agent-browser skill for JS-heavy pages. URL to fetch (http/https) Optional extraction hint for downstream summarization Timeout in milliseconds (default: 30000) Max bytes to return (default: 512000) |
| `fn_research_run` | Start a bounded research run and optionally wait for findings. |
| `fn_research_list` | List recent research runs. |
| `fn_research_get` | Get one research run and structured findings. |

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({