CLI: sanitize channel setup notes

This commit is contained in:
Gustavo Madeira Santana
2026-04-17 12:04:44 -04:00
parent 1f1e854807
commit 1ca444081e
2 changed files with 255 additions and 27 deletions

View File

@@ -1,31 +1,78 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const listChatChannels = vi.hoisted(() => vi.fn(() => [{ id: "discord" }, { id: "bluebubbles" }]));
vi.mock("../channels/chat-meta.js", () => ({
listChatChannels: () => listChatChannels(),
}));
vi.mock("../channels/registry.js", () => ({
formatChannelPrimerLine: vi.fn(() => ""),
formatChannelSelectionLine: vi.fn(() => ""),
}));
vi.mock("../commands/channel-setup/discovery.js", () => ({
resolveChannelSetupEntries: vi.fn(() => ({
const listChatChannels = vi.hoisted(() =>
vi.fn(() => [
{ id: "discord", label: "Discord" },
{ id: "bluebubbles", label: "BlueBubbles" },
]),
);
const resolveChannelSetupEntries = vi.hoisted(() =>
vi.fn(() => ({
entries: [],
installedCatalogEntries: [],
installableCatalogEntries: [],
installedCatalogById: new Map(),
installableCatalogById: new Map(),
})),
);
const formatChannelPrimerLine = vi.hoisted(() =>
vi.fn((meta: { label: string; blurb: string }) => `${meta.label}: ${meta.blurb}`),
);
const formatChannelSelectionLine = vi.hoisted(() =>
vi.fn((meta: { label: string; blurb: string }) => `${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: unknown) => formatChannelPrimerLine(meta),
formatChannelSelectionLine: (meta: unknown, docsLink: unknown) =>
formatChannelSelectionLine(meta, docsLink),
}));
vi.mock("../commands/channel-setup/discovery.js", () => ({
resolveChannelSetupEntries: (params: unknown) => resolveChannelSetupEntries(params),
shouldShowChannelInSetup: (meta: { exposure?: { setup?: boolean }; showInSetup?: boolean }) =>
meta.showInSetup !== false && meta.exposure?.setup !== false,
}));
import { resolveChannelSetupSelectionContributions } from "./channel-setup.status.js";
vi.mock("../config/channel-configured.js", () => ({
isChannelConfigured: (cfg: unknown, channelId: string) => isChannelConfigured(cfg, channelId),
}));
import {
collectChannelStatus,
noteChannelPrimer,
resolveChannelSelectionNoteLines,
resolveChannelSetupSelectionContributions,
} from "./channel-setup.status.js";
describe("resolveChannelSetupSelectionContributions", () => {
beforeEach(() => {
vi.clearAllMocks();
listChatChannels.mockReturnValue([
{ id: "discord", label: "Discord" },
{ id: "bluebubbles", label: "BlueBubbles" },
]);
resolveChannelSetupEntries.mockReturnValue({
entries: [],
installedCatalogEntries: [],
installableCatalogEntries: [],
installedCatalogById: new Map(),
installableCatalogById: new Map(),
});
formatChannelPrimerLine.mockImplementation(
(meta: { label: string; blurb: string }) => `${meta.label}: ${meta.blurb}`,
);
formatChannelSelectionLine.mockImplementation(
(meta: { label: string; blurb: string }) => `${meta.label}${meta.blurb}`,
);
isChannelConfigured.mockReturnValue(false);
});
it("sorts channels alphabetically by picker label", () => {
const contributions = resolveChannelSetupSelectionContributions({
entries: [
@@ -135,4 +182,130 @@ describe("resolveChannelSetupSelectionContributions", () => {
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",
},
},
] as never,
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([{ id: "discord", label: "Discord\u001B[31m\nCore\u0007" }]);
resolveChannelSetupEntries.mockReturnValue({
entries: [],
installedCatalogEntries: [
{
id: "matrix",
pluginId: "matrix",
meta: { id: "matrix", label: "Matrix\u001B[2K\nPlugin\u0007" },
},
],
installableCatalogEntries: [
{
id: "zalo",
pluginId: "zalo",
meta: { id: "zalo", label: "Zalo\u001B[2K\nPlugin\u0007" },
},
],
installedCatalogById: new Map(),
installableCatalogById: new Map(),
});
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("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",
},
] as never,
);
expect(formatChannelPrimerLine).toHaveBeenCalledWith(
expect.objectContaining({
id: "bad\\nid",
label: "bad\\nid",
selectionLabel: "bad\\nid",
blurb: "Blurb\\nline",
}),
);
expect(note).toHaveBeenCalledWith(
expect.stringContaining("bad\\nid: Blurb\\nline"),
"How channels work",
);
});
it("sanitizes channel metadata before selection notes", () => {
resolveChannelSetupEntries.mockReturnValue({
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"],
},
},
],
installedCatalogEntries: [],
installableCatalogEntries: [],
installedCatalogById: new Map(),
installableCatalogById: new Map(),
});
const lines = resolveChannelSelectionNoteLines({
cfg: {} as never,
installedPlugins: [],
selection: ["zalo"],
});
expect(formatChannelSelectionLine).toHaveBeenCalledWith(
expect.objectContaining({
label: "Zalo\\nBot",
blurb: "Setup\\nhelp",
docsLabel: "Docs\\nLabel",
selectionDocsPrefix: "Docs\\nPrefix",
selectionExtras: ["Extra\\nOne"],
}),
expect.any(Function),
);
expect(lines).toEqual(["Zalo\\nBot — Setup\\nhelp"]);
});
});

View File

@@ -3,6 +3,7 @@ import { listChatChannels } from "../channels/chat-meta.js";
import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js";
import { listChannelSetupPlugins } from "../channels/plugins/setup-registry.js";
import type { ChannelSetupPlugin } from "../channels/plugins/setup-wizard-types.js";
import type { ChannelMeta } from "../channels/plugins/types.core.js";
import { formatChannelPrimerLine, formatChannelSelectionLine } from "../channels/registry.js";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveChannelSetupEntries } from "../commands/channel-setup/discovery.js";
@@ -69,7 +70,11 @@ function buildChannelSetupSelectionContribution(params: {
}
function formatSetupSelectionLabel(label: string, fallback: string): string {
return sanitizeTerminalText(label) || fallback;
return (
sanitizeTerminalText(label).trim() ||
sanitizeTerminalText(fallback).trim() ||
"<invalid channel>"
);
}
function formatSetupSelectionHint(hint: string | undefined): string | undefined {
@@ -79,6 +84,49 @@ function formatSetupSelectionHint(hint: string | undefined): string | undefined
return sanitizeTerminalText(hint) || undefined;
}
function formatSetupDisplayText(value: string | undefined, fallback = ""): string {
return (
sanitizeTerminalText(value ?? "").trim() ||
sanitizeTerminalText(fallback).trim() ||
"<invalid channel>"
);
}
function formatSetupFreeText(value: string | undefined): string {
return sanitizeTerminalText(value ?? "").trim();
}
function formatSetupOptionalDisplayText(value: string | undefined): string | undefined {
const safe = sanitizeTerminalText(value ?? "").trim();
return safe || undefined;
}
function formatSetupDisplayList(values: readonly string[] | undefined): string[] | undefined {
const safe = (values ?? []).flatMap((value) => {
const sanitized = formatSetupOptionalDisplayText(value);
return sanitized ? [sanitized] : [];
});
return safe.length > 0 ? safe : undefined;
}
function formatSetupDisplayMeta(meta: ChannelMeta): ChannelMeta {
const safeId = formatSetupDisplayText(meta.id, "<invalid channel>");
const safeLabel = formatSetupDisplayText(meta.label, safeId);
const safeSelectionDocsPrefix = formatSetupOptionalDisplayText(meta.selectionDocsPrefix);
const safeSelectionExtras = formatSetupDisplayList(meta.selectionExtras);
return {
...meta,
id: safeId,
label: safeLabel,
selectionLabel: formatSetupDisplayText(meta.selectionLabel, safeLabel),
docsPath: formatSetupDisplayText(meta.docsPath, "/"),
...(meta.docsLabel ? { docsLabel: formatSetupDisplayText(meta.docsLabel, safeId) } : {}),
blurb: formatSetupFreeText(meta.blurb),
...(safeSelectionDocsPrefix ? { selectionDocsPrefix: safeSelectionDocsPrefix } : {}),
...(safeSelectionExtras ? { selectionExtras: safeSelectionExtras } : {}),
};
}
export async function collectChannelStatus(params: {
cfg: OpenClawConfig;
options?: SetupChannelsOptions;
@@ -125,7 +173,7 @@ export async function collectChannelStatus(params: {
return {
channel: meta.id,
configured,
statusLines: [`${meta.label}: ${statusLabel}`],
statusLines: [`${formatSetupSelectionLabel(meta.label, meta.id)}: ${statusLabel}`],
selectionHint: configured ? "configured · plugin disabled" : "not configured",
quickstartScore: 0,
};
@@ -146,7 +194,7 @@ export async function collectChannelStatus(params: {
return {
channel: entry.id as ChannelChoice,
configured,
statusLines: [`${entry.meta.label}: ${statusLabel}`],
statusLines: [`${formatSetupSelectionLabel(entry.meta.label, entry.id)}: ${statusLabel}`],
selectionHint: statusLabel,
quickstartScore: 0,
};
@@ -154,7 +202,9 @@ export async function collectChannelStatus(params: {
const catalogStatuses = installableCatalogEntries.map((entry) => ({
channel: entry.id,
configured: false,
statusLines: [`${entry.meta.label}: install plugin to enable`],
statusLines: [
`${formatSetupSelectionLabel(entry.meta.label, entry.id)}: install plugin to enable`,
],
selectionHint: "plugin · install",
quickstartScore: 0,
}));
@@ -200,13 +250,15 @@ export async function noteChannelPrimer(
channels: Array<{ id: ChannelChoice; blurb: string; label: string }>,
): Promise<void> {
const channelLines = channels.map((channel) =>
formatChannelPrimerLine({
id: channel.id,
label: channel.label,
selectionLabel: channel.label,
docsPath: "/",
blurb: channel.blurb,
}),
formatChannelPrimerLine(
formatSetupDisplayMeta({
id: channel.id,
label: channel.label,
selectionLabel: channel.label,
docsPath: "/",
blurb: channel.blurb,
}),
),
);
await prompter.note(
[
@@ -251,7 +303,10 @@ export function resolveChannelSelectionNoteLines(params: {
});
const selectionNotes = new Map<string, string>();
for (const entry of entries) {
selectionNotes.set(entry.id, formatChannelSelectionLine(entry.meta, formatDocsLink));
selectionNotes.set(
entry.id,
formatChannelSelectionLine(formatSetupDisplayMeta(entry.meta), formatDocsLink),
);
}
return params.selection
.map((channel) => selectionNotes.get(channel))