// Channel setup status tests cover status text and docs link rendering. import { expectDefined } from "@openclaw/normalization-core/expect"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { withEnv, withEnvAsync } from "../test-utils/env.js"; import { makeCatalogEntry, makeChannelSetupEntries, makeMeta, } from "./channel-setup.test-helpers.js"; type ListChatChannels = typeof import("../channels/chat-meta.js").listChatChannels; type ResolveChannelSetupEntries = typeof import("../commands/channel-setup/discovery.js").resolveChannelSetupEntries; type FormatChannelPrimerLine = typeof import("../channels/registry.js").formatChannelPrimerLine; type FormatChannelSelectionLine = typeof import("../channels/registry.js").formatChannelSelectionLine; type IsChannelConfigured = typeof import("../config/channel-configured.js").isChannelConfigured; type ChannelSetupPlugin = import("../channels/plugins/setup-wizard-types.js").ChannelSetupPlugin; type NoteChannelPrimerChannels = Parameters< typeof import("./channel-setup.status.js").noteChannelPrimer >[1]; const listChatChannels = vi.hoisted(() => vi.fn(() => [])); const resolveChannelSetupEntries = vi.hoisted(() => vi.fn(() => ({ entries: [], installedCatalogEntries: [], installableCatalogEntries: [], installedCatalogById: new Map(), installableCatalogById: new Map(), })), ); const formatChannelPrimerLine = vi.hoisted(() => vi.fn((meta) => `${meta.label}: ${meta.blurb}`), ); const formatChannelSelectionLine = vi.hoisted(() => vi.fn((meta) => `${meta.label} — ${meta.blurb}`), ); const isChannelConfigured = vi.hoisted(() => vi.fn(() => false)); vi.mock("../channels/chat-meta.js", () => ({ listChatChannels: () => listChatChannels(), })); vi.mock("../channels/registry.js", () => ({ formatChannelPrimerLine: (meta: Parameters[0]) => formatChannelPrimerLine(meta), formatChannelSelectionLine: ( meta: Parameters[0], docsLink: Parameters[1], ) => formatChannelSelectionLine(meta, docsLink), normalizeAnyChannelId: (channelId?: string) => channelId?.trim().toLowerCase() ?? null, })); vi.mock("../commands/channel-setup/discovery.js", () => ({ resolveChannelSetupEntries: (params: Parameters[0]) => resolveChannelSetupEntries(params), shouldShowChannelInSetup: (meta: { exposure?: { setup?: boolean } }) => meta.exposure?.setup !== false, })); vi.mock("../config/channel-configured.js", () => ({ isChannelConfigured: ( cfg: Parameters[0], channelId: Parameters[1], ) => isChannelConfigured(cfg, channelId), })); // Avoid touching the real `extensions/` tree from unit tests. Status // rendering for installable catalog entries asks `bundled-sources` whether // a plugin already lives in-tree to decide between // "install plugin to enable" vs "bundled · enable to use". For these tests // we want the installable-catalog branch unconditionally, so we stub the // bundled lookup to "nothing is bundled". vi.mock("../plugins/bundled-sources.js", () => ({ resolveBundledPluginSources: () => new Map(), findBundledPluginSourceInMap: () => undefined, })); import { collectChannelStatus, noteChannelPrimer, noteChannelStatus, resolveChannelSelectionNoteLines, resolveChannelSetupSelectionContributions, } from "./channel-setup.status.js"; describe("resolveChannelSetupSelectionContributions", () => { beforeEach(() => { vi.clearAllMocks(); listChatChannels.mockReturnValue([ makeMeta("discord", "Discord"), makeMeta("imessage", "iMessage"), ]); resolveChannelSetupEntries.mockReturnValue(makeChannelSetupEntries()); formatChannelPrimerLine.mockImplementation( (meta: { label: string; blurb: string }) => `${meta.label}: ${meta.blurb}`, ); formatChannelSelectionLine.mockImplementation((meta) => `${meta.label} — ${meta.blurb}`); isChannelConfigured.mockReturnValue(false); }); it("uses the caller-selected workspace for explicit-fleet status and selection notes", async () => { const params = { cfg: { agents: { ownership: "explicit" as const, entries: { alpha: {}, beta: {} } } }, workspaceDir: "/tmp/beta-workspace", accountOverrides: {}, installedPlugins: [], selection: [], }; await collectChannelStatus(params); resolveChannelSelectionNoteLines(params); expect(resolveChannelSetupEntries).toHaveBeenCalledTimes(2); for (const [request] of resolveChannelSetupEntries.mock.calls) { expect(request.workspaceDir).toBe(params.workspaceDir); } }); it("uses the configured system agent workspace for explicit multi-agent setup", async () => { const cfg = { agents: { ownership: "explicit", defaults: { systemAgent: { agentId: "main" } }, entries: { main: { workspace: "/tmp/openclaw-main-workspace" }, helper: { workspace: "/tmp/openclaw-helper-workspace" }, third: { workspace: "/tmp/openclaw-third-workspace" }, }, }, } as const; await collectChannelStatus({ cfg, accountOverrides: {}, installedPlugins: [], }); resolveChannelSelectionNoteLines({ cfg, installedPlugins: [], selection: [] }); expect(resolveChannelSetupEntries).toHaveBeenNthCalledWith( 1, expect.objectContaining({ workspaceDir: "/tmp/openclaw-main-workspace" }), ); expect(resolveChannelSetupEntries).toHaveBeenNthCalledWith( 2, expect.objectContaining({ workspaceDir: "/tmp/openclaw-main-workspace" }), ); }); it("sorts channels alphabetically by picker label", () => { const contributions = resolveChannelSetupSelectionContributions({ entries: [ { id: "zalo", meta: { id: "zalo", label: "Zalo", selectionLabel: "Zalo (Bot API)", }, }, { id: "discord", meta: { id: "discord", label: "Discord", selectionLabel: "Discord (Bot API)", }, }, { id: "imessage", meta: { id: "imessage", label: "iMessage", selectionLabel: "iMessage (macOS app)", }, }, ], statusByChannel: new Map(), resolveDisabledHint: () => undefined, }); expect(contributions.map((contribution) => contribution.option.label)).toEqual([ "Discord (Bot API)", "iMessage (macOS app)", "Zalo (Bot API)", ]); }); it("does not invent hints before status has been collected", () => { const contributions = resolveChannelSetupSelectionContributions({ entries: [ { id: "zalo", meta: { id: "zalo", label: "Zalo", selectionLabel: "Zalo (Bot API)", }, }, ], statusByChannel: new Map(), resolveDisabledHint: () => undefined, }); expect(contributions.map((contribution) => contribution.option)).toEqual([ { value: "zalo", label: "Zalo (Bot API)", }, ]); }); it("combines real status and disabled hints when available", () => { const contributions = resolveChannelSetupSelectionContributions({ entries: [ { id: "zalo", meta: { id: "zalo", label: "Zalo", selectionLabel: "Zalo (Bot API)", }, }, ], statusByChannel: new Map([["zalo", { selectionHint: "configured" }]]), resolveDisabledHint: () => "disabled", }); expect(contributions[0]?.option).toEqual({ value: "zalo", label: "Zalo (Bot API)", hint: "configured · disabled", }); }); it("sanitizes picker labels and hints before terminal rendering", () => { const contributions = resolveChannelSetupSelectionContributions({ entries: [ { id: "zalo", meta: { id: "zalo", label: "Zalo\u001B[31m\nBot\u0007", }, }, ], statusByChannel: new Map([["zalo", { selectionHint: "configured\u001B[2K\nnow" }]]), resolveDisabledHint: () => "disabled\u0007", }); expect(contributions[0]?.option).toEqual({ value: "zalo", label: "Zalo\\nBot", hint: "configured\\nnow · disabled", }); }); it("sanitizes the picker fallback label when metadata sanitizes to empty", () => { const contributions = resolveChannelSetupSelectionContributions({ entries: [ { id: "bad\u001B[31m\nid", meta: { id: "bad\u001B[31m\nid", label: "\u001B[31m\u0007", }, }, ], statusByChannel: new Map(), resolveDisabledHint: () => undefined, }); expect(contributions[0]?.option).toEqual({ value: "bad\u001B[31m\nid", label: "bad\\nid", }); }); it("sanitizes channel labels in status note lines", async () => { listChatChannels.mockReturnValue([makeMeta("discord", "Discord\u001B[31m\nCore\u0007")]); resolveChannelSetupEntries.mockReturnValue( makeChannelSetupEntries({ installedCatalogEntries: [makeCatalogEntry("matrix", "Matrix\u001B[2K\nPlugin\u0007")], installableCatalogEntries: [makeCatalogEntry("zalo", "Zalo\u001B[2K\nPlugin\u0007")], }), ); const summary = await collectChannelStatus({ cfg: {} as never, accountOverrides: {}, installedPlugins: [], }); expect(summary.statusLines).toEqual([ "Discord\\nCore: not configured", "Matrix\\nPlugin: installed", "Zalo\\nPlugin: install plugin to enable", ]); }); it.each(["rejected status check", "synchronous status check", "adapter resolution"] as const)( "keeps healthy channels selectable after a %s failure", async (failurePoint) => { const installedPlugins = [ { id: "matrix", meta: makeMeta("matrix", "Matrix"), capabilities: { chatTypes: [] }, config: {} as ChannelSetupPlugin["config"], }, { id: "telegram", meta: makeMeta("telegram", "Telegram"), capabilities: { chatTypes: [] }, config: {} as ChannelSetupPlugin["config"], }, ] satisfies ChannelSetupPlugin[]; listChatChannels.mockReturnValue([ makeMeta("matrix", "Matrix"), makeMeta("telegram", "Telegram"), ]); isChannelConfigured.mockImplementation((_, channelId) => channelId === "matrix"); const failure = new Error("lazy Matrix setup module unavailable"); const summary = await collectChannelStatus({ cfg: {} as never, accountOverrides: {}, installedPlugins, resolveAdapter: (channel) => { if (channel === "matrix" && failurePoint === "adapter resolution") { throw failure; } return { channel, getStatus: channel === "matrix" ? failurePoint === "synchronous status check" ? () => { throw failure; } : async () => { throw failure; } : async () => ({ channel: "telegram", configured: true, statusLines: ["Telegram: configured"], selectionHint: "configured", quickstartScore: 5, }), } as never; }, }); expect(summary.statusByChannel.get("matrix")).toEqual({ channel: "matrix", configured: true, statusLines: ["Matrix: status unavailable (lazy Matrix setup module unavailable)"], selectionHint: "status unavailable", }); expect(summary.statusByChannel.get("telegram")).toEqual({ channel: "telegram", configured: true, statusLines: ["Telegram: configured"], selectionHint: "configured", quickstartScore: 5, }); expect(summary.statusLines).toEqual([ "Matrix: status unavailable (lazy Matrix setup module unavailable)", "Telegram: configured", ]); }, ); it("redacts credentials and terminal controls in failed channel status checks", async () => { const token = "sk-abcdefghijklmnopqrstuv"; const summary = await collectChannelStatus({ cfg: {} as never, accountOverrides: {}, installedPlugins: [ { id: "matrix", meta: makeMeta("matrix", "Matrix"), capabilities: { chatTypes: [] }, config: {} as ChannelSetupPlugin["config"], }, ], resolveAdapter: (channel) => ({ channel, getStatus: async () => { throw new Error(`\u001B[31mloader failed\nAuthorization: Bearer ${token}`); }, }) as never, }); const statusLine = summary.statusLines[0]; expect(statusLine).toContain( "Matrix: status unavailable (loader failed\\nAuthorization: Bearer", ); expect(statusLine).not.toContain(token); expect(statusLine).not.toContain("\u001B"); expect(statusLine).not.toContain("\n"); }); it("localizes channel status note labels", async () => { listChatChannels.mockReturnValue([ makeMeta("discord", "Discord"), makeMeta("telegram", "Telegram"), ]); isChannelConfigured.mockImplementation((_, channelId) => channelId === "discord"); resolveChannelSetupEntries.mockReturnValue( makeChannelSetupEntries({ installedCatalogEntries: [makeCatalogEntry("matrix", "Matrix")], installableCatalogEntries: [makeCatalogEntry("zalo", "Zalo")], }), ); await withEnvAsync({ OPENCLAW_LOCALE: "zh-CN" }, async () => { const summary = await collectChannelStatus({ cfg: {} as never, accountOverrides: {}, installedPlugins: [], }); expect(summary.statusLines).toEqual([ "Discord: 已配置(插件已禁用)", "Telegram: 未配置", "Matrix: 已安装", "Zalo: 安装插件后启用", ]); }); }); it("localizes channel status note title", async () => { const note = vi.fn(async () => {}); listChatChannels.mockReturnValue([makeMeta("discord", "Discord")]); isChannelConfigured.mockReturnValue(true); await withEnvAsync({ OPENCLAW_LOCALE: "zh-CN" }, async () => { await noteChannelStatus({ cfg: {} as never, prompter: { note } as never, installedPlugins: [], }); expect(note).toHaveBeenCalledWith(expect.any(String), "频道状态"); }); }); it("sanitizes channel metadata before primer notes", async () => { const note = vi.fn(async () => undefined); await noteChannelPrimer( { note } as never, [ { id: "bad\u001B[31m\nid", label: "\u001B[31m\u0007", blurb: "Blurb\u001B[2K\nline\u0007", } satisfies NoteChannelPrimerChannels[number], ] as NoteChannelPrimerChannels, ); expect(formatChannelPrimerLine).toHaveBeenCalledOnce(); const [primerMeta] = expectDefined( formatChannelPrimerLine.mock.calls.at(0), "primer line call", ); expect(primerMeta?.id).toBe("bad\\nid"); expect(primerMeta?.label).toBe("bad\\nid"); expect(primerMeta?.selectionLabel).toBe("bad\\nid"); expect(primerMeta?.blurb).toBe("Blurb\\nline"); expect(note).toHaveBeenCalledWith( [ "Inbound DM safety defaults to pairing: unknown senders get a pairing code first.", "Approve with: openclaw pairing approve ", 'Open/public DMs require dmPolicy="open" plus allowFrom=["*"].', 'For multi-user DMs, isolate sessions with: openclaw config set session.dmScope "per-channel-peer" (or "per-account-channel-peer" for multi-account channels).', "Docs: https://docs.openclaw.ai/channels/pairing", "", "bad\\nid: Blurb\\nline", ].join("\n"), "How channels work", ); }); it("localizes built-in channel primer copy", async () => { const note = vi.fn(async () => undefined); await withEnvAsync({ OPENCLAW_LOCALE: "zh-CN" }, async () => { await noteChannelPrimer( { note } as never, [ { id: "discord", label: "Discord", blurb: "very well supported right now.", } satisfies NoteChannelPrimerChannels[number], ] as NoteChannelPrimerChannels, ); }); expect(formatChannelPrimerLine).toHaveBeenCalledWith( expect.objectContaining({ label: "Discord", blurb: "目前支持很完善。", }), ); expect(note).toHaveBeenCalledWith( expect.stringContaining("入站 DM 安全默认使用配对"), "频道工作方式", ); }); it("sanitizes channel metadata before selection notes", () => { resolveChannelSetupEntries.mockReturnValue( makeChannelSetupEntries({ entries: [ { id: "zalo", meta: { id: "zalo", label: "Zalo\u001B[31m\nBot\u0007", selectionLabel: "Zalo", docsPath: "/channels/zalo", docsLabel: "Docs\u001B[2K\nLabel", blurb: "Setup\u001B[2K\nhelp\u0007", selectionDocsPrefix: "Docs\u001B[2K\nPrefix", selectionExtras: ["Extra\u001B[2K\nOne", "\u001B[31m\u0007"], }, }, ], }), ); const lines = resolveChannelSelectionNoteLines({ cfg: {} as never, installedPlugins: [], selection: ["zalo"], }); expect(formatChannelSelectionLine).toHaveBeenCalledOnce(); const [selectionMeta, docsLink] = expectDefined( formatChannelSelectionLine.mock.calls.at(0), "selection line call", ); expect(selectionMeta?.label).toBe("Zalo\\nBot"); expect(selectionMeta?.blurb).toBe("Setup\\nhelp"); expect(selectionMeta?.docsLabel).toBe("Docs\\nLabel"); expect(selectionMeta?.selectionDocsPrefix).toBe("Docs\\nPrefix"); expect(selectionMeta?.selectionExtras).toEqual(["Extra\\nOne"]); if (typeof docsLink !== "function") { throw new Error("Expected docs link formatter"); } expect(docsLink("/channels/zalo", "Docs")).toBe("https://docs.openclaw.ai/channels/zalo"); expect(lines).toEqual(["Zalo\\nBot — Setup\\nhelp"]); }); it.each([ ["empty", "", ""], ["whitespace", " \t ", "Docs:"], ["control-only", "\u001B[2K\u0007", "Docs:"], ] as const)("normalizes %s selection docs prefixes", (_label, prefix, expected) => { resolveChannelSetupEntries.mockReturnValue( makeChannelSetupEntries({ entries: [ { id: "custom-chat", meta: { id: "custom-chat", label: "Custom Chat", selectionLabel: "Custom Chat", docsPath: "/channels/custom-chat", blurb: "External channel.", selectionDocsPrefix: prefix, }, }, ], }), ); resolveChannelSelectionNoteLines({ cfg: {} as never, installedPlugins: [], selection: ["custom-chat"], }); const [selectionMeta] = expectDefined( formatChannelSelectionLine.mock.calls.at(0), "selection line call", ); expect(selectionMeta?.selectionDocsPrefix).toBe(expected); }); it("localizes built-in channel blurbs before selection notes", () => { resolveChannelSetupEntries.mockReturnValue( makeChannelSetupEntries({ entries: [ { id: "feishu", meta: { id: "feishu", label: "Feishu", selectionLabel: "Feishu", docsPath: "/channels/feishu", docsLabel: "feishu", blurb: "飞书/Lark enterprise messaging.", }, }, ], }), ); withEnv({ OPENCLAW_LOCALE: "zh-CN" }, () => { const lines = resolveChannelSelectionNoteLines({ cfg: {} as never, installedPlugins: [], selection: ["feishu"], }); expect(formatChannelSelectionLine).toHaveBeenCalledWith( expect.objectContaining({ label: "Feishu", blurb: "飞书/Lark 企业消息。", selectionDocsPrefix: "文档:", }), expect.any(Function), ); expect(lines).toEqual(["Feishu — 飞书/Lark 企业消息。"]); }); }); });