mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 23:04:32 +00:00
refactor(test): share web fetch e2e setup helpers
This commit is contained in:
@@ -90,6 +90,41 @@ function requestUrl(input: RequestInfo): string {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function installMockFetch(impl: (input: RequestInfo) => Promise<Response>) {
|
||||||
|
const mockFetch = vi.fn(impl);
|
||||||
|
// @ts-expect-error mock fetch
|
||||||
|
global.fetch = mockFetch;
|
||||||
|
return mockFetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFetchTool(fetchOverrides: Record<string, unknown> = {}) {
|
||||||
|
return createWebFetchTool({
|
||||||
|
config: {
|
||||||
|
tools: {
|
||||||
|
web: {
|
||||||
|
fetch: {
|
||||||
|
cacheTtlMinutes: 0,
|
||||||
|
...fetchOverrides,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sandboxed: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureToolErrorMessage(params: {
|
||||||
|
tool: ReturnType<typeof createWebFetchTool>;
|
||||||
|
url: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
await params.tool?.execute?.("call", { url: params.url });
|
||||||
|
return "";
|
||||||
|
} catch (error) {
|
||||||
|
return (error as Error).message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("web_fetch extraction fallbacks", () => {
|
describe("web_fetch extraction fallbacks", () => {
|
||||||
const priorFetch = global.fetch;
|
const priorFetch = global.fetch;
|
||||||
|
|
||||||
@@ -112,7 +147,7 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("wraps fetched text with external content markers", async () => {
|
it("wraps fetched text with external content markers", async () => {
|
||||||
const mockFetch = vi.fn((input: RequestInfo) =>
|
installMockFetch((input: RequestInfo) =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -121,19 +156,8 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
url: requestUrl(input),
|
url: requestUrl(input),
|
||||||
} as Response),
|
} as Response),
|
||||||
);
|
);
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({ firecrawl: { enabled: false } });
|
||||||
config: {
|
|
||||||
tools: {
|
|
||||||
web: {
|
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", { url: "https://example.com/plain" });
|
const result = await tool?.execute?.("call", { url: "https://example.com/plain" });
|
||||||
const details = result?.details as {
|
const details = result?.details as {
|
||||||
@@ -161,7 +185,7 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
|
|
||||||
it("enforces maxChars after wrapping", async () => {
|
it("enforces maxChars after wrapping", async () => {
|
||||||
const longText = "x".repeat(5_000);
|
const longText = "x".repeat(5_000);
|
||||||
const mockFetch = vi.fn((input: RequestInfo) =>
|
installMockFetch((input: RequestInfo) =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -170,18 +194,10 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
url: requestUrl(input),
|
url: requestUrl(input),
|
||||||
} as Response),
|
} as Response),
|
||||||
);
|
);
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({
|
||||||
config: {
|
firecrawl: { enabled: false },
|
||||||
tools: {
|
maxChars: 2000,
|
||||||
web: {
|
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false }, maxChars: 2000 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", { url: "https://example.com/long" });
|
const result = await tool?.execute?.("call", { url: "https://example.com/long" });
|
||||||
@@ -192,7 +208,7 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("honors maxChars even when wrapper overhead exceeds limit", async () => {
|
it("honors maxChars even when wrapper overhead exceeds limit", async () => {
|
||||||
const mockFetch = vi.fn((input: RequestInfo) =>
|
installMockFetch((input: RequestInfo) =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -201,18 +217,10 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
url: requestUrl(input),
|
url: requestUrl(input),
|
||||||
} as Response),
|
} as Response),
|
||||||
);
|
);
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({
|
||||||
config: {
|
firecrawl: { enabled: false },
|
||||||
tools: {
|
maxChars: 100,
|
||||||
web: {
|
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false }, maxChars: 100 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", { url: "https://example.com/short" });
|
const result = await tool?.execute?.("call", { url: "https://example.com/short" });
|
||||||
@@ -226,7 +234,7 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
// The sanitization of these fields is verified by external-content.test.ts tests.
|
// The sanitization of these fields is verified by external-content.test.ts tests.
|
||||||
|
|
||||||
it("falls back to firecrawl when readability returns no content", async () => {
|
it("falls back to firecrawl when readability returns no content", async () => {
|
||||||
const mockFetch = vi.fn((input: RequestInfo) => {
|
installMockFetch((input: RequestInfo) => {
|
||||||
const url = requestUrl(input);
|
const url = requestUrl(input);
|
||||||
if (url.includes("api.firecrawl.dev")) {
|
if (url.includes("api.firecrawl.dev")) {
|
||||||
return Promise.resolve(firecrawlResponse("firecrawl content")) as Promise<Response>;
|
return Promise.resolve(firecrawlResponse("firecrawl content")) as Promise<Response>;
|
||||||
@@ -235,21 +243,9 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
htmlResponse("<!doctype html><html><head></head><body></body></html>", url),
|
htmlResponse("<!doctype html><html><head></head><body></body></html>", url),
|
||||||
) as Promise<Response>;
|
) as Promise<Response>;
|
||||||
});
|
});
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({
|
||||||
config: {
|
firecrawl: { apiKey: "firecrawl-test" },
|
||||||
tools: {
|
|
||||||
web: {
|
|
||||||
fetch: {
|
|
||||||
cacheTtlMinutes: 0,
|
|
||||||
firecrawl: { apiKey: "firecrawl-test" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", { url: "https://example.com/empty" });
|
const result = await tool?.execute?.("call", { url: "https://example.com/empty" });
|
||||||
@@ -259,21 +255,13 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("throws when readability is disabled and firecrawl is unavailable", async () => {
|
it("throws when readability is disabled and firecrawl is unavailable", async () => {
|
||||||
const mockFetch = vi.fn((input: RequestInfo) =>
|
installMockFetch((input: RequestInfo) =>
|
||||||
Promise.resolve(htmlResponse("<html><body>hi</body></html>", requestUrl(input))),
|
Promise.resolve(htmlResponse("<html><body>hi</body></html>", requestUrl(input))),
|
||||||
);
|
);
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({
|
||||||
config: {
|
readability: false,
|
||||||
tools: {
|
firecrawl: { enabled: false },
|
||||||
web: {
|
|
||||||
fetch: { readability: false, cacheTtlMinutes: 0, firecrawl: { enabled: false } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -282,7 +270,7 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("throws when readability is empty and firecrawl fails", async () => {
|
it("throws when readability is empty and firecrawl fails", async () => {
|
||||||
const mockFetch = vi.fn((input: RequestInfo) => {
|
installMockFetch((input: RequestInfo) => {
|
||||||
const url = requestUrl(input);
|
const url = requestUrl(input);
|
||||||
if (url.includes("api.firecrawl.dev")) {
|
if (url.includes("api.firecrawl.dev")) {
|
||||||
return Promise.resolve(firecrawlError()) as Promise<Response>;
|
return Promise.resolve(firecrawlError()) as Promise<Response>;
|
||||||
@@ -291,18 +279,9 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
htmlResponse("<!doctype html><html><head></head><body></body></html>", url),
|
htmlResponse("<!doctype html><html><head></head><body></body></html>", url),
|
||||||
) as Promise<Response>;
|
) as Promise<Response>;
|
||||||
});
|
});
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({
|
||||||
config: {
|
firecrawl: { apiKey: "firecrawl-test" },
|
||||||
tools: {
|
|
||||||
web: {
|
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { apiKey: "firecrawl-test" } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -311,7 +290,7 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses firecrawl when direct fetch fails", async () => {
|
it("uses firecrawl when direct fetch fails", async () => {
|
||||||
const mockFetch = vi.fn((input: RequestInfo) => {
|
installMockFetch((input: RequestInfo) => {
|
||||||
const url = requestUrl(input);
|
const url = requestUrl(input);
|
||||||
if (url.includes("api.firecrawl.dev")) {
|
if (url.includes("api.firecrawl.dev")) {
|
||||||
return Promise.resolve(firecrawlResponse("firecrawl fallback", url)) as Promise<Response>;
|
return Promise.resolve(firecrawlResponse("firecrawl fallback", url)) as Promise<Response>;
|
||||||
@@ -323,18 +302,9 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
text: async () => "blocked",
|
text: async () => "blocked",
|
||||||
} as Response);
|
} as Response);
|
||||||
});
|
});
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({
|
||||||
config: {
|
firecrawl: { apiKey: "firecrawl-test" },
|
||||||
tools: {
|
|
||||||
web: {
|
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { apiKey: "firecrawl-test" } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", { url: "https://example.com/blocked" });
|
const result = await tool?.execute?.("call", { url: "https://example.com/blocked" });
|
||||||
@@ -345,22 +315,14 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
|
|
||||||
it("wraps external content and clamps oversized maxChars", async () => {
|
it("wraps external content and clamps oversized maxChars", async () => {
|
||||||
const large = "a".repeat(80_000);
|
const large = "a".repeat(80_000);
|
||||||
const mockFetch = vi.fn(
|
installMockFetch(
|
||||||
(input: RequestInfo) =>
|
(input: RequestInfo) =>
|
||||||
Promise.resolve(textResponse(large, requestUrl(input))) as Promise<Response>,
|
Promise.resolve(textResponse(large, requestUrl(input))) as Promise<Response>,
|
||||||
);
|
);
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({
|
||||||
config: {
|
firecrawl: { enabled: false },
|
||||||
tools: {
|
maxCharsCap: 10_000,
|
||||||
web: {
|
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false }, maxCharsCap: 10_000 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tool?.execute?.("call", {
|
const result = await tool?.execute?.("call", {
|
||||||
@@ -373,36 +335,23 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
expect(details.length).toBeLessThanOrEqual(10_000);
|
expect(details.length).toBeLessThanOrEqual(10_000);
|
||||||
expect(details.truncated).toBe(true);
|
expect(details.truncated).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips and truncates HTML from error responses", async () => {
|
it("strips and truncates HTML from error responses", async () => {
|
||||||
const long = "x".repeat(12_000);
|
const long = "x".repeat(12_000);
|
||||||
const html =
|
const html =
|
||||||
"<!doctype html><html><head><title>Not Found</title></head><body><h1>Not Found</h1><p>" +
|
"<!doctype html><html><head><title>Not Found</title></head><body><h1>Not Found</h1><p>" +
|
||||||
long +
|
long +
|
||||||
"</p></body></html>";
|
"</p></body></html>";
|
||||||
const mockFetch = vi.fn((input: RequestInfo) =>
|
installMockFetch((input: RequestInfo) =>
|
||||||
Promise.resolve(errorHtmlResponse(html, 404, requestUrl(input), "Text/HTML; charset=utf-8")),
|
Promise.resolve(errorHtmlResponse(html, 404, requestUrl(input), "Text/HTML; charset=utf-8")),
|
||||||
);
|
);
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({ firecrawl: { enabled: false } });
|
||||||
config: {
|
const message = await captureToolErrorMessage({
|
||||||
tools: {
|
tool,
|
||||||
web: {
|
url: "https://example.com/missing",
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = "";
|
|
||||||
try {
|
|
||||||
await tool?.execute?.("call", { url: "https://example.com/missing" });
|
|
||||||
} catch (error) {
|
|
||||||
message = (error as Error).message;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(message).toContain("Web fetch failed (404):");
|
expect(message).toContain("Web fetch failed (404):");
|
||||||
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
|
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
|
||||||
expect(message).toContain("SECURITY NOTICE");
|
expect(message).toContain("SECURITY NOTICE");
|
||||||
@@ -414,37 +363,23 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
it("strips HTML errors when content-type is missing", async () => {
|
it("strips HTML errors when content-type is missing", async () => {
|
||||||
const html =
|
const html =
|
||||||
"<!DOCTYPE HTML><html><head><title>Oops</title></head><body><h1>Oops</h1></body></html>";
|
"<!DOCTYPE HTML><html><head><title>Oops</title></head><body><h1>Oops</h1></body></html>";
|
||||||
const mockFetch = vi.fn((input: RequestInfo) =>
|
installMockFetch((input: RequestInfo) =>
|
||||||
Promise.resolve(errorHtmlResponse(html, 500, requestUrl(input), null)),
|
Promise.resolve(errorHtmlResponse(html, 500, requestUrl(input), null)),
|
||||||
);
|
);
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({ firecrawl: { enabled: false } });
|
||||||
config: {
|
const message = await captureToolErrorMessage({
|
||||||
tools: {
|
tool,
|
||||||
web: {
|
url: "https://example.com/oops",
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = "";
|
|
||||||
try {
|
|
||||||
await tool?.execute?.("call", { url: "https://example.com/oops" });
|
|
||||||
} catch (error) {
|
|
||||||
message = (error as Error).message;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(message).toContain("Web fetch failed (500):");
|
expect(message).toContain("Web fetch failed (500):");
|
||||||
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
|
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
|
||||||
expect(message).toContain("Oops");
|
expect(message).toContain("Oops");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("wraps firecrawl error details", async () => {
|
it("wraps firecrawl error details", async () => {
|
||||||
const mockFetch = vi.fn((input: RequestInfo) => {
|
installMockFetch((input: RequestInfo) => {
|
||||||
const url = requestUrl(input);
|
const url = requestUrl(input);
|
||||||
if (url.includes("api.firecrawl.dev")) {
|
if (url.includes("api.firecrawl.dev")) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
@@ -455,26 +390,15 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new Error("network down"));
|
return Promise.reject(new Error("network down"));
|
||||||
});
|
});
|
||||||
// @ts-expect-error mock fetch
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
const tool = createWebFetchTool({
|
const tool = createFetchTool({
|
||||||
config: {
|
firecrawl: { apiKey: "firecrawl-test" },
|
||||||
tools: {
|
|
||||||
web: {
|
|
||||||
fetch: { cacheTtlMinutes: 0, firecrawl: { apiKey: "firecrawl-test" } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sandboxed: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = "";
|
const message = await captureToolErrorMessage({
|
||||||
try {
|
tool,
|
||||||
await tool?.execute?.("call", { url: "https://example.com/firecrawl-error" });
|
url: "https://example.com/firecrawl-error",
|
||||||
} catch (error) {
|
});
|
||||||
message = (error as Error).message;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(message).toContain("Firecrawl fetch failed (403):");
|
expect(message).toContain("Firecrawl fetch failed (403):");
|
||||||
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
|
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
|
||||||
|
|||||||
Reference in New Issue
Block a user