mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 12:07:40 +00:00
refactor(agent): dedupe harness and command workflows
This commit is contained in:
@@ -12,6 +12,28 @@ vi.mock("../agent-scope.js", () => ({
|
||||
import { createCronTool } from "./cron-tool.js";
|
||||
|
||||
describe("cron tool", () => {
|
||||
async function executeAddAndReadDelivery(params: {
|
||||
callId: string;
|
||||
agentSessionKey: string;
|
||||
delivery?: { mode?: string; channel?: string; to?: string } | null;
|
||||
}) {
|
||||
const tool = createCronTool({ agentSessionKey: params.agentSessionKey });
|
||||
await tool.execute(params.callId, {
|
||||
action: "add",
|
||||
job: {
|
||||
name: "reminder",
|
||||
schedule: { at: new Date(123).toISOString() },
|
||||
payload: { kind: "agentTurn", message: "hello" },
|
||||
...(params.delivery !== undefined ? { delivery: params.delivery } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
||||
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
|
||||
};
|
||||
return call?.params?.delivery;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
callGatewayMock.mockReset();
|
||||
callGatewayMock.mockResolvedValue({ ok: true });
|
||||
@@ -249,24 +271,12 @@ describe("cron tool", () => {
|
||||
});
|
||||
|
||||
it("infers delivery from threaded session keys", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||
|
||||
const tool = createCronTool({
|
||||
agentSessionKey: "agent:main:slack:channel:general:thread:1699999999.0001",
|
||||
});
|
||||
await tool.execute("call-thread", {
|
||||
action: "add",
|
||||
job: {
|
||||
name: "reminder",
|
||||
schedule: { at: new Date(123).toISOString() },
|
||||
payload: { kind: "agentTurn", message: "hello" },
|
||||
},
|
||||
});
|
||||
|
||||
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
||||
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
|
||||
};
|
||||
expect(call?.params?.delivery).toEqual({
|
||||
expect(
|
||||
await executeAddAndReadDelivery({
|
||||
callId: "call-thread",
|
||||
agentSessionKey: "agent:main:slack:channel:general:thread:1699999999.0001",
|
||||
}),
|
||||
).toEqual({
|
||||
mode: "announce",
|
||||
channel: "slack",
|
||||
to: "general",
|
||||
@@ -274,24 +284,12 @@ describe("cron tool", () => {
|
||||
});
|
||||
|
||||
it("preserves telegram forum topics when inferring delivery", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||
|
||||
const tool = createCronTool({
|
||||
agentSessionKey: "agent:main:telegram:group:-1001234567890:topic:99",
|
||||
});
|
||||
await tool.execute("call-telegram-topic", {
|
||||
action: "add",
|
||||
job: {
|
||||
name: "reminder",
|
||||
schedule: { at: new Date(123).toISOString() },
|
||||
payload: { kind: "agentTurn", message: "hello" },
|
||||
},
|
||||
});
|
||||
|
||||
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
||||
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
|
||||
};
|
||||
expect(call?.params?.delivery).toEqual({
|
||||
expect(
|
||||
await executeAddAndReadDelivery({
|
||||
callId: "call-telegram-topic",
|
||||
agentSessionKey: "agent:main:telegram:group:-1001234567890:topic:99",
|
||||
}),
|
||||
).toEqual({
|
||||
mode: "announce",
|
||||
channel: "telegram",
|
||||
to: "-1001234567890:topic:99",
|
||||
@@ -299,23 +297,13 @@ describe("cron tool", () => {
|
||||
});
|
||||
|
||||
it("infers delivery when delivery is null", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||
|
||||
const tool = createCronTool({ agentSessionKey: "agent:main:dm:alice" });
|
||||
await tool.execute("call-null-delivery", {
|
||||
action: "add",
|
||||
job: {
|
||||
name: "reminder",
|
||||
schedule: { at: new Date(123).toISOString() },
|
||||
payload: { kind: "agentTurn", message: "hello" },
|
||||
expect(
|
||||
await executeAddAndReadDelivery({
|
||||
callId: "call-null-delivery",
|
||||
agentSessionKey: "agent:main:dm:alice",
|
||||
delivery: null,
|
||||
},
|
||||
});
|
||||
|
||||
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
||||
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
|
||||
};
|
||||
expect(call?.params?.delivery).toEqual({
|
||||
}),
|
||||
).toEqual({
|
||||
mode: "announce",
|
||||
to: "alice",
|
||||
});
|
||||
|
||||
@@ -62,6 +62,22 @@ function createMinimaxImageConfig(): OpenClawConfig {
|
||||
};
|
||||
}
|
||||
|
||||
async function expectImageToolExecOk(
|
||||
tool: {
|
||||
execute: (toolCallId: string, input: { prompt: string; image: string }) => Promise<unknown>;
|
||||
},
|
||||
image: string,
|
||||
) {
|
||||
await expect(
|
||||
tool.execute("t1", {
|
||||
prompt: "Describe the image.",
|
||||
image,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
});
|
||||
}
|
||||
|
||||
describe("image tool implicit imageModel config", () => {
|
||||
const priorFetch = global.fetch;
|
||||
|
||||
@@ -220,14 +236,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
throw new Error("expected image tool");
|
||||
}
|
||||
|
||||
await expect(
|
||||
withWorkspace.execute("t1", {
|
||||
prompt: "Describe the image.",
|
||||
image: imagePath,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
});
|
||||
await expectImageToolExecOk(withWorkspace, imagePath);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
@@ -250,14 +259,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
throw new Error("expected image tool");
|
||||
}
|
||||
|
||||
await expect(
|
||||
tool.execute("t1", {
|
||||
prompt: "Describe the image.",
|
||||
image: imagePath,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
});
|
||||
await expectImageToolExecOk(tool, imagePath);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
@@ -383,15 +385,15 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
global.fetch = priorFetch;
|
||||
});
|
||||
|
||||
it("calls /v1/coding_plan/vlm for minimax image models", async () => {
|
||||
async function createMinimaxVlmFixture(baseResp: { status_code: number; status_msg: string }) {
|
||||
const fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: new Headers(),
|
||||
json: async () => ({
|
||||
content: "ok",
|
||||
base_resp: { status_code: 0, status_msg: "" },
|
||||
content: baseResp.status_code === 0 ? "ok" : "",
|
||||
base_resp: baseResp,
|
||||
}),
|
||||
});
|
||||
// @ts-expect-error partial global
|
||||
@@ -407,6 +409,11 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
if (!tool) {
|
||||
throw new Error("expected image tool");
|
||||
}
|
||||
return { fetch, tool };
|
||||
}
|
||||
|
||||
it("calls /v1/coding_plan/vlm for minimax image models", async () => {
|
||||
const { fetch, tool } = await createMinimaxVlmFixture({ status_code: 0, status_msg: "" });
|
||||
|
||||
const res = await tool.execute("t1", {
|
||||
prompt: "Describe the image.",
|
||||
@@ -428,29 +435,7 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
});
|
||||
|
||||
it("surfaces MiniMax API errors from /v1/coding_plan/vlm", async () => {
|
||||
const fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: new Headers(),
|
||||
json: async () => ({
|
||||
content: "",
|
||||
base_resp: { status_code: 1004, status_msg: "bad key" },
|
||||
}),
|
||||
});
|
||||
// @ts-expect-error partial global
|
||||
global.fetch = fetch;
|
||||
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-minimax-vlm-"));
|
||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.1" } } },
|
||||
};
|
||||
const tool = createImageTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) {
|
||||
throw new Error("expected image tool");
|
||||
}
|
||||
const { tool } = await createMinimaxVlmFixture({ status_code: 1004, status_msg: "bad key" });
|
||||
|
||||
await expect(
|
||||
tool.execute("t1", {
|
||||
|
||||
@@ -19,17 +19,22 @@ vi.mock("../../infra/outbound/message-action-runner.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
function mockSendResult(overrides: { channel?: string; to?: string } = {}) {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: overrides.channel ?? "telegram",
|
||||
...(overrides.to ? { to: overrides.to } : {}),
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
}
|
||||
|
||||
describe("message tool agent routing", () => {
|
||||
it("derives agentId from the session key", async () => {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: "telegram",
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
mockSendResult();
|
||||
|
||||
const tool = createMessageTool({
|
||||
agentSessionKey: "agent:alpha:main",
|
||||
@@ -50,16 +55,7 @@ describe("message tool agent routing", () => {
|
||||
|
||||
describe("message tool path passthrough", () => {
|
||||
it("does not convert path to media for send", async () => {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
mockSendResult({ to: "telegram:123" });
|
||||
|
||||
const tool = createMessageTool({
|
||||
config: {} as never,
|
||||
@@ -78,16 +74,7 @@ describe("message tool path passthrough", () => {
|
||||
});
|
||||
|
||||
it("does not convert filePath to media for send", async () => {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
mockSendResult({ to: "telegram:123" });
|
||||
|
||||
const tool = createMessageTool({
|
||||
config: {} as never,
|
||||
@@ -164,16 +151,7 @@ describe("message tool description", () => {
|
||||
|
||||
describe("message tool reasoning tag sanitization", () => {
|
||||
it("strips <think> tags from text field before sending", async () => {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: "signal",
|
||||
to: "signal:+15551234567",
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
mockSendResult({ channel: "signal", to: "signal:+15551234567" });
|
||||
|
||||
const tool = createMessageTool({ config: {} as never });
|
||||
|
||||
@@ -188,16 +166,7 @@ describe("message tool reasoning tag sanitization", () => {
|
||||
});
|
||||
|
||||
it("strips <think> tags from content field before sending", async () => {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: "discord",
|
||||
to: "discord:123",
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
mockSendResult({ channel: "discord", to: "discord:123" });
|
||||
|
||||
const tool = createMessageTool({ config: {} as never });
|
||||
|
||||
@@ -212,16 +181,7 @@ describe("message tool reasoning tag sanitization", () => {
|
||||
});
|
||||
|
||||
it("passes through text without reasoning tags unchanged", async () => {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: "signal",
|
||||
to: "signal:+15551234567",
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
mockSendResult({ channel: "signal", to: "signal:+15551234567" });
|
||||
|
||||
const tool = createMessageTool({ config: {} as never });
|
||||
|
||||
@@ -238,16 +198,7 @@ describe("message tool reasoning tag sanitization", () => {
|
||||
|
||||
describe("message tool sandbox passthrough", () => {
|
||||
it("forwards sandboxRoot to runMessageAction", async () => {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
mockSendResult({ to: "telegram:123" });
|
||||
|
||||
const tool = createMessageTool({
|
||||
config: {} as never,
|
||||
@@ -265,16 +216,7 @@ describe("message tool sandbox passthrough", () => {
|
||||
});
|
||||
|
||||
it("omits sandboxRoot when not configured", async () => {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
kind: "send",
|
||||
action: "send",
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
handledBy: "plugin",
|
||||
payload: {},
|
||||
dryRun: true,
|
||||
} satisfies MessageActionRunResult);
|
||||
mockSendResult({ to: "telegram:123" });
|
||||
|
||||
const tool = createMessageTool({
|
||||
config: {} as never,
|
||||
|
||||
@@ -22,6 +22,29 @@ vi.mock("../../telegram/send.js", () => ({
|
||||
}));
|
||||
|
||||
describe("handleTelegramAction", () => {
|
||||
const defaultReactionAction = {
|
||||
action: "react",
|
||||
chatId: "123",
|
||||
messageId: "456",
|
||||
emoji: "✅",
|
||||
} as const;
|
||||
|
||||
function reactionConfig(reactionLevel: "minimal" | "extensive" | "off" | "ack"): OpenClawConfig {
|
||||
return {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel } },
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
async function expectReactionAdded(reactionLevel: "minimal" | "extensive") {
|
||||
await handleTelegramAction(defaultReactionAction, reactionConfig(reactionLevel));
|
||||
expect(reactMessageTelegram).toHaveBeenCalledWith(
|
||||
"123",
|
||||
456,
|
||||
"✅",
|
||||
expect.objectContaining({ token: "tok", remove: false }),
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
reactMessageTelegram.mockClear();
|
||||
sendMessageTelegram.mockClear();
|
||||
@@ -39,24 +62,7 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("adds reactions when reactionLevel is minimal", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "minimal" } },
|
||||
} as OpenClawConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
chatId: "123",
|
||||
messageId: "456",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(reactMessageTelegram).toHaveBeenCalledWith(
|
||||
"123",
|
||||
456,
|
||||
"✅",
|
||||
expect.objectContaining({ token: "tok", remove: false }),
|
||||
);
|
||||
await expectReactionAdded("minimal");
|
||||
});
|
||||
|
||||
it("surfaces non-fatal reaction warnings", async () => {
|
||||
@@ -64,18 +70,7 @@ describe("handleTelegramAction", () => {
|
||||
ok: false,
|
||||
warning: "Reaction unavailable: ✅",
|
||||
});
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "minimal" } },
|
||||
} as OpenClawConfig;
|
||||
const result = await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
chatId: "123",
|
||||
messageId: "456",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
const result = await handleTelegramAction(defaultReactionAction, reactionConfig("minimal"));
|
||||
const textPayload = result.content.find((item) => item.type === "text");
|
||||
expect(textPayload?.type).toBe("text");
|
||||
const parsed = JSON.parse((textPayload as { type: "text"; text: string }).text) as {
|
||||
@@ -91,24 +86,7 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("adds reactions when reactionLevel is extensive", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "extensive" } },
|
||||
} as OpenClawConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
chatId: "123",
|
||||
messageId: "456",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(reactMessageTelegram).toHaveBeenCalledWith(
|
||||
"123",
|
||||
456,
|
||||
"✅",
|
||||
expect.objectContaining({ token: "tok", remove: false }),
|
||||
);
|
||||
await expectReactionAdded("extensive");
|
||||
});
|
||||
|
||||
it("removes reactions on empty emoji", async () => {
|
||||
@@ -167,9 +145,7 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("removes reactions when remove flag set", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "extensive" } },
|
||||
} as OpenClawConfig;
|
||||
const cfg = reactionConfig("extensive");
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
@@ -189,9 +165,7 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("blocks reactions when reactionLevel is off", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "off" } },
|
||||
} as OpenClawConfig;
|
||||
const cfg = reactionConfig("off");
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
@@ -206,9 +180,7 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
|
||||
it("blocks reactions when reactionLevel is ack", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "ack" } },
|
||||
} as OpenClawConfig;
|
||||
const cfg = reactionConfig("ack");
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import * as logger from "../../logger.js";
|
||||
import {
|
||||
createBaseWebFetchToolConfig,
|
||||
installWebFetchSsrfHarness,
|
||||
} from "./web-fetch.test-harness.js";
|
||||
import "./web-fetch.test-mocks.js";
|
||||
import { createWebFetchTool } from "./web-tools.js";
|
||||
|
||||
// Avoid dynamic-importing heavy readability deps in this unit test suite.
|
||||
vi.mock("./web-fetch-utils.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./web-fetch-utils.js")>("./web-fetch-utils.js");
|
||||
return {
|
||||
...actual,
|
||||
extractReadableContent: vi.fn().mockResolvedValue({
|
||||
title: "HTML Page",
|
||||
text: "HTML Page\n\nContent here.",
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const lookupMock = vi.fn();
|
||||
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
||||
const baseToolConfig = {
|
||||
config: {
|
||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
||||
},
|
||||
} as const;
|
||||
const baseToolConfig = createBaseWebFetchToolConfig();
|
||||
installWebFetchSsrfHarness();
|
||||
|
||||
function makeHeaders(map: Record<string, string>): { get: (key: string) => string | null } {
|
||||
return {
|
||||
@@ -49,22 +35,6 @@ function htmlResponse(body: string): Response {
|
||||
}
|
||||
|
||||
describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
const priorFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
|
||||
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation((hostname) =>
|
||||
resolvePinnedHostname(hostname, lookupMock),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-expect-error restore
|
||||
global.fetch = priorFetch;
|
||||
lookupMock.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("sends Accept header preferring text/markdown", async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(markdownResponse("# Test Page\n\nHello world."));
|
||||
// @ts-expect-error mock fetch
|
||||
|
||||
@@ -1,47 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createBaseWebFetchToolConfig,
|
||||
installWebFetchSsrfHarness,
|
||||
} from "./web-fetch.test-harness.js";
|
||||
import "./web-fetch.test-mocks.js";
|
||||
import { createWebFetchTool } from "./web-tools.js";
|
||||
|
||||
// Avoid dynamic-importing heavy readability deps in this unit test suite.
|
||||
vi.mock("./web-fetch-utils.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./web-fetch-utils.js")>("./web-fetch-utils.js");
|
||||
return {
|
||||
...actual,
|
||||
extractReadableContent: vi.fn().mockResolvedValue({
|
||||
title: "HTML Page",
|
||||
text: "HTML Page\n\nContent here.",
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const lookupMock = vi.fn();
|
||||
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
||||
const baseToolConfig = {
|
||||
config: {
|
||||
tools: {
|
||||
web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false }, maxResponseBytes: 1024 } },
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
const baseToolConfig = createBaseWebFetchToolConfig({ maxResponseBytes: 1024 });
|
||||
installWebFetchSsrfHarness();
|
||||
|
||||
describe("web_fetch response size limits", () => {
|
||||
const priorFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
|
||||
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation((hostname) =>
|
||||
resolvePinnedHostname(hostname, lookupMock),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-expect-error restore
|
||||
global.fetch = priorFetch;
|
||||
lookupMock.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("caps response bytes and does not hang on endless streams", async () => {
|
||||
const chunk = new TextEncoder().encode("<html><body><div>hi</div></body></html>");
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
|
||||
@@ -28,6 +28,30 @@ function textResponse(body: string): Response {
|
||||
} as Response;
|
||||
}
|
||||
|
||||
function setMockFetch(impl?: (...args: unknown[]) => unknown) {
|
||||
const fetchSpy = vi.fn(impl);
|
||||
global.fetch = fetchSpy as typeof fetch;
|
||||
return fetchSpy;
|
||||
}
|
||||
|
||||
async function createWebFetchToolForTest(params?: {
|
||||
firecrawl?: { enabled?: boolean; apiKey?: string };
|
||||
}) {
|
||||
const { createWebFetchTool } = await import("./web-tools.js");
|
||||
return createWebFetchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
cacheTtlMinutes: 0,
|
||||
firecrawl: params?.firecrawl ?? { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("web_fetch SSRF protection", () => {
|
||||
const priorFetch = global.fetch;
|
||||
|
||||
@@ -45,22 +69,9 @@ describe("web_fetch SSRF protection", () => {
|
||||
});
|
||||
|
||||
it("blocks localhost hostnames before fetch/firecrawl", async () => {
|
||||
const fetchSpy = vi.fn();
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const { createWebFetchTool } = await import("./web-tools.js");
|
||||
const tool = createWebFetchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
cacheTtlMinutes: 0,
|
||||
firecrawl: { apiKey: "firecrawl-test" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
const fetchSpy = setMockFetch();
|
||||
const tool = await createWebFetchToolForTest({
|
||||
firecrawl: { apiKey: "firecrawl-test" },
|
||||
});
|
||||
|
||||
await expect(tool?.execute?.("call", { url: "http://localhost/test" })).rejects.toThrow(
|
||||
@@ -71,16 +82,8 @@ describe("web_fetch SSRF protection", () => {
|
||||
});
|
||||
|
||||
it("blocks private IP literals without DNS", async () => {
|
||||
const fetchSpy = vi.fn();
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const { createWebFetchTool } = await import("./web-tools.js");
|
||||
const tool = createWebFetchTool({
|
||||
config: {
|
||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
||||
},
|
||||
});
|
||||
const fetchSpy = setMockFetch();
|
||||
const tool = await createWebFetchToolForTest();
|
||||
|
||||
await expect(tool?.execute?.("call", { url: "http://127.0.0.1/test" })).rejects.toThrow(
|
||||
/private|internal|blocked/i,
|
||||
@@ -100,16 +103,8 @@ describe("web_fetch SSRF protection", () => {
|
||||
return [{ address: "10.0.0.5", family: 4 }];
|
||||
});
|
||||
|
||||
const fetchSpy = vi.fn();
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const { createWebFetchTool } = await import("./web-tools.js");
|
||||
const tool = createWebFetchTool({
|
||||
config: {
|
||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
||||
},
|
||||
});
|
||||
const fetchSpy = setMockFetch();
|
||||
const tool = await createWebFetchToolForTest();
|
||||
|
||||
await expect(tool?.execute?.("call", { url: "https://private.test/resource" })).rejects.toThrow(
|
||||
/private|internal|blocked/i,
|
||||
@@ -120,19 +115,11 @@ describe("web_fetch SSRF protection", () => {
|
||||
it("blocks redirects to private hosts", async () => {
|
||||
lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
|
||||
|
||||
const fetchSpy = vi.fn().mockResolvedValueOnce(redirectResponse("http://127.0.0.1/secret"));
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const { createWebFetchTool } = await import("./web-tools.js");
|
||||
const tool = createWebFetchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
fetch: { cacheTtlMinutes: 0, firecrawl: { apiKey: "firecrawl-test" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
const fetchSpy = setMockFetch().mockResolvedValueOnce(
|
||||
redirectResponse("http://127.0.0.1/secret"),
|
||||
);
|
||||
const tool = await createWebFetchToolForTest({
|
||||
firecrawl: { apiKey: "firecrawl-test" },
|
||||
});
|
||||
|
||||
await expect(tool?.execute?.("call", { url: "https://example.com" })).rejects.toThrow(
|
||||
@@ -144,16 +131,8 @@ describe("web_fetch SSRF protection", () => {
|
||||
it("allows public hosts", async () => {
|
||||
lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
|
||||
|
||||
const fetchSpy = vi.fn().mockResolvedValue(textResponse("ok"));
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const { createWebFetchTool } = await import("./web-tools.js");
|
||||
const tool = createWebFetchTool({
|
||||
config: {
|
||||
tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } },
|
||||
},
|
||||
});
|
||||
setMockFetch().mockResolvedValue(textResponse("ok"));
|
||||
const tool = await createWebFetchToolForTest();
|
||||
|
||||
const result = await tool?.execute?.("call", { url: "https://example.com" });
|
||||
expect(result?.details).toMatchObject({
|
||||
|
||||
49
src/agents/tools/web-fetch.test-harness.ts
Normal file
49
src/agents/tools/web-fetch.test-harness.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
|
||||
export function installWebFetchSsrfHarness() {
|
||||
const lookupMock = vi.fn();
|
||||
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
||||
const priorFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
|
||||
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation((hostname) =>
|
||||
resolvePinnedHostname(hostname, lookupMock),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = priorFetch;
|
||||
lookupMock.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
}
|
||||
|
||||
export function createBaseWebFetchToolConfig(opts?: { maxResponseBytes?: number }): {
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
cacheTtlMinutes: number;
|
||||
firecrawl: { enabled: boolean };
|
||||
maxResponseBytes?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} {
|
||||
return {
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
cacheTtlMinutes: 0,
|
||||
firecrawl: { enabled: false },
|
||||
...(opts?.maxResponseBytes ? { maxResponseBytes: opts.maxResponseBytes } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
14
src/agents/tools/web-fetch.test-mocks.ts
Normal file
14
src/agents/tools/web-fetch.test-mocks.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Avoid dynamic-importing heavy readability deps in unit test suites.
|
||||
vi.mock("./web-fetch-utils.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./web-fetch-utils.js")>("./web-fetch-utils.js");
|
||||
return {
|
||||
...actual,
|
||||
extractReadableContent: vi.fn().mockResolvedValue({
|
||||
title: "HTML Page",
|
||||
text: "HTML Page\n\nContent here.",
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,34 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createWebFetchTool, createWebSearchTool } from "./web-tools.js";
|
||||
|
||||
function installMockFetch(payload: unknown) {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(payload),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
return mockFetch;
|
||||
}
|
||||
|
||||
function createPerplexitySearchTool(perplexityConfig?: { apiKey?: string; baseUrl?: string }) {
|
||||
return createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "perplexity",
|
||||
...(perplexityConfig ? { perplexity: perplexityConfig } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("web tools defaults", () => {
|
||||
it("enables web_fetch by default (non-sandbox)", () => {
|
||||
const tool = createWebFetchTool({ config: {}, sandboxed: false });
|
||||
@@ -35,15 +63,7 @@ describe("web_search country and language parameters", () => {
|
||||
});
|
||||
|
||||
it("should pass country parameter to Brave API", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ web: { results: [] } }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const mockFetch = installMockFetch({ web: { results: [] } });
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
expect(tool).not.toBeNull();
|
||||
|
||||
@@ -55,15 +75,7 @@ describe("web_search country and language parameters", () => {
|
||||
});
|
||||
|
||||
it("should pass search_lang parameter to Brave API", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ web: { results: [] } }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const mockFetch = installMockFetch({ web: { results: [] } });
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
await tool?.execute?.(1, { query: "test", search_lang: "de" });
|
||||
|
||||
@@ -72,15 +84,7 @@ describe("web_search country and language parameters", () => {
|
||||
});
|
||||
|
||||
it("should pass ui_lang parameter to Brave API", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ web: { results: [] } }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const mockFetch = installMockFetch({ web: { results: [] } });
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
await tool?.execute?.(1, { query: "test", ui_lang: "de" });
|
||||
|
||||
@@ -89,15 +93,7 @@ describe("web_search country and language parameters", () => {
|
||||
});
|
||||
|
||||
it("should pass freshness parameter to Brave API", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ web: { results: [] } }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const mockFetch = installMockFetch({ web: { results: [] } });
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
await tool?.execute?.(1, { query: "test", freshness: "pw" });
|
||||
|
||||
@@ -106,15 +102,7 @@ describe("web_search country and language parameters", () => {
|
||||
});
|
||||
|
||||
it("rejects invalid freshness values", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ web: { results: [] } }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const mockFetch = installMockFetch({ web: { results: [] } });
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
const result = await tool?.execute?.(1, { query: "test", freshness: "yesterday" });
|
||||
|
||||
@@ -134,19 +122,11 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
|
||||
it("defaults to Perplexity direct when PERPLEXITY_API_KEY is set", async () => {
|
||||
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: { tools: { web: { search: { provider: "perplexity" } } } },
|
||||
sandboxed: true,
|
||||
const mockFetch = installMockFetch({
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: [],
|
||||
});
|
||||
const tool = createPerplexitySearchTool();
|
||||
await tool?.execute?.(1, { query: "test-openrouter" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
@@ -161,19 +141,11 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
|
||||
it("passes freshness to Perplexity provider as search_recency_filter", async () => {
|
||||
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: { tools: { web: { search: { provider: "perplexity" } } } },
|
||||
sandboxed: true,
|
||||
const mockFetch = installMockFetch({
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: [],
|
||||
});
|
||||
const tool = createPerplexitySearchTool();
|
||||
await tool?.execute?.(1, { query: "perplexity-freshness-test", freshness: "pw" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
@@ -184,19 +156,11 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
it("defaults to OpenRouter when OPENROUTER_API_KEY is set", async () => {
|
||||
vi.stubEnv("PERPLEXITY_API_KEY", "");
|
||||
vi.stubEnv("OPENROUTER_API_KEY", "sk-or-test");
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: { tools: { web: { search: { provider: "perplexity" } } } },
|
||||
sandboxed: true,
|
||||
const mockFetch = installMockFetch({
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: [],
|
||||
});
|
||||
const tool = createPerplexitySearchTool();
|
||||
await tool?.execute?.(1, { query: "test-openrouter-env" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
@@ -212,19 +176,11 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
it("prefers PERPLEXITY_API_KEY when both env keys are set", async () => {
|
||||
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
|
||||
vi.stubEnv("OPENROUTER_API_KEY", "sk-or-test");
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: { tools: { web: { search: { provider: "perplexity" } } } },
|
||||
sandboxed: true,
|
||||
const mockFetch = installMockFetch({
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: [],
|
||||
});
|
||||
const tool = createPerplexitySearchTool();
|
||||
await tool?.execute?.(1, { query: "test-both-env" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
@@ -233,28 +189,11 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
|
||||
it("uses configured baseUrl even when PERPLEXITY_API_KEY is set", async () => {
|
||||
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "perplexity",
|
||||
perplexity: { baseUrl: "https://example.com/pplx" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
const mockFetch = installMockFetch({
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: [],
|
||||
});
|
||||
const tool = createPerplexitySearchTool({ baseUrl: "https://example.com/pplx" });
|
||||
await tool?.execute?.(1, { query: "test-config-baseurl" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
@@ -262,28 +201,11 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
});
|
||||
|
||||
it("defaults to Perplexity direct when apiKey looks like Perplexity", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "perplexity",
|
||||
perplexity: { apiKey: "pplx-config" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
const mockFetch = installMockFetch({
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: [],
|
||||
});
|
||||
const tool = createPerplexitySearchTool({ apiKey: "pplx-config" });
|
||||
await tool?.execute?.(1, { query: "test-config-apikey" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
@@ -291,28 +213,11 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
});
|
||||
|
||||
it("defaults to OpenRouter when apiKey looks like OpenRouter", async () => {
|
||||
const mockFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [{ message: { content: "ok" } }], citations: [] }),
|
||||
} as Response),
|
||||
);
|
||||
// @ts-expect-error mock fetch
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "perplexity",
|
||||
perplexity: { apiKey: "sk-or-v1-test" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
const mockFetch = installMockFetch({
|
||||
choices: [{ message: { content: "ok" } }],
|
||||
citations: [],
|
||||
});
|
||||
const tool = createPerplexitySearchTool({ apiKey: "sk-or-v1-test" });
|
||||
await tool?.execute?.(1, { query: "test-openrouter-config" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user