chore: Fix types in tests 5/N.

This commit is contained in:
cpojer
2026-02-17 10:51:25 +09:00
parent c49234cbfb
commit b6d4f7c00e
8 changed files with 155 additions and 143 deletions

View File

@@ -1,12 +1,12 @@
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import type { GatewayMessageChannel } from "../utils/message-channel.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
import type { AnyAgentTool } from "./tools/common.js";
import { resolvePluginTools } from "../plugins/tools.js"; import { resolvePluginTools } from "../plugins/tools.js";
import type { GatewayMessageChannel } from "../utils/message-channel.js";
import { resolveSessionAgentId } from "./agent-scope.js"; import { resolveSessionAgentId } from "./agent-scope.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
import { createAgentsListTool } from "./tools/agents-list-tool.js"; import { createAgentsListTool } from "./tools/agents-list-tool.js";
import { createBrowserTool } from "./tools/browser-tool.js"; import { createBrowserTool } from "./tools/browser-tool.js";
import { createCanvasTool } from "./tools/canvas-tool.js"; import { createCanvasTool } from "./tools/canvas-tool.js";
import type { AnyAgentTool } from "./tools/common.js";
import { createCronTool } from "./tools/cron-tool.js"; import { createCronTool } from "./tools/cron-tool.js";
import { createGatewayTool } from "./tools/gateway-tool.js"; import { createGatewayTool } from "./tools/gateway-tool.js";
import { createImageTool } from "./tools/image-tool.js"; import { createImageTool } from "./tools/image-tool.js";

View File

@@ -24,7 +24,7 @@ describe("sanitizeToolUseResultPairing", () => {
content: [{ type: "text", text: "ok" }], content: [{ type: "text", text: "ok" }],
isError: false, isError: false,
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolUseResultPairing(input); const out = sanitizeToolUseResultPairing(input);
expect(out[0]?.role).toBe("assistant"); expect(out[0]?.role).toBe("assistant");
@@ -56,7 +56,7 @@ describe("sanitizeToolUseResultPairing", () => {
isError: false, isError: false,
}, },
{ role: "user", content: "ok" }, { role: "user", content: "ok" },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolUseResultPairing(input); const out = sanitizeToolUseResultPairing(input);
expect(out.filter((m) => m.role === "toolResult")).toHaveLength(1); expect(out.filter((m) => m.role === "toolResult")).toHaveLength(1);
@@ -83,7 +83,7 @@ describe("sanitizeToolUseResultPairing", () => {
content: [{ type: "text", text: "second (duplicate)" }], content: [{ type: "text", text: "second (duplicate)" }],
isError: false, isError: false,
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolUseResultPairing(input); const out = sanitizeToolUseResultPairing(input);
const results = out.filter((m) => m.role === "toolResult") as Array<{ const results = out.filter((m) => m.role === "toolResult") as Array<{
@@ -107,7 +107,7 @@ describe("sanitizeToolUseResultPairing", () => {
role: "assistant", role: "assistant",
content: [{ type: "text", text: "ok" }], content: [{ type: "text", text: "ok" }],
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolUseResultPairing(input); const out = sanitizeToolUseResultPairing(input);
expect(out.some((m) => m.role === "toolResult")).toBe(false); expect(out.some((m) => m.role === "toolResult")).toBe(false);
@@ -125,7 +125,7 @@ describe("sanitizeToolUseResultPairing", () => {
stopReason: "error", stopReason: "error",
}, },
{ role: "user", content: "something went wrong" }, { role: "user", content: "something went wrong" },
] as AgentMessage[]; ] as unknown as AgentMessage[];
const result = repairToolUseResultPairing(input); const result = repairToolUseResultPairing(input);
@@ -147,7 +147,7 @@ describe("sanitizeToolUseResultPairing", () => {
stopReason: "aborted", stopReason: "aborted",
}, },
{ role: "user", content: "retrying after abort" }, { role: "user", content: "retrying after abort" },
] as AgentMessage[]; ] as unknown as AgentMessage[];
const result = repairToolUseResultPairing(input); const result = repairToolUseResultPairing(input);
@@ -168,7 +168,7 @@ describe("sanitizeToolUseResultPairing", () => {
stopReason: "toolUse", stopReason: "toolUse",
}, },
{ role: "user", content: "user message" }, { role: "user", content: "user message" },
] as AgentMessage[]; ] as unknown as AgentMessage[];
const result = repairToolUseResultPairing(input); const result = repairToolUseResultPairing(input);
@@ -195,7 +195,7 @@ describe("sanitizeToolUseResultPairing", () => {
isError: false, isError: false,
}, },
{ role: "user", content: "retrying" }, { role: "user", content: "retrying" },
] as AgentMessage[]; ] as unknown as AgentMessage[];
const result = repairToolUseResultPairing(input); const result = repairToolUseResultPairing(input);
@@ -211,20 +211,20 @@ describe("sanitizeToolUseResultPairing", () => {
describe("sanitizeToolCallInputs", () => { describe("sanitizeToolCallInputs", () => {
it("drops tool calls missing input or arguments", () => { it("drops tool calls missing input or arguments", () => {
const input: AgentMessage[] = [ const input = [
{ {
role: "assistant", role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read" }], content: [{ type: "toolCall", id: "call_1", name: "read" }],
}, },
{ role: "user", content: "hello" }, { role: "user", content: "hello" },
]; ] as unknown as AgentMessage[];
const out = sanitizeToolCallInputs(input); const out = sanitizeToolCallInputs(input);
expect(out.map((m) => m.role)).toEqual(["user"]); expect(out.map((m) => m.role)).toEqual(["user"]);
}); });
it("drops tool calls with missing or blank name/id", () => { it("drops tool calls with missing or blank name/id", () => {
const input: AgentMessage[] = [ const input = [
{ {
role: "assistant", role: "assistant",
content: [ content: [
@@ -234,7 +234,7 @@ describe("sanitizeToolCallInputs", () => {
{ type: "functionCall", id: "", name: "exec", arguments: {} }, { type: "functionCall", id: "", name: "exec", arguments: {} },
], ],
}, },
]; ] as unknown as AgentMessage[];
const out = sanitizeToolCallInputs(input); const out = sanitizeToolCallInputs(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>; const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
@@ -250,7 +250,7 @@ describe("sanitizeToolCallInputs", () => {
}); });
it("keeps valid tool calls and preserves text blocks", () => { it("keeps valid tool calls and preserves text blocks", () => {
const input: AgentMessage[] = [ const input = [
{ {
role: "assistant", role: "assistant",
content: [ content: [
@@ -259,7 +259,7 @@ describe("sanitizeToolCallInputs", () => {
{ type: "toolCall", id: "call_drop", name: "read" }, { type: "toolCall", id: "call_drop", name: "read" },
], ],
}, },
]; ] as unknown as AgentMessage[];
const out = sanitizeToolCallInputs(input); const out = sanitizeToolCallInputs(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>; const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;

View File

@@ -1,9 +1,17 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
const agentSpy = vi.fn(async () => ({ runId: "run-main", status: "ok" })); type AgentCallRequest = { method?: string; params?: Record<string, unknown> };
const sessionsDeleteSpy = vi.fn(); type RequesterResolution = {
const readLatestAssistantReplyMock = vi.fn(async () => "raw subagent reply"); requesterSessionKey: string;
requesterOrigin?: Record<string, unknown>;
} | null;
const agentSpy = vi.fn(async (_req: AgentCallRequest) => ({ runId: "run-main", status: "ok" }));
const sessionsDeleteSpy = vi.fn((_req: AgentCallRequest) => undefined);
const readLatestAssistantReplyMock = vi.fn(
async (_sessionKey?: string): Promise<string | undefined> => "raw subagent reply",
);
const embeddedRunMock = { const embeddedRunMock = {
isEmbeddedPiRunActive: vi.fn(() => false), isEmbeddedPiRunActive: vi.fn(() => false),
isEmbeddedPiRunStreaming: vi.fn(() => false), isEmbeddedPiRunStreaming: vi.fn(() => false),
@@ -12,8 +20,8 @@ const embeddedRunMock = {
}; };
const subagentRegistryMock = { const subagentRegistryMock = {
isSubagentSessionRunActive: vi.fn(() => true), isSubagentSessionRunActive: vi.fn(() => true),
countActiveDescendantRuns: vi.fn(() => 0), countActiveDescendantRuns: vi.fn((_sessionKey: string) => 0),
resolveRequesterForChildSession: vi.fn(() => null), resolveRequesterForChildSession: vi.fn((_sessionKey: string): RequesterResolution => null),
}; };
let sessionStore: Record<string, Record<string, unknown>> = {}; let sessionStore: Record<string, Record<string, unknown>> = {};
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {

View File

@@ -26,7 +26,7 @@ const buildDuplicateIdCollisionInput = () =>
toolName: "read", toolName: "read",
content: [{ type: "text", text: "two" }], content: [{ type: "text", text: "two" }],
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
function expectCollisionIdsRemainDistinct( function expectCollisionIdsRemainDistinct(
out: AgentMessage[], out: AgentMessage[],
@@ -62,7 +62,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
toolName: "read", toolName: "read",
content: [{ type: "text", text: "ok" }], content: [{ type: "text", text: "ok" }],
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input); const out = sanitizeToolCallIdsForCloudCodeAssist(input);
expect(out).toBe(input); expect(out).toBe(input);
@@ -80,7 +80,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
toolName: "read", toolName: "read",
content: [{ type: "text", text: "ok" }], content: [{ type: "text", text: "ok" }],
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input); const out = sanitizeToolCallIdsForCloudCodeAssist(input);
expect(out).not.toBe(input); expect(out).not.toBe(input);
@@ -126,7 +126,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
toolName: "read", toolName: "read",
content: [{ type: "text", text: "two" }], content: [{ type: "text", text: "two" }],
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input); const out = sanitizeToolCallIdsForCloudCodeAssist(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>; const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
@@ -168,7 +168,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
toolName: "login", toolName: "login",
content: [{ type: "text", text: "ok" }], content: [{ type: "text", text: "ok" }],
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict"); const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict");
expect(out).not.toBe(input); expect(out).not.toBe(input);
@@ -217,7 +217,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
toolName: "read", toolName: "read",
content: [{ type: "text", text: "two" }], content: [{ type: "text", text: "two" }],
}, },
] satisfies AgentMessage[]; ] as unknown as AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict9"); const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict9");
expect(out).not.toBe(input); expect(out).not.toBe(input);

View File

@@ -1,27 +1,31 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
const browserClientMocks = vi.hoisted(() => ({ const browserClientMocks = vi.hoisted(() => ({
browserCloseTab: vi.fn(async () => ({})), browserCloseTab: vi.fn(async (..._args: unknown[]) => ({})),
browserFocusTab: vi.fn(async () => ({})), browserFocusTab: vi.fn(async (..._args: unknown[]) => ({})),
browserOpenTab: vi.fn(async () => ({})), browserOpenTab: vi.fn(async (..._args: unknown[]) => ({})),
browserProfiles: vi.fn(async () => []), browserProfiles: vi.fn(
browserSnapshot: vi.fn(async () => ({ async (..._args: unknown[]): Promise<Array<Record<string, unknown>>> => [],
ok: true, ),
format: "ai", browserSnapshot: vi.fn(
targetId: "t1", async (..._args: unknown[]): Promise<Record<string, unknown>> => ({
url: "https://example.com", ok: true,
snapshot: "ok", format: "ai",
})), targetId: "t1",
browserStart: vi.fn(async () => ({})), url: "https://example.com",
browserStatus: vi.fn(async () => ({ snapshot: "ok",
}),
),
browserStart: vi.fn(async (..._args: unknown[]) => ({})),
browserStatus: vi.fn(async (..._args: unknown[]) => ({
ok: true, ok: true,
running: true, running: true,
pid: 1, pid: 1,
cdpPort: 18792, cdpPort: 18792,
cdpUrl: "http://127.0.0.1:18792", cdpUrl: "http://127.0.0.1:18792",
})), })),
browserStop: vi.fn(async () => ({})), browserStop: vi.fn(async (..._args: unknown[]) => ({})),
browserTabs: vi.fn(async () => []), browserTabs: vi.fn(async (..._args: unknown[]): Promise<Array<Record<string, unknown>>> => []),
})); }));
vi.mock("../../browser/client.js", () => browserClientMocks); vi.mock("../../browser/client.js", () => browserClientMocks);
@@ -55,7 +59,7 @@ const browserConfigMocks = vi.hoisted(() => ({
vi.mock("../../browser/config.js", () => browserConfigMocks); vi.mock("../../browser/config.js", () => browserConfigMocks);
const nodesUtilsMocks = vi.hoisted(() => ({ const nodesUtilsMocks = vi.hoisted(() => ({
listNodes: vi.fn(async () => []), listNodes: vi.fn(async (..._args: unknown[]): Promise<Array<Record<string, unknown>>> => []),
})); }));
vi.mock("./nodes-utils.js", async () => { vi.mock("./nodes-utils.js", async () => {
const actual = await vi.importActual<typeof import("./nodes-utils.js")>("./nodes-utils.js"); const actual = await vi.importActual<typeof import("./nodes-utils.js")>("./nodes-utils.js");
@@ -101,7 +105,7 @@ describe("browser tool snapshot maxChars", () => {
it("applies the default ai snapshot limit", async () => { it("applies the default ai snapshot limit", async () => {
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai" }); await tool.execute?.("call-1", { action: "snapshot", snapshotFormat: "ai" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined, undefined,
@@ -115,7 +119,7 @@ describe("browser tool snapshot maxChars", () => {
it("respects an explicit maxChars override", async () => { it("respects an explicit maxChars override", async () => {
const tool = createBrowserTool(); const tool = createBrowserTool();
const override = 2_000; const override = 2_000;
await tool.execute?.(null, { await tool.execute?.("call-1", {
action: "snapshot", action: "snapshot",
snapshotFormat: "ai", snapshotFormat: "ai",
maxChars: override, maxChars: override,
@@ -131,27 +135,29 @@ describe("browser tool snapshot maxChars", () => {
it("skips the default when maxChars is explicitly zero", async () => { it("skips the default when maxChars is explicitly zero", async () => {
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.(null, { await tool.execute?.("call-1", {
action: "snapshot", action: "snapshot",
snapshotFormat: "ai", snapshotFormat: "ai",
maxChars: 0, maxChars: 0,
}); });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalled(); expect(browserClientMocks.browserSnapshot).toHaveBeenCalled();
const [, opts] = browserClientMocks.browserSnapshot.mock.calls.at(-1) ?? []; const opts = browserClientMocks.browserSnapshot.mock.calls.at(-1)?.[1] as
| { maxChars?: number }
| undefined;
expect(Object.hasOwn(opts ?? {}, "maxChars")).toBe(false); expect(Object.hasOwn(opts ?? {}, "maxChars")).toBe(false);
}); });
it("lists profiles", async () => { it("lists profiles", async () => {
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.(null, { action: "profiles" }); await tool.execute?.("call-1", { action: "profiles" });
expect(browserClientMocks.browserProfiles).toHaveBeenCalledWith(undefined); expect(browserClientMocks.browserProfiles).toHaveBeenCalledWith(undefined);
}); });
it("passes refs mode through to browser snapshot", async () => { it("passes refs mode through to browser snapshot", async () => {
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai", refs: "aria" }); await tool.execute?.("call-1", { action: "snapshot", snapshotFormat: "ai", refs: "aria" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined, undefined,
@@ -167,7 +173,7 @@ describe("browser tool snapshot maxChars", () => {
browser: { snapshotDefaults: { mode: "efficient" } }, browser: { snapshotDefaults: { mode: "efficient" } },
}); });
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai" }); await tool.execute?.("call-1", { action: "snapshot", snapshotFormat: "ai" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined, undefined,
@@ -182,16 +188,18 @@ describe("browser tool snapshot maxChars", () => {
browser: { snapshotDefaults: { mode: "efficient" } }, browser: { snapshotDefaults: { mode: "efficient" } },
}); });
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.(null, { action: "snapshot", snapshotFormat: "aria" }); await tool.execute?.("call-1", { action: "snapshot", snapshotFormat: "aria" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalled(); expect(browserClientMocks.browserSnapshot).toHaveBeenCalled();
const [, opts] = browserClientMocks.browserSnapshot.mock.calls.at(-1) ?? []; const opts = browserClientMocks.browserSnapshot.mock.calls.at(-1)?.[1] as
| { mode?: string }
| undefined;
expect(opts?.mode).toBeUndefined(); expect(opts?.mode).toBeUndefined();
}); });
it("defaults to host when using profile=chrome (even in sandboxed sessions)", async () => { it("defaults to host when using profile=chrome (even in sandboxed sessions)", async () => {
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.(null, { action: "snapshot", profile: "chrome", snapshotFormat: "ai" }); await tool.execute?.("call-1", { action: "snapshot", profile: "chrome", snapshotFormat: "ai" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined, undefined,
@@ -212,7 +220,7 @@ describe("browser tool snapshot maxChars", () => {
}, },
]); ]);
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.(null, { action: "status", target: "node" }); await tool.execute?.("call-1", { action: "status", target: "node" });
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith( expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
"node.invoke", "node.invoke",
@@ -236,7 +244,7 @@ describe("browser tool snapshot maxChars", () => {
}, },
]); ]);
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.(null, { action: "status" }); await tool.execute?.("call-1", { action: "status" });
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith( expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
"http://127.0.0.1:9999", "http://127.0.0.1:9999",
@@ -256,7 +264,7 @@ describe("browser tool snapshot maxChars", () => {
}, },
]); ]);
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.(null, { action: "status", profile: "chrome" }); await tool.execute?.("call-1", { action: "status", profile: "chrome" });
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith( expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
undefined, undefined,
@@ -292,7 +300,7 @@ describe("browser tool snapshot labels", () => {
imagePath: "/tmp/snap.png", imagePath: "/tmp/snap.png",
}); });
const result = await tool.execute?.(null, { const result = await tool.execute?.("call-1", {
action: "snapshot", action: "snapshot",
snapshotFormat: "ai", snapshotFormat: "ai",
labels: true, labels: true,
@@ -335,7 +343,7 @@ describe("browser tool external content wrapping", () => {
}); });
const tool = createBrowserTool(); const tool = createBrowserTool();
const result = await tool.execute?.(null, { action: "snapshot", snapshotFormat: "aria" }); const result = await tool.execute?.("call-1", { action: "snapshot", snapshotFormat: "aria" });
expect(result?.content?.[0]).toMatchObject({ expect(result?.content?.[0]).toMatchObject({
type: "text", type: "text",
text: expect.stringContaining("<<<EXTERNAL_UNTRUSTED_CONTENT>>>"), text: expect.stringContaining("<<<EXTERNAL_UNTRUSTED_CONTENT>>>"),
@@ -369,7 +377,7 @@ describe("browser tool external content wrapping", () => {
]); ]);
const tool = createBrowserTool(); const tool = createBrowserTool();
const result = await tool.execute?.(null, { action: "tabs" }); const result = await tool.execute?.("call-1", { action: "tabs" });
expect(result?.content?.[0]).toMatchObject({ expect(result?.content?.[0]).toMatchObject({
type: "text", type: "text",
text: expect.stringContaining("<<<EXTERNAL_UNTRUSTED_CONTENT>>>"), text: expect.stringContaining("<<<EXTERNAL_UNTRUSTED_CONTENT>>>"),
@@ -402,7 +410,7 @@ describe("browser tool external content wrapping", () => {
}); });
const tool = createBrowserTool(); const tool = createBrowserTool();
const result = await tool.execute?.(null, { action: "console" }); const result = await tool.execute?.("call-1", { action: "console" });
expect(result?.content?.[0]).toMatchObject({ expect(result?.content?.[0]).toMatchObject({
type: "text", type: "text",
text: expect.stringContaining("<<<EXTERNAL_UNTRUSTED_CONTENT>>>"), text: expect.stringContaining("<<<EXTERNAL_UNTRUSTED_CONTENT>>>"),

View File

@@ -43,35 +43,35 @@ const kickMemberDiscord = vi.fn(async () => ({}));
const banMemberDiscord = vi.fn(async () => ({})); const banMemberDiscord = vi.fn(async () => ({}));
vi.mock("../../discord/send.js", () => ({ vi.mock("../../discord/send.js", () => ({
banMemberDiscord: (...args: unknown[]) => banMemberDiscord(...args), banMemberDiscord,
createChannelDiscord: (...args: unknown[]) => createChannelDiscord(...args), createChannelDiscord,
createThreadDiscord: (...args: unknown[]) => createThreadDiscord(...args), createThreadDiscord,
deleteChannelDiscord: (...args: unknown[]) => deleteChannelDiscord(...args), deleteChannelDiscord,
deleteMessageDiscord: (...args: unknown[]) => deleteMessageDiscord(...args), deleteMessageDiscord,
editChannelDiscord: (...args: unknown[]) => editChannelDiscord(...args), editChannelDiscord,
editMessageDiscord: (...args: unknown[]) => editMessageDiscord(...args), editMessageDiscord,
fetchMessageDiscord: (...args: unknown[]) => fetchMessageDiscord(...args), fetchMessageDiscord,
fetchChannelPermissionsDiscord: (...args: unknown[]) => fetchChannelPermissionsDiscord(...args), fetchChannelPermissionsDiscord,
fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args), fetchReactionsDiscord,
kickMemberDiscord: (...args: unknown[]) => kickMemberDiscord(...args), kickMemberDiscord,
listGuildChannelsDiscord: (...args: unknown[]) => listGuildChannelsDiscord(...args), listGuildChannelsDiscord,
listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args), listPinsDiscord,
listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args), listThreadsDiscord,
moveChannelDiscord: (...args: unknown[]) => moveChannelDiscord(...args), moveChannelDiscord,
pinMessageDiscord: (...args: unknown[]) => pinMessageDiscord(...args), pinMessageDiscord,
reactMessageDiscord: (...args: unknown[]) => reactMessageDiscord(...args), reactMessageDiscord,
readMessagesDiscord: (...args: unknown[]) => readMessagesDiscord(...args), readMessagesDiscord,
removeChannelPermissionDiscord: (...args: unknown[]) => removeChannelPermissionDiscord(...args), removeChannelPermissionDiscord,
removeOwnReactionsDiscord: (...args: unknown[]) => removeOwnReactionsDiscord(...args), removeOwnReactionsDiscord,
removeReactionDiscord: (...args: unknown[]) => removeReactionDiscord(...args), removeReactionDiscord,
searchMessagesDiscord: (...args: unknown[]) => searchMessagesDiscord(...args), searchMessagesDiscord,
sendMessageDiscord: (...args: unknown[]) => sendMessageDiscord(...args), sendMessageDiscord,
sendVoiceMessageDiscord: (...args: unknown[]) => sendVoiceMessageDiscord(...args), sendVoiceMessageDiscord,
sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args), sendPollDiscord,
sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args), sendStickerDiscord,
setChannelPermissionDiscord: (...args: unknown[]) => setChannelPermissionDiscord(...args), setChannelPermissionDiscord,
timeoutMemberDiscord: (...args: unknown[]) => timeoutMemberDiscord(...args), timeoutMemberDiscord,
unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args), unpinMessageDiscord,
})); }));
const enableAllActions = () => true; const enableAllActions = () => true;
@@ -165,7 +165,9 @@ describe("handleDiscordMessagingAction", () => {
}); });
it("adds normalized timestamps to readMessages payloads", async () => { it("adds normalized timestamps to readMessages payloads", async () => {
readMessagesDiscord.mockResolvedValueOnce([{ id: "1", timestamp: "2026-01-15T10:00:00.000Z" }]); readMessagesDiscord.mockResolvedValueOnce([
{ id: "1", timestamp: "2026-01-15T10:00:00.000Z" },
] as never);
const result = await handleDiscordMessagingAction( const result = await handleDiscordMessagingAction(
"readMessages", "readMessages",

View File

@@ -2,34 +2,34 @@ import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/config.js";
import { handleSlackAction } from "./slack-actions.js"; import { handleSlackAction } from "./slack-actions.js";
const deleteSlackMessage = vi.fn(async () => ({})); const deleteSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
const editSlackMessage = vi.fn(async () => ({})); const editSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
const getSlackMemberInfo = vi.fn(async () => ({})); const getSlackMemberInfo = vi.fn(async (..._args: unknown[]) => ({}));
const listSlackEmojis = vi.fn(async () => ({})); const listSlackEmojis = vi.fn(async (..._args: unknown[]) => ({}));
const listSlackPins = vi.fn(async () => ({})); const listSlackPins = vi.fn(async (..._args: unknown[]) => ({}));
const listSlackReactions = vi.fn(async () => ({})); const listSlackReactions = vi.fn(async (..._args: unknown[]) => ({}));
const pinSlackMessage = vi.fn(async () => ({})); const pinSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
const reactSlackMessage = vi.fn(async () => ({})); const reactSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
const readSlackMessages = vi.fn(async () => ({})); const readSlackMessages = vi.fn(async (..._args: unknown[]) => ({}));
const removeOwnSlackReactions = vi.fn(async () => ["thumbsup"]); const removeOwnSlackReactions = vi.fn(async (..._args: unknown[]) => ["thumbsup"]);
const removeSlackReaction = vi.fn(async () => ({})); const removeSlackReaction = vi.fn(async (..._args: unknown[]) => ({}));
const sendSlackMessage = vi.fn(async () => ({})); const sendSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
const unpinSlackMessage = vi.fn(async () => ({})); const unpinSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
vi.mock("../../slack/actions.js", () => ({ vi.mock("../../slack/actions.js", () => ({
deleteSlackMessage: (...args: unknown[]) => deleteSlackMessage(...args), deleteSlackMessage,
editSlackMessage: (...args: unknown[]) => editSlackMessage(...args), editSlackMessage,
getSlackMemberInfo: (...args: unknown[]) => getSlackMemberInfo(...args), getSlackMemberInfo,
listSlackEmojis: (...args: unknown[]) => listSlackEmojis(...args), listSlackEmojis,
listSlackPins: (...args: unknown[]) => listSlackPins(...args), listSlackPins,
listSlackReactions: (...args: unknown[]) => listSlackReactions(...args), listSlackReactions,
pinSlackMessage: (...args: unknown[]) => pinSlackMessage(...args), pinSlackMessage,
reactSlackMessage: (...args: unknown[]) => reactSlackMessage(...args), reactSlackMessage,
readSlackMessages: (...args: unknown[]) => readSlackMessages(...args), readSlackMessages,
removeOwnSlackReactions: (...args: unknown[]) => removeOwnSlackReactions(...args), removeOwnSlackReactions,
removeSlackReaction: (...args: unknown[]) => removeSlackReaction(...args), removeSlackReaction,
sendSlackMessage: (...args: unknown[]) => sendSlackMessage(...args), sendSlackMessage,
unpinSlackMessage: (...args: unknown[]) => unpinSlackMessage(...args), unpinSlackMessage,
})); }));
describe("handleSlackAction", () => { describe("handleSlackAction", () => {
@@ -521,7 +521,7 @@ describe("handleSlackAction", () => {
cfg, cfg,
); );
const [, opts] = readSlackMessages.mock.calls[0] ?? []; const opts = readSlackMessages.mock.calls[0]?.[1] as { threadId?: string } | undefined;
expect(opts?.threadId).toBe("12345.6789"); expect(opts?.threadId).toBe("12345.6789");
}); });
@@ -551,7 +551,7 @@ describe("handleSlackAction", () => {
readSlackMessages.mockClear(); readSlackMessages.mockClear();
readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false }); readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false });
await handleSlackAction({ action: "readMessages", channelId: "C1" }, cfg); await handleSlackAction({ action: "readMessages", channelId: "C1" }, cfg);
const [, opts] = readSlackMessages.mock.calls[0] ?? []; const opts = readSlackMessages.mock.calls[0]?.[1] as { token?: string } | undefined;
expect(opts?.token).toBe("xoxp-1"); expect(opts?.token).toBe("xoxp-1");
}); });
@@ -562,7 +562,7 @@ describe("handleSlackAction", () => {
readSlackMessages.mockClear(); readSlackMessages.mockClear();
readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false }); readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false });
await handleSlackAction({ action: "readMessages", channelId: "C1" }, cfg); await handleSlackAction({ action: "readMessages", channelId: "C1" }, cfg);
const [, opts] = readSlackMessages.mock.calls[0] ?? []; const opts = readSlackMessages.mock.calls[0]?.[1] as { token?: string } | undefined;
expect(opts?.token).toBeUndefined(); expect(opts?.token).toBeUndefined();
}); });
@@ -572,7 +572,7 @@ describe("handleSlackAction", () => {
} as OpenClawConfig; } as OpenClawConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
await handleSlackAction({ action: "sendMessage", to: "channel:C1", content: "Hello" }, cfg); await handleSlackAction({ action: "sendMessage", to: "channel:C1", content: "Hello" }, cfg);
const [, , opts] = sendSlackMessage.mock.calls[0] ?? []; const opts = sendSlackMessage.mock.calls[0]?.[2] as { token?: string } | undefined;
expect(opts?.token).toBeUndefined(); expect(opts?.token).toBeUndefined();
}); });
@@ -584,7 +584,7 @@ describe("handleSlackAction", () => {
} as OpenClawConfig; } as OpenClawConfig;
sendSlackMessage.mockClear(); sendSlackMessage.mockClear();
await handleSlackAction({ action: "sendMessage", to: "channel:C1", content: "Hello" }, cfg); await handleSlackAction({ action: "sendMessage", to: "channel:C1", content: "Hello" }, cfg);
const [, , opts] = sendSlackMessage.mock.calls[0] ?? []; const opts = sendSlackMessage.mock.calls[0]?.[2] as { token?: string } | undefined;
expect(opts?.token).toBe("xoxp-1"); expect(opts?.token).toBe("xoxp-1");
}); });

View File

@@ -2,13 +2,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createWebFetchTool, createWebSearchTool } from "./web-tools.js"; import { createWebFetchTool, createWebSearchTool } from "./web-tools.js";
function installMockFetch(payload: unknown) { function installMockFetch(payload: unknown) {
const mockFetch = vi.fn(() => const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => Promise.resolve(payload), json: () => Promise.resolve(payload),
} as Response), } as Response),
); );
// @ts-expect-error mock fetch
global.fetch = mockFetch; global.fetch = mockFetch;
return mockFetch; return mockFetch;
} }
@@ -55,7 +54,7 @@ async function executePerplexitySearch(
const mockFetch = installPerplexitySuccessFetch(); const mockFetch = installPerplexitySuccessFetch();
const tool = createPerplexitySearchTool(options?.perplexityConfig); const tool = createPerplexitySearchTool(options?.perplexityConfig);
await tool?.execute?.( await tool?.execute?.(
1, "call-1",
options?.freshness ? { query, freshness: options.freshness } : { query }, options?.freshness ? { query, freshness: options.freshness } : { query },
); );
return mockFetch; return mockFetch;
@@ -90,7 +89,6 @@ describe("web_search country and language parameters", () => {
afterEach(() => { afterEach(() => {
vi.unstubAllEnvs(); vi.unstubAllEnvs();
// @ts-expect-error global fetch cleanup
global.fetch = priorFetch; global.fetch = priorFetch;
}); });
@@ -105,7 +103,7 @@ describe("web_search country and language parameters", () => {
const mockFetch = installMockFetch({ web: { results: [] } }); const mockFetch = installMockFetch({ web: { results: [] } });
const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const tool = createWebSearchTool({ config: undefined, sandboxed: true });
expect(tool).not.toBeNull(); expect(tool).not.toBeNull();
await tool?.execute?.(1, { query: "test", ...params }); await tool?.execute?.("call-1", { query: "test", ...params });
expect(mockFetch).toHaveBeenCalled(); expect(mockFetch).toHaveBeenCalled();
return new URL(mockFetch.mock.calls[0][0] as string); return new URL(mockFetch.mock.calls[0][0] as string);
} }
@@ -123,7 +121,7 @@ describe("web_search country and language parameters", () => {
it("rejects invalid freshness values", async () => { it("rejects invalid freshness values", async () => {
const mockFetch = installMockFetch({ web: { results: [] } }); const mockFetch = installMockFetch({ web: { results: [] } });
const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const result = await tool?.execute?.(1, { query: "test", freshness: "yesterday" }); const result = await tool?.execute?.("call-1", { query: "test", freshness: "yesterday" });
expect(mockFetch).not.toHaveBeenCalled(); expect(mockFetch).not.toHaveBeenCalled();
expect(result?.details).toMatchObject({ error: "invalid_freshness" }); expect(result?.details).toMatchObject({ error: "invalid_freshness" });
@@ -135,7 +133,6 @@ describe("web_search perplexity baseUrl defaults", () => {
afterEach(() => { afterEach(() => {
vi.unstubAllEnvs(); vi.unstubAllEnvs();
// @ts-expect-error global fetch cleanup
global.fetch = priorFetch; global.fetch = priorFetch;
}); });
@@ -213,7 +210,6 @@ describe("web_search external content wrapping", () => {
afterEach(() => { afterEach(() => {
vi.unstubAllEnvs(); vi.unstubAllEnvs();
// @ts-expect-error global fetch cleanup
global.fetch = priorFetch; global.fetch = priorFetch;
}); });
@@ -236,11 +232,10 @@ describe("web_search external content wrapping", () => {
}), }),
} as Response), } as Response),
); );
// @ts-expect-error mock fetch
global.fetch = mockFetch; global.fetch = mockFetch;
const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const result = await tool?.execute?.(1, { query: "test" }); const result = await tool?.execute?.("call-1", { query: "test" });
const details = result?.details as { const details = result?.details as {
externalContent?: { untrusted?: boolean; source?: string; wrapped?: boolean }; externalContent?: { untrusted?: boolean; source?: string; wrapped?: boolean };
results?: Array<{ description?: string }>; results?: Array<{ description?: string }>;
@@ -275,11 +270,10 @@ describe("web_search external content wrapping", () => {
}), }),
} as Response), } as Response),
); );
// @ts-expect-error mock fetch
global.fetch = mockFetch; global.fetch = mockFetch;
const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const result = await tool?.execute?.(1, { query: "unique-test-url-not-wrapped" }); const result = await tool?.execute?.("call-1", { query: "unique-test-url-not-wrapped" });
const details = result?.details as { results?: Array<{ url?: string }> }; const details = result?.details as { results?: Array<{ url?: string }> };
// URL should NOT be wrapped - kept raw for tool chaining (e.g., web_fetch) // URL should NOT be wrapped - kept raw for tool chaining (e.g., web_fetch)
@@ -306,11 +300,10 @@ describe("web_search external content wrapping", () => {
}), }),
} as Response), } as Response),
); );
// @ts-expect-error mock fetch
global.fetch = mockFetch; global.fetch = mockFetch;
const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const result = await tool?.execute?.(1, { query: "unique-test-site-name-wrapping" }); const result = await tool?.execute?.("call-1", { query: "unique-test-site-name-wrapping" });
const details = result?.details as { results?: Array<{ siteName?: string }> }; const details = result?.details as { results?: Array<{ siteName?: string }> };
expect(details.results?.[0]?.siteName).toBe("example.com"); expect(details.results?.[0]?.siteName).toBe("example.com");
@@ -337,11 +330,12 @@ describe("web_search external content wrapping", () => {
}), }),
} as Response), } as Response),
); );
// @ts-expect-error mock fetch
global.fetch = mockFetch; global.fetch = mockFetch;
const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const result = await tool?.execute?.(1, { query: "unique-test-brave-published-wrapping" }); const result = await tool?.execute?.("call-1", {
query: "unique-test-brave-published-wrapping",
});
const details = result?.details as { results?: Array<{ published?: string }> }; const details = result?.details as { results?: Array<{ published?: string }> };
expect(details.results?.[0]?.published).toBe("2 days ago"); expect(details.results?.[0]?.published).toBe("2 days ago");
@@ -360,14 +354,13 @@ describe("web_search external content wrapping", () => {
}), }),
} as Response), } as Response),
); );
// @ts-expect-error mock fetch
global.fetch = mockFetch; global.fetch = mockFetch;
const tool = createWebSearchTool({ const tool = createWebSearchTool({
config: { tools: { web: { search: { provider: "perplexity" } } } }, config: { tools: { web: { search: { provider: "perplexity" } } } },
sandboxed: true, sandboxed: true,
}); });
const result = await tool?.execute?.(1, { query: "test" }); const result = await tool?.execute?.("call-1", { query: "test" });
const details = result?.details as { content?: string }; const details = result?.details as { content?: string };
expect(details.content).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>"); expect(details.content).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
@@ -377,7 +370,7 @@ describe("web_search external content wrapping", () => {
it("does not wrap Perplexity citations (raw for tool chaining)", async () => { it("does not wrap Perplexity citations (raw for tool chaining)", async () => {
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test"); vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
const citation = "https://example.com/some-article"; const citation = "https://example.com/some-article";
const mockFetch = vi.fn(() => const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => json: () =>
@@ -387,14 +380,15 @@ describe("web_search external content wrapping", () => {
}), }),
} as Response), } as Response),
); );
// @ts-expect-error mock fetch
global.fetch = mockFetch; global.fetch = mockFetch;
const tool = createWebSearchTool({ const tool = createWebSearchTool({
config: { tools: { web: { search: { provider: "perplexity" } } } }, config: { tools: { web: { search: { provider: "perplexity" } } } },
sandboxed: true, sandboxed: true,
}); });
const result = await tool?.execute?.(1, { query: "unique-test-perplexity-citations-raw" }); const result = await tool?.execute?.("call-1", {
query: "unique-test-perplexity-citations-raw",
});
const details = result?.details as { citations?: string[] }; const details = result?.details as { citations?: string[] };
// Citations are URLs - should NOT be wrapped for tool chaining // Citations are URLs - should NOT be wrapped for tool chaining