openclaw / src /flows /channel-setup.status.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw History Blame Contribute Delete
20.9 kB
// 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<ListChatChannels>(() => []));
const resolveChannelSetupEntries = vi.hoisted(() =>
vi.fn<ResolveChannelSetupEntries>(() => ({
entries: [],
installedCatalogEntries: [],
installableCatalogEntries: [],
installedCatalogById: new Map(),
installableCatalogById: new Map(),
})),
);
const formatChannelPrimerLine = vi.hoisted(() =>
vi.fn<FormatChannelPrimerLine>((meta) => `${meta.label}: ${meta.blurb}`),
);
const formatChannelSelectionLine = vi.hoisted(() =>
vi.fn<FormatChannelSelectionLine>((meta) => `${meta.label} — ${meta.blurb}`),
);
const isChannelConfigured = vi.hoisted(() => vi.fn<IsChannelConfigured>(() => false));
vi.mock("../channels/chat-meta.js", () => ({
listChatChannels: () => listChatChannels(),
}));
vi.mock("../channels/registry.js", () => ({
formatChannelPrimerLine: (meta: Parameters<FormatChannelPrimerLine>[0]) =>
formatChannelPrimerLine(meta),
formatChannelSelectionLine: (
meta: Parameters<FormatChannelSelectionLine>[0],
docsLink: Parameters<FormatChannelSelectionLine>[1],
) => formatChannelSelectionLine(meta, docsLink),
normalizeAnyChannelId: (channelId?: string) => channelId?.trim().toLowerCase() ?? null,
}));
vi.mock("../commands/channel-setup/discovery.js", () => ({
resolveChannelSetupEntries: (params: Parameters<ResolveChannelSetupEntries>[0]) =>
resolveChannelSetupEntries(params),
shouldShowChannelInSetup: (meta: { exposure?: { setup?: boolean } }) =>
meta.exposure?.setup !== false,
}));
vi.mock("../config/channel-configured.js", () => ({
isChannelConfigured: (
cfg: Parameters<IsChannelConfigured>[0],
channelId: Parameters<IsChannelConfigured>[1],
) => isChannelConfigured(cfg, channelId),
}));
// Avoid touching the real `extensions/<id>` 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 <channel> <code>",
'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 企业消息。"]);
});
});
});