CLI: address channel setup review comments

This commit is contained in:
Gustavo Madeira Santana
2026-04-17 03:06:00 -04:00
parent 10528f7435
commit 204098b7fd
4 changed files with 116 additions and 3 deletions

View File

@@ -24,7 +24,7 @@ import { removeChannelConfigWizard } from "./configure.channels.js";
describe("removeChannelConfigWizard", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
confirm.mockResolvedValue(true);
});
@@ -34,6 +34,8 @@ describe("removeChannelConfigWizard", () => {
await removeChannelConfigWizard(
{
channels: {
defaults: { groupPolicy: "open" },
modelByChannel: { openai: { telegram: "gpt-5.4" } },
twitch: {},
unknown: {},
telegram: {},
@@ -80,6 +82,26 @@ describe("removeChannelConfigWizard", () => {
);
});
it("preserves channel-wide defaults when deleting the last channel block", async () => {
select.mockResolvedValueOnce("telegram").mockResolvedValueOnce("done");
const next = await removeChannelConfigWizard(
{
channels: {
defaults: { groupPolicy: "open" },
modelByChannel: { openai: { telegram: "gpt-5.4" } },
telegram: { token: "secret" },
},
} as never,
{} as never,
);
expect(next.channels).toEqual({
defaults: { groupPolicy: "open" },
modelByChannel: { openai: { telegram: "gpt-5.4" } },
});
});
it("sanitizes unknown channel keys before rendering prompts", async () => {
const unsafeChannel = "bad\u001B[31m\nkey\u0007";
select.mockResolvedValueOnce(unsafeChannel).mockResolvedValueOnce("done");

View File

@@ -14,6 +14,8 @@ type ConfiguredChannelRemovalChoice = {
label: string;
};
const RESERVED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
function listConfiguredChannelRemovalChoices(
cfg: OpenClawConfig,
): ConfiguredChannelRemovalChoice[] {
@@ -23,6 +25,7 @@ function listConfiguredChannelRemovalChoices(
}
const labelsById = new Map(listChatChannels().map((meta) => [meta.id, meta.label]));
return Object.keys(channels)
.filter((id) => !RESERVED_CHANNEL_CONFIG_KEYS.has(id))
.map((id) => ({
id,
label: labelsById.get(id) ?? formatUnknownChannelRemovalLabel(id),

View File

@@ -72,7 +72,8 @@ vi.mock("../commands/channel-setup/plugin-install.js", () => ({
}));
vi.mock("../commands/channel-setup/registry.js", () => ({
resolveChannelSetupWizardAdapterForPlugin: () => undefined,
resolveChannelSetupWizardAdapterForPlugin: (plugin?: { setupWizard?: unknown }) =>
plugin?.setupWizard,
}));
vi.mock("../commands/channel-setup/trusted-catalog.js", () => ({
@@ -226,4 +227,91 @@ describe("setupChannels workspace shadow exclusion", () => {
expect(getChannelSetupPlugin).not.toHaveBeenCalled();
expect(loadChannelSetupPluginRegistrySnapshotForChannel).not.toHaveBeenCalled();
});
it("loads the selected bundled catalog plugin without writing explicit plugin enablement", async () => {
const setupWizard = {
channel: "telegram",
getStatus: vi.fn(async () => ({
channel: "telegram",
configured: false,
statusLines: [],
})),
configure: vi.fn(async ({ cfg }: { cfg: Record<string, unknown> }) => ({
cfg: {
...cfg,
channels: {
telegram: { token: "secret" },
},
},
})),
};
const telegramPlugin = {
id: "telegram",
meta: { id: "telegram", label: "Telegram", blurb: "" },
capabilities: {},
config: {
resolveAccount: vi.fn(() => ({})),
},
setupWizard,
};
const installedCatalogEntry = {
id: "telegram",
pluginId: "telegram",
origin: "bundled",
meta: { id: "telegram", label: "Telegram", blurb: "" },
};
resolveChannelSetupEntries.mockReturnValue({
entries: [
{
id: "telegram",
meta: { id: "telegram", label: "Telegram", blurb: "" },
},
],
installedCatalogEntries: [installedCatalogEntry],
installableCatalogEntries: [],
installedCatalogById: new Map([["telegram", installedCatalogEntry]]),
installableCatalogById: new Map(),
});
loadChannelSetupPluginRegistrySnapshotForChannel.mockReturnValue({
channels: [{ plugin: telegramPlugin }],
channelSetups: [],
});
const select = vi.fn().mockResolvedValueOnce("telegram").mockResolvedValueOnce("__done__");
const next = await setupChannels(
{} as never,
{} as never,
{
confirm: vi.fn(async () => true),
note: vi.fn(async () => undefined),
select,
} as never,
{
deferStatusUntilSelection: true,
skipConfirm: true,
skipDmPolicyPrompt: true,
},
);
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(1);
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(
expect.objectContaining({
channel: "telegram",
pluginId: "telegram",
workspaceDir: "/tmp/openclaw-workspace",
}),
);
expect(getChannelSetupPlugin).not.toHaveBeenCalled();
expect(collectChannelStatus).not.toHaveBeenCalled();
expect(setupWizard.configure).toHaveBeenCalledWith(
expect.objectContaining({
cfg: {},
}),
);
expect(next).toEqual({
channels: {
telegram: { token: "secret" },
},
});
});
});

View File

@@ -512,7 +512,7 @@ export async function setupChannels(
}
await loadScopedChannelPlugin(channel, result.pluginId ?? catalogEntry.pluginId);
await refreshStatus(channel);
} else if (installedCatalogEntry && installedCatalogEntry.origin !== "bundled") {
} else if (installedCatalogEntry) {
const plugin = await loadScopedChannelPlugin(channel, installedCatalogEntry.pluginId);
if (!plugin) {
await prompter.note(`${channel} plugin not available.`, "Channel setup");