import { afterEach, describe, expect, it, vi } from "vitest"; import { createPluginSetupWizardAdapter, createTestWizardPrompter, promptSetupWizardAllowFrom, runSetupWizardConfigure, type WizardPrompter, } from "../../../test/helpers/extensions/setup-wizard.js"; import { expectStopPendingUntilAbort, startAccountAndTrackLifecycle, waitForStartedMocks, } from "../../../test/helpers/extensions/start-account-lifecycle.js"; import type { ResolvedIrcAccount } from "./accounts.js"; import { ircPlugin } from "./channel.js"; import { ircSetupAdapter, parsePort, setIrcAllowFrom, setIrcDmPolicy, setIrcGroupAccess, setIrcNickServ, updateIrcAccountConfig, } from "./setup-core.js"; import type { CoreConfig } from "./types.js"; const hoisted = vi.hoisted(() => ({ monitorIrcProvider: vi.fn(), })); vi.mock("./monitor.js", async () => { const actual = await vi.importActual("./monitor.js"); return { ...actual, monitorIrcProvider: hoisted.monitorIrcProvider, }; }); const ircConfigureAdapter = createPluginSetupWizardAdapter(ircPlugin); function buildAccount(): ResolvedIrcAccount { return { accountId: "default", enabled: true, name: "default", configured: true, host: "irc.example.com", port: 6697, tls: true, nick: "openclaw", username: "openclaw", realname: "OpenClaw", password: "", passwordSource: "none", config: {} as ResolvedIrcAccount["config"], }; } describe("irc setup", () => { afterEach(() => { vi.clearAllMocks(); }); it("parses valid ports and falls back for invalid values", () => { expect(parsePort("6697", 6667)).toBe(6697); expect(parsePort(" 7000 ", 6667)).toBe(7000); expect(parsePort("", 6667)).toBe(6667); expect(parsePort("70000", 6667)).toBe(6667); expect(parsePort("abc", 6667)).toBe(6667); }); it("updates top-level dm policy and allowlist", () => { const cfg: CoreConfig = { channels: { irc: {} } }; expect(setIrcDmPolicy(cfg, "open")).toMatchObject({ channels: { irc: { dmPolicy: "open", }, }, }); expect(setIrcAllowFrom(cfg, ["alice", "bob"])).toMatchObject({ channels: { irc: { allowFrom: ["alice", "bob"], }, }, }); }); it("stores nickserv and account config patches on the scoped account", () => { const cfg: CoreConfig = { channels: { irc: {} } }; expect( setIrcNickServ(cfg, "work", { enabled: true, service: "NickServ", }), ).toMatchObject({ channels: { irc: { accounts: { work: { nickserv: { enabled: true, service: "NickServ", }, }, }, }, }, }); expect( updateIrcAccountConfig(cfg, "work", { host: "irc.libera.chat", nick: "openclaw-work", }), ).toMatchObject({ channels: { irc: { accounts: { work: { host: "irc.libera.chat", nick: "openclaw-work", }, }, }, }, }); }); it("normalizes allowlist groups and handles non-allowlist policies", () => { const cfg: CoreConfig = { channels: { irc: {} } }; expect( setIrcGroupAccess( cfg, "default", "allowlist", ["openclaw", "#ops", "openclaw", "*"], (raw) => { const trimmed = raw.trim(); if (!trimmed) { return null; } if (trimmed === "*") { return "*"; } return trimmed.startsWith("#") ? trimmed : `#${trimmed}`; }, ), ).toMatchObject({ channels: { irc: { enabled: true, groupPolicy: "allowlist", groups: { "#openclaw": {}, "#ops": {}, "*": {}, }, }, }, }); expect(setIrcGroupAccess(cfg, "default", "disabled", [], () => null)).toMatchObject({ channels: { irc: { enabled: true, groupPolicy: "disabled", }, }, }); }); it("validates required input and applies normalized account config", () => { const validateInput = ircSetupAdapter.validateInput; const applyAccountConfig = ircSetupAdapter.applyAccountConfig; expect(validateInput).toBeTypeOf("function"); expect(applyAccountConfig).toBeTypeOf("function"); expect( validateInput!({ input: { host: "", nick: "openclaw" }, } as never), ).toBe("IRC requires host."); expect( validateInput!({ input: { host: "irc.libera.chat", nick: "" }, } as never), ).toBe("IRC requires nick."); expect( validateInput!({ input: { host: "irc.libera.chat", nick: "openclaw" }, } as never), ).toBeNull(); expect( applyAccountConfig!({ cfg: { channels: { irc: {} } }, accountId: "default", input: { name: "Default", host: " irc.libera.chat ", port: "7000", tls: true, nick: " openclaw ", username: " claw ", realname: " OpenClaw Bot ", password: " secret ", channels: ["#openclaw"], }, } as never), ).toEqual({ channels: { irc: { enabled: true, name: "Default", host: "irc.libera.chat", port: 7000, tls: true, nick: "openclaw", username: "claw", realname: "OpenClaw Bot", password: "secret", channels: ["#openclaw"], }, }, }); }); it("configures host and nick via setup prompts", async () => { const prompter = createTestWizardPrompter({ text: vi.fn(async ({ message }: { message: string }) => { if (message === "IRC server host") { return "irc.libera.chat"; } if (message === "IRC server port") { return "6697"; } if (message === "IRC nick") { return "openclaw-bot"; } if (message === "IRC username") { return "openclaw"; } if (message === "IRC real name") { return "OpenClaw Bot"; } if (message.startsWith("Auto-join IRC channels")) { return "#openclaw, #ops"; } if (message.startsWith("IRC channels allowlist")) { return "#openclaw, #ops"; } throw new Error(`Unexpected prompt: ${message}`); }) as WizardPrompter["text"], confirm: vi.fn(async ({ message }: { message: string }) => { if (message === "Use TLS for IRC?") { return true; } if (message === "Configure IRC channels access?") { return true; } return false; }), }); const result = await runSetupWizardConfigure({ configure: ircConfigureAdapter.configure, cfg: {} as CoreConfig, prompter, options: {}, }); expect(result.accountId).toBe("default"); expect(result.cfg.channels?.irc?.enabled).toBe(true); expect(result.cfg.channels?.irc?.host).toBe("irc.libera.chat"); expect(result.cfg.channels?.irc?.nick).toBe("openclaw-bot"); expect(result.cfg.channels?.irc?.tls).toBe(true); expect(result.cfg.channels?.irc?.channels).toEqual(["#openclaw", "#ops"]); expect(result.cfg.channels?.irc?.groupPolicy).toBe("allowlist"); expect(Object.keys(result.cfg.channels?.irc?.groups ?? {})).toEqual(["#openclaw", "#ops"]); }); it("writes DM allowFrom to top-level config for non-default account prompts", async () => { const prompter = createTestWizardPrompter({ text: vi.fn(async ({ message }: { message: string }) => { if (message === "IRC allowFrom (nick or nick!user@host)") { return "Alice, Bob!ident@example.org"; } throw new Error(`Unexpected prompt: ${message}`); }) as WizardPrompter["text"], confirm: vi.fn(async () => false), }); const promptAllowFrom = ircConfigureAdapter.dmPolicy?.promptAllowFrom; if (!promptAllowFrom) { throw new Error("promptAllowFrom unavailable"); } const cfg: CoreConfig = { channels: { irc: { accounts: { work: { host: "irc.libera.chat", nick: "openclaw-work", }, }, }, }, }; const updated = (await promptSetupWizardAllowFrom({ promptAllowFrom, cfg, prompter, accountId: "work", })) as CoreConfig; expect(updated.channels?.irc?.allowFrom).toEqual(["alice", "bob!ident@example.org"]); expect(updated.channels?.irc?.accounts?.work?.allowFrom).toBeUndefined(); }); it("keeps startAccount pending until abort, then stops the monitor", async () => { const stop = vi.fn(); hoisted.monitorIrcProvider.mockResolvedValue({ stop }); const { abort, task, isSettled } = startAccountAndTrackLifecycle({ startAccount: ircPlugin.gateway!.startAccount!, account: buildAccount(), }); await expectStopPendingUntilAbort({ waitForStarted: waitForStartedMocks(hoisted.monitorIrcProvider), isSettled, abort, task, stop, }); }); });