refactor: install optional channels for remove
This commit is contained in:
parent
0443ee82be
commit
f3097b4c09
154
src/commands/channels.remove.test.ts
Normal file
154
src/commands/channels.remove.test.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js";
|
||||||
|
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
|
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||||
|
import {
|
||||||
|
ensureChannelSetupPluginInstalled,
|
||||||
|
loadChannelSetupPluginRegistrySnapshotForChannel,
|
||||||
|
} from "./channel-setup/plugin-install.js";
|
||||||
|
import { configMocks } from "./channels.mock-harness.js";
|
||||||
|
import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js";
|
||||||
|
|
||||||
|
const catalogMocks = vi.hoisted(() => ({
|
||||||
|
listChannelPluginCatalogEntries: vi.fn((): ChannelPluginCatalogEntry[] => []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../channels/plugins/catalog.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../channels/plugins/catalog.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
listChannelPluginCatalogEntries: catalogMocks.listChannelPluginCatalogEntries,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("./channel-setup/plugin-install.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("./channel-setup/plugin-install.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
ensureChannelSetupPluginInstalled: vi.fn(async ({ cfg }) => ({ cfg, installed: true })),
|
||||||
|
loadChannelSetupPluginRegistrySnapshotForChannel: vi.fn(() => createTestRegistry()),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtime = createTestRuntime();
|
||||||
|
let channelsRemoveCommand: typeof import("./channels.js").channelsRemoveCommand;
|
||||||
|
|
||||||
|
describe("channelsRemoveCommand", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
({ channelsRemoveCommand } = await import("./channels.js"));
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configMocks.readConfigFileSnapshot.mockClear();
|
||||||
|
configMocks.writeConfigFile.mockClear();
|
||||||
|
runtime.log.mockClear();
|
||||||
|
runtime.error.mockClear();
|
||||||
|
runtime.exit.mockClear();
|
||||||
|
catalogMocks.listChannelPluginCatalogEntries.mockClear();
|
||||||
|
catalogMocks.listChannelPluginCatalogEntries.mockReturnValue([]);
|
||||||
|
vi.mocked(ensureChannelSetupPluginInstalled).mockClear();
|
||||||
|
vi.mocked(ensureChannelSetupPluginInstalled).mockImplementation(async ({ cfg }) => ({
|
||||||
|
cfg,
|
||||||
|
installed: true,
|
||||||
|
}));
|
||||||
|
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel).mockClear();
|
||||||
|
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel).mockReturnValue(
|
||||||
|
createTestRegistry(),
|
||||||
|
);
|
||||||
|
setActivePluginRegistry(createTestRegistry());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes an external channel account after installing its plugin on demand", async () => {
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||||
|
...baseConfigSnapshot,
|
||||||
|
config: {
|
||||||
|
channels: {
|
||||||
|
msteams: {
|
||||||
|
enabled: true,
|
||||||
|
tenantId: "tenant-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const catalogEntry: ChannelPluginCatalogEntry = {
|
||||||
|
id: "msteams",
|
||||||
|
pluginId: "@openclaw/msteams-plugin",
|
||||||
|
meta: {
|
||||||
|
id: "msteams",
|
||||||
|
label: "Microsoft Teams",
|
||||||
|
selectionLabel: "Microsoft Teams",
|
||||||
|
docsPath: "/channels/msteams",
|
||||||
|
blurb: "teams channel",
|
||||||
|
},
|
||||||
|
install: {
|
||||||
|
npmSpec: "@openclaw/msteams",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
catalogMocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
|
||||||
|
const scopedPlugin = {
|
||||||
|
...createChannelTestPluginBase({
|
||||||
|
id: "msteams",
|
||||||
|
label: "Microsoft Teams",
|
||||||
|
docsPath: "/channels/msteams",
|
||||||
|
}),
|
||||||
|
config: {
|
||||||
|
...createChannelTestPluginBase({
|
||||||
|
id: "msteams",
|
||||||
|
label: "Microsoft Teams",
|
||||||
|
docsPath: "/channels/msteams",
|
||||||
|
}).config,
|
||||||
|
deleteAccount: vi.fn(({ cfg }: { cfg: Record<string, unknown> }) => {
|
||||||
|
const channels = (cfg.channels as Record<string, unknown> | undefined) ?? {};
|
||||||
|
const nextChannels = { ...channels };
|
||||||
|
delete nextChannels.msteams;
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: nextChannels,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel)
|
||||||
|
.mockReturnValueOnce(createTestRegistry())
|
||||||
|
.mockReturnValueOnce(
|
||||||
|
createTestRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "@openclaw/msteams-plugin",
|
||||||
|
plugin: scopedPlugin,
|
||||||
|
source: "test",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await channelsRemoveCommand(
|
||||||
|
{
|
||||||
|
channel: "msteams",
|
||||||
|
account: "default",
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
{ hasFlags: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ensureChannelSetupPluginInstalled).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
entry: catalogEntry,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
channel: "msteams",
|
||||||
|
pluginId: "@openclaw/msteams-plugin",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(configMocks.writeConfigFile).toHaveBeenCalledWith(
|
||||||
|
expect.not.objectContaining({
|
||||||
|
channels: expect.objectContaining({
|
||||||
|
msteams: expect.anything(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(runtime.error).not.toHaveBeenCalled();
|
||||||
|
expect(runtime.exit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -8,6 +8,7 @@ import { type OpenClawConfig, writeConfigFile } from "../../config/config.js";
|
|||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||||
|
import { resolveInstallableChannelPlugin } from "../channel-setup/channel-plugin-resolution.js";
|
||||||
import { type ChatChannel, channelLabel, requireValidConfig, shouldUseWizard } from "./shared.js";
|
import { type ChatChannel, channelLabel, requireValidConfig, shouldUseWizard } from "./shared.js";
|
||||||
|
|
||||||
export type ChannelsRemoveOptions = {
|
export type ChannelsRemoveOptions = {
|
||||||
@ -29,14 +30,16 @@ export async function channelsRemoveCommand(
|
|||||||
runtime: RuntimeEnv = defaultRuntime,
|
runtime: RuntimeEnv = defaultRuntime,
|
||||||
params?: { hasFlags?: boolean },
|
params?: { hasFlags?: boolean },
|
||||||
) {
|
) {
|
||||||
const cfg = await requireValidConfig(runtime);
|
const loadedCfg = await requireValidConfig(runtime);
|
||||||
if (!cfg) {
|
if (!loadedCfg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let cfg = loadedCfg;
|
||||||
|
|
||||||
const useWizard = shouldUseWizard(params);
|
const useWizard = shouldUseWizard(params);
|
||||||
const prompter = useWizard ? createClackPrompter() : null;
|
const prompter = useWizard ? createClackPrompter() : null;
|
||||||
let channel: ChatChannel | null = normalizeChannelId(opts.channel);
|
const rawChannel = opts.channel?.trim() ?? "";
|
||||||
|
let channel: ChatChannel | null = normalizeChannelId(rawChannel);
|
||||||
let accountId = normalizeAccountId(opts.account);
|
let accountId = normalizeAccountId(opts.account);
|
||||||
const deleteConfig = Boolean(opts.delete);
|
const deleteConfig = Boolean(opts.delete);
|
||||||
|
|
||||||
@ -73,15 +76,16 @@ export async function channelsRemoveCommand(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!channel) {
|
if (!rawChannel) {
|
||||||
runtime.error("Channel is required. Use --channel <name>.");
|
runtime.error("Channel is required. Use --channel <name>.");
|
||||||
runtime.exit(1);
|
runtime.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!deleteConfig) {
|
if (!deleteConfig) {
|
||||||
const confirm = createClackPrompter();
|
const confirm = createClackPrompter();
|
||||||
|
const channelPromptLabel = channel ? channelLabel(channel) : rawChannel;
|
||||||
const ok = await confirm.confirm({
|
const ok = await confirm.confirm({
|
||||||
message: `Disable ${channelLabel(channel)} account "${accountId}"? (keeps config)`,
|
message: `Disable ${channelPromptLabel} account "${accountId}"? (keeps config)`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@ -90,7 +94,20 @@ export async function channelsRemoveCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = getChannelPlugin(channel);
|
const resolvedPluginState =
|
||||||
|
!useWizard && rawChannel
|
||||||
|
? await resolveInstallableChannelPlugin({
|
||||||
|
cfg,
|
||||||
|
runtime,
|
||||||
|
rawChannel,
|
||||||
|
allowInstall: true,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (resolvedPluginState?.configChanged) {
|
||||||
|
cfg = resolvedPluginState.cfg;
|
||||||
|
}
|
||||||
|
channel = resolvedPluginState?.channelId ?? channel;
|
||||||
|
const plugin = resolvedPluginState?.plugin ?? (channel ? getChannelPlugin(channel) : undefined);
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
runtime.error(`Unknown channel: ${channel}`);
|
runtime.error(`Unknown channel: ${channel}`);
|
||||||
runtime.exit(1);
|
runtime.exit(1);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user