Files
openclaw/extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts
2026-03-24 15:48:35 +00:00

363 lines
10 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() => vi.fn());
const resolveConfiguredBindingRouteMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
ensureConfiguredBindingRouteReady: (...args: unknown[]) =>
ensureConfiguredBindingRouteReadyMock(...args),
resolveConfiguredBindingRoute: (...args: unknown[]) =>
resolveConfiguredBindingRouteMock(...args),
};
});
import { __testing as sessionBindingTesting } from "../../../../src/infra/outbound/session-binding-service.js";
import { preflightDiscordMessage } from "./message-handler.preflight.js";
import {
createDiscordMessage,
createDiscordPreflightArgs,
createGuildEvent,
createGuildTextClient,
DEFAULT_PREFLIGHT_CFG,
} from "./message-handler.preflight.test-helpers.js";
const GUILD_ID = "guild-1";
const CHANNEL_ID = "channel-1";
function createConfiguredDiscordBinding() {
return {
spec: {
channel: "discord",
accountId: "default",
conversationId: CHANNEL_ID,
agentId: "codex",
mode: "persistent",
},
record: {
bindingId: "config:acp:discord:default:channel-1",
targetSessionKey: "agent:codex:acp:binding:discord:default:abc123",
targetKind: "session",
conversation: {
channel: "discord",
accountId: "default",
conversationId: CHANNEL_ID,
},
status: "active",
boundAt: 0,
metadata: {
source: "config",
mode: "persistent",
agentId: "codex",
},
},
} as const;
}
function createConfiguredDiscordRoute() {
const configuredBinding = createConfiguredDiscordBinding();
return {
bindingResolution: {
conversation: {
channel: "discord",
accountId: "default",
conversationId: CHANNEL_ID,
},
compiledBinding: {
channel: "discord",
accountPattern: "default",
binding: {
type: "acp",
agentId: "codex",
match: {
channel: "discord",
accountId: "default",
peer: {
kind: "channel",
id: CHANNEL_ID,
},
},
},
bindingConversationId: CHANNEL_ID,
target: {
conversationId: CHANNEL_ID,
},
agentId: "codex",
provider: {
compileConfiguredBinding: () => ({ conversationId: CHANNEL_ID }),
matchInboundConversation: () => ({ conversationId: CHANNEL_ID }),
},
targetFactory: {
driverId: "acp",
materialize: () => ({
record: configuredBinding.record,
statefulTarget: {
kind: "stateful",
driverId: "acp",
sessionKey: configuredBinding.record.targetSessionKey,
agentId: configuredBinding.spec.agentId,
},
}),
},
},
match: {
conversationId: CHANNEL_ID,
},
record: configuredBinding.record,
statefulTarget: {
kind: "stateful",
driverId: "acp",
sessionKey: configuredBinding.record.targetSessionKey,
agentId: configuredBinding.spec.agentId,
},
},
configuredBinding,
boundSessionKey: configuredBinding.record.targetSessionKey,
route: {
agentId: "codex",
accountId: "default",
channel: "discord",
sessionKey: configuredBinding.record.targetSessionKey,
mainSessionKey: "agent:codex:main",
matchedBy: "binding.channel",
lastRoutePolicy: "bound",
},
} as const;
}
function createBasePreflightParams(overrides?: Record<string, unknown>) {
const message = createDiscordMessage({
id: "m-1",
channelId: CHANNEL_ID,
content: "<@bot-1> hello",
mentionedUsers: [{ id: "bot-1" }],
author: {
id: "user-1",
bot: false,
username: "alice",
},
});
return {
...createDiscordPreflightArgs({
cfg: DEFAULT_PREFLIGHT_CFG,
discordConfig: {
allowBots: true,
} as NonNullable<
import("../../../../src/config/config.js").OpenClawConfig["channels"]
>["discord"],
data: createGuildEvent({
channelId: CHANNEL_ID,
guildId: GUILD_ID,
author: message.author,
message,
}),
client: createGuildTextClient(CHANNEL_ID),
botUserId: "bot-1",
}),
discordConfig: {
allowBots: true,
} as NonNullable<
import("../../../../src/config/config.js").OpenClawConfig["channels"]
>["discord"],
...overrides,
} satisfies Parameters<typeof preflightDiscordMessage>[0];
}
function createAllowedGuildEntries(requireMention = false) {
return {
[GUILD_ID]: {
id: GUILD_ID,
channels: {
[CHANNEL_ID]: {
allow: true,
enabled: true,
requireMention,
},
},
},
};
}
function createHydratedGuildClient(restPayload: Record<string, unknown>) {
const restGet = vi.fn(async () => restPayload);
const client = {
...createGuildTextClient(CHANNEL_ID),
rest: {
get: restGet,
},
} as unknown as Parameters<typeof preflightDiscordMessage>[0]["client"];
return { client, restGet };
}
async function runRestHydrationPreflight(params: {
messageId: string;
restPayload: Record<string, unknown>;
}) {
const message = createDiscordMessage({
id: params.messageId,
channelId: CHANNEL_ID,
content: "",
author: {
id: "user-1",
bot: false,
username: "alice",
},
});
const { client, restGet } = createHydratedGuildClient(params.restPayload);
const result = await preflightDiscordMessage(
createBasePreflightParams({
client,
data: createGuildEvent({
channelId: CHANNEL_ID,
guildId: GUILD_ID,
author: message.author,
message,
}),
guildEntries: createAllowedGuildEntries(false),
}),
);
return { result, restGet };
}
describe("preflightDiscordMessage configured ACP bindings", () => {
beforeEach(() => {
sessionBindingTesting.resetSessionBindingAdaptersForTests();
ensureConfiguredBindingRouteReadyMock.mockReset();
resolveConfiguredBindingRouteMock.mockReset();
resolveConfiguredBindingRouteMock.mockReturnValue(createConfiguredDiscordRoute());
ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: true });
});
it("does not initialize configured ACP bindings for rejected messages", async () => {
const result = await preflightDiscordMessage(
createBasePreflightParams({
guildEntries: {
[GUILD_ID]: {
id: GUILD_ID,
channels: {
[CHANNEL_ID]: {
allow: true,
enabled: false,
},
},
},
},
}),
);
expect(result).toBeNull();
expect(resolveConfiguredBindingRouteMock).toHaveBeenCalledTimes(1);
expect(ensureConfiguredBindingRouteReadyMock).not.toHaveBeenCalled();
});
it("initializes configured ACP bindings only after preflight accepts the message", async () => {
const result = await preflightDiscordMessage(
createBasePreflightParams({
guildEntries: {
[GUILD_ID]: {
id: GUILD_ID,
channels: {
[CHANNEL_ID]: {
allow: true,
enabled: true,
requireMention: false,
},
},
},
},
}),
);
expect(result).not.toBeNull();
expect(resolveConfiguredBindingRouteMock).toHaveBeenCalledTimes(1);
expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);
expect(result?.boundSessionKey).toBe("agent:codex:acp:binding:discord:default:abc123");
});
it("accepts plain messages in configured ACP-bound channels without a mention", async () => {
const message = createDiscordMessage({
id: "m-no-mention",
channelId: CHANNEL_ID,
content: "hello",
mentionedUsers: [],
author: {
id: "user-1",
bot: false,
username: "alice",
},
});
const result = await preflightDiscordMessage(
createBasePreflightParams({
data: createGuildEvent({
channelId: CHANNEL_ID,
guildId: GUILD_ID,
author: message.author,
message,
}),
guildEntries: createAllowedGuildEntries(true),
}),
);
expect(result).not.toBeNull();
expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);
expect(result?.boundSessionKey).toBe("agent:codex:acp:binding:discord:default:abc123");
});
it("hydrates empty guild message payloads from REST before ensuring configured ACP bindings", async () => {
const { result, restGet } = await runRestHydrationPreflight({
messageId: "m-rest",
restPayload: {
id: "m-rest",
content: "hello from rest",
attachments: [],
embeds: [],
mentions: [],
mention_roles: [],
mention_everyone: false,
author: {
id: "user-1",
username: "alice",
},
},
});
expect(restGet).toHaveBeenCalledTimes(1);
expect(result?.messageText).toBe("hello from rest");
expect(result?.data.message.content).toBe("hello from rest");
expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);
});
it("hydrates sticker-only guild message payloads from REST before ensuring configured ACP bindings", async () => {
const { result, restGet } = await runRestHydrationPreflight({
messageId: "m-rest-sticker",
restPayload: {
id: "m-rest-sticker",
content: "",
attachments: [],
embeds: [],
mentions: [],
mention_roles: [],
mention_everyone: false,
sticker_items: [
{
id: "sticker-1",
name: "wave",
},
],
author: {
id: "user-1",
username: "alice",
},
},
});
expect(restGet).toHaveBeenCalledTimes(1);
expect(result?.messageText).toBe("<media:sticker> (1 sticker)");
expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);
});
});