mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:30:47 +00:00
CLI: sanitize channel setup notes
This commit is contained in:
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user