Files
openclaw/src/agents/tools/sessions.e2e.test.ts
2026-02-21 21:44:57 +00:00

224 lines
6.8 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
import { extractAssistantText, sanitizeTextContent } from "./sessions-helpers.js";
const callGatewayMock = vi.fn();
vi.mock("../../gateway/call.js", () => ({
callGateway: (opts: unknown) => callGatewayMock(opts),
}));
vi.mock("../../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../config/config.js")>();
return {
...actual,
loadConfig: () =>
({
session: { scope: "per-sender", mainKey: "main" },
tools: { agentToAgent: { enabled: false } },
}) as never,
};
});
import { createSessionsListTool } from "./sessions-list-tool.js";
import { createSessionsSendTool } from "./sessions-send-tool.js";
let resolveAnnounceTarget: (typeof import("./sessions-announce-target.js"))["resolveAnnounceTarget"];
let setActivePluginRegistry: (typeof import("../../plugins/runtime.js"))["setActivePluginRegistry"];
const installRegistry = async () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "discord",
source: "test",
plugin: {
id: "discord",
meta: {
id: "discord",
label: "Discord",
selectionLabel: "Discord",
docsPath: "/channels/discord",
blurb: "Discord test stub.",
},
capabilities: { chatTypes: ["direct", "channel", "thread"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
},
},
},
{
pluginId: "whatsapp",
source: "test",
plugin: {
id: "whatsapp",
meta: {
id: "whatsapp",
label: "WhatsApp",
selectionLabel: "WhatsApp",
docsPath: "/channels/whatsapp",
blurb: "WhatsApp test stub.",
preferSessionLookupForAnnounceTarget: true,
},
capabilities: { chatTypes: ["direct", "group"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
},
},
},
]),
);
};
describe("sanitizeTextContent", () => {
it("strips minimax tool call XML and downgraded markers", () => {
const input =
'Hello <invoke name="tool">payload</invoke></minimax:tool_call> ' +
"[Tool Call: foo (ID: 1)] world";
const result = sanitizeTextContent(input).trim();
expect(result).toBe("Hello world");
expect(result).not.toContain("invoke");
expect(result).not.toContain("Tool Call");
});
it("strips thinking tags", () => {
const input = "Before <think>secret</think> after";
const result = sanitizeTextContent(input).trim();
expect(result).toBe("Before after");
});
});
beforeAll(async () => {
({ resolveAnnounceTarget } = await import("./sessions-announce-target.js"));
({ setActivePluginRegistry } = await import("../../plugins/runtime.js"));
});
describe("extractAssistantText", () => {
it("sanitizes blocks without injecting newlines", () => {
const message = {
role: "assistant",
content: [
{ type: "text", text: "Hi " },
{ type: "text", text: "<think>secret</think>there" },
],
};
expect(extractAssistantText(message)).toBe("Hi there");
});
it("rewrites error-ish assistant text only when the transcript marks it as an error", () => {
const message = {
role: "assistant",
stopReason: "error",
errorMessage: "500 Internal Server Error",
content: [{ type: "text", text: "500 Internal Server Error" }],
};
expect(extractAssistantText(message)).toBe("HTTP 500: Internal Server Error");
});
it("keeps normal status text that mentions billing", () => {
const message = {
role: "assistant",
content: [
{
type: "text",
text: "Firebase downgraded us to the free Spark plan. Check whether billing should be re-enabled.",
},
],
};
expect(extractAssistantText(message)).toBe(
"Firebase downgraded us to the free Spark plan. Check whether billing should be re-enabled.",
);
});
});
describe("resolveAnnounceTarget", () => {
beforeEach(async () => {
callGatewayMock.mockReset();
await installRegistry();
});
it("derives non-WhatsApp announce targets from the session key", async () => {
const target = await resolveAnnounceTarget({
sessionKey: "agent:main:discord:group:dev",
displayKey: "agent:main:discord:group:dev",
});
expect(target).toEqual({ channel: "discord", to: "channel:dev" });
expect(callGatewayMock).not.toHaveBeenCalled();
});
it("hydrates WhatsApp accountId from sessions.list when available", async () => {
callGatewayMock.mockResolvedValueOnce({
sessions: [
{
key: "agent:main:whatsapp:group:123@g.us",
deliveryContext: {
channel: "whatsapp",
to: "123@g.us",
accountId: "work",
},
},
],
});
const target = await resolveAnnounceTarget({
sessionKey: "agent:main:whatsapp:group:123@g.us",
displayKey: "agent:main:whatsapp:group:123@g.us",
});
expect(target).toEqual({
channel: "whatsapp",
to: "123@g.us",
accountId: "work",
});
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const first = callGatewayMock.mock.calls[0]?.[0] as { method?: string } | undefined;
expect(first).toBeDefined();
expect(first?.method).toBe("sessions.list");
});
});
describe("sessions_list gating", () => {
beforeEach(() => {
callGatewayMock.mockReset();
callGatewayMock.mockResolvedValue({
path: "/tmp/sessions.json",
sessions: [
{ key: "agent:main:main", kind: "direct" },
{ key: "agent:other:main", kind: "direct" },
],
});
});
it("filters out other agents when tools.agentToAgent.enabled is false", async () => {
const tool = createSessionsListTool({ agentSessionKey: "agent:main:main" });
const result = await tool.execute("call1", {});
expect(result.details).toMatchObject({
count: 1,
sessions: [{ key: "agent:main:main" }],
});
});
});
describe("sessions_send gating", () => {
beforeEach(() => {
callGatewayMock.mockReset();
});
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
const tool = createSessionsSendTool({
agentSessionKey: "agent:main:main",
agentChannel: "whatsapp",
});
const result = await tool.execute("call1", {
sessionKey: "agent:other:main",
message: "hi",
timeoutSeconds: 0,
});
expect(callGatewayMock).toHaveBeenCalledTimes(1);
expect(callGatewayMock.mock.calls[0]?.[0]).toMatchObject({ method: "sessions.list" });
expect(result.details).toMatchObject({ status: "forbidden" });
});
});