import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ResolvedIrcAccount } from "./accounts.js"; import { handleIrcInbound } from "./inbound.js"; import type { RuntimeEnv } from "./runtime-api.js"; import { setIrcRuntime } from "./runtime.js"; import type { CoreConfig, IrcInboundMessage } from "./types.js"; const { createChannelPairingControllerMock, deliverFormattedTextWithAttachmentsMock, dispatchInboundReplyWithBaseMock, isDangerousNameMatchingEnabledMock, logInboundDropMock, readStoreAllowFromForDmPolicyMock, resolveAllowlistProviderRuntimeGroupPolicyMock, resolveControlCommandGateMock, resolveDefaultGroupPolicyMock, resolveEffectiveAllowFromListsMock, warnMissingProviderGroupPolicyFallbackOnceMock, } = vi.hoisted(() => { return { createChannelPairingControllerMock: vi.fn(), deliverFormattedTextWithAttachmentsMock: vi.fn(), dispatchInboundReplyWithBaseMock: vi.fn(), isDangerousNameMatchingEnabledMock: vi.fn(), logInboundDropMock: vi.fn(), readStoreAllowFromForDmPolicyMock: vi.fn(), resolveAllowlistProviderRuntimeGroupPolicyMock: vi.fn(), resolveControlCommandGateMock: vi.fn(), resolveDefaultGroupPolicyMock: vi.fn(), resolveEffectiveAllowFromListsMock: vi.fn(), warnMissingProviderGroupPolicyFallbackOnceMock: vi.fn(), }; }); const sendMessageIrcMock = vi.hoisted(() => vi.fn()); vi.mock("./runtime-api.js", async () => { const actual = await vi.importActual("./runtime-api.js"); return { ...actual, createChannelPairingController: createChannelPairingControllerMock, deliverFormattedTextWithAttachments: deliverFormattedTextWithAttachmentsMock, dispatchInboundReplyWithBase: dispatchInboundReplyWithBaseMock, isDangerousNameMatchingEnabled: isDangerousNameMatchingEnabledMock, logInboundDrop: logInboundDropMock, readStoreAllowFromForDmPolicy: readStoreAllowFromForDmPolicyMock, resolveAllowlistProviderRuntimeGroupPolicy: resolveAllowlistProviderRuntimeGroupPolicyMock, resolveControlCommandGate: resolveControlCommandGateMock, resolveDefaultGroupPolicy: resolveDefaultGroupPolicyMock, resolveEffectiveAllowFromLists: resolveEffectiveAllowFromListsMock, warnMissingProviderGroupPolicyFallbackOnce: warnMissingProviderGroupPolicyFallbackOnceMock, }; }); vi.mock("./send.js", () => ({ sendMessageIrc: sendMessageIrcMock, })); function installIrcRuntime() { setIrcRuntime({ channel: { commands: { shouldHandleTextCommands: vi.fn(() => false), }, text: { hasControlCommand: vi.fn(() => false), }, mentions: { buildMentionRegexes: vi.fn(() => []), matchesMentionPatterns: vi.fn(() => false), }, }, } as never); } function createRuntimeEnv() { return { log: vi.fn(), error: vi.fn(), } as unknown as RuntimeEnv; } function createAccount(overrides?: Partial): ResolvedIrcAccount { return { accountId: "default", enabled: true, server: "irc.example.com", nick: "OpenClaw", config: { dmPolicy: "pairing", allowFrom: [], groupPolicy: "allowlist", groupAllowFrom: [], }, ...overrides, } as ResolvedIrcAccount; } function createMessage(overrides?: Partial): IrcInboundMessage { return { messageId: "msg-1", target: "alice", senderNick: "alice", senderUser: "ident", senderHost: "example.com", text: "hello", timestamp: Date.now(), isGroup: false, ...overrides, }; } describe("irc inbound behavior", () => { beforeEach(() => { vi.clearAllMocks(); installIrcRuntime(); resolveDefaultGroupPolicyMock.mockReturnValue("allowlist"); resolveAllowlistProviderRuntimeGroupPolicyMock.mockReturnValue({ groupPolicy: "allowlist", providerMissingFallbackApplied: false, }); warnMissingProviderGroupPolicyFallbackOnceMock.mockReturnValue(undefined); readStoreAllowFromForDmPolicyMock.mockResolvedValue([]); isDangerousNameMatchingEnabledMock.mockReturnValue(false); resolveEffectiveAllowFromListsMock.mockReturnValue({ effectiveAllowFrom: [], effectiveGroupAllowFrom: [], }); deliverFormattedTextWithAttachmentsMock.mockImplementation(async ({ payload, send }) => { await send({ text: payload.text, replyToId: undefined }); return true; }); }); it("issues a DM pairing challenge and sends the reply to the sender nick", async () => { const issueChallenge = vi.fn(async ({ sendPairingReply }) => { await sendPairingReply("pair me"); }); createChannelPairingControllerMock.mockReturnValue({ readStoreForDmPolicy: vi.fn(), issueChallenge, }); resolveControlCommandGateMock.mockReturnValue({ commandAuthorized: false, shouldBlock: false, }); const sendReply = vi.fn(async () => {}); await handleIrcInbound({ message: createMessage(), account: createAccount(), config: { channels: { irc: {} } } as CoreConfig, runtime: createRuntimeEnv(), sendReply, }); expect(issueChallenge).toHaveBeenCalledTimes(1); expect(sendReply).toHaveBeenCalledWith("alice", "pair me", undefined); expect(dispatchInboundReplyWithBaseMock).not.toHaveBeenCalled(); }); it("drops unauthorized group control commands before dispatch", async () => { createChannelPairingControllerMock.mockReturnValue({ readStoreForDmPolicy: vi.fn(), issueChallenge: vi.fn(), }); resolveEffectiveAllowFromListsMock.mockReturnValue({ effectiveAllowFrom: [], effectiveGroupAllowFrom: ["alice!ident@example.com"], }); resolveControlCommandGateMock.mockReturnValue({ commandAuthorized: false, shouldBlock: true, }); const runtime = createRuntimeEnv(); await handleIrcInbound({ message: createMessage({ target: "#ops", isGroup: true, text: "/admin", }), account: createAccount({ config: { dmPolicy: "pairing", allowFrom: [], groupPolicy: "allowlist", groupAllowFrom: ["alice!ident@example.com"], groups: { "#ops": {}, }, }, }), config: { channels: { irc: {} }, commands: { useAccessGroups: true } } as CoreConfig, runtime, }); expect(logInboundDropMock).toHaveBeenCalledWith( expect.objectContaining({ channel: "irc", reason: "control command (unauthorized)", target: "alice!ident@example.com", }), ); expect(dispatchInboundReplyWithBaseMock).not.toHaveBeenCalled(); }); });