mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 21:30:21 +00:00
refactor: install heavy plugins on demand
This commit is contained in:
@@ -2,17 +2,33 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { runChannelLogin, runChannelLogout } from "./channel-auth.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
resolveAgentWorkspaceDir: vi.fn(),
|
||||
resolveDefaultAgentId: vi.fn(),
|
||||
getChannelPluginCatalogEntry: vi.fn(),
|
||||
resolveChannelDefaultAccountId: vi.fn(),
|
||||
getChannelPlugin: vi.fn(),
|
||||
normalizeChannelId: vi.fn(),
|
||||
loadConfig: vi.fn(),
|
||||
writeConfigFile: vi.fn(),
|
||||
resolveMessageChannelSelection: vi.fn(),
|
||||
setVerbose: vi.fn(),
|
||||
createClackPrompter: vi.fn(),
|
||||
ensureChannelSetupPluginInstalled: vi.fn(),
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel: vi.fn(),
|
||||
login: vi.fn(),
|
||||
logoutAccount: vi.fn(),
|
||||
resolveAccount: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../agents/agent-scope.js", () => ({
|
||||
resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId: mocks.resolveDefaultAgentId,
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/catalog.js", () => ({
|
||||
getChannelPluginCatalogEntry: mocks.getChannelPluginCatalogEntry,
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/helpers.js", () => ({
|
||||
resolveChannelDefaultAccountId: mocks.resolveChannelDefaultAccountId,
|
||||
}));
|
||||
@@ -24,6 +40,7 @@ vi.mock("../channels/plugins/index.js", () => ({
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: mocks.loadConfig,
|
||||
writeConfigFile: mocks.writeConfigFile,
|
||||
}));
|
||||
|
||||
vi.mock("../infra/outbound/channel-selection.js", () => ({
|
||||
@@ -34,9 +51,20 @@ vi.mock("../globals.js", () => ({
|
||||
setVerbose: mocks.setVerbose,
|
||||
}));
|
||||
|
||||
vi.mock("../wizard/clack-prompter.js", () => ({
|
||||
createClackPrompter: mocks.createClackPrompter,
|
||||
}));
|
||||
|
||||
vi.mock("../commands/channel-setup/plugin-install.js", () => ({
|
||||
ensureChannelSetupPluginInstalled: mocks.ensureChannelSetupPluginInstalled,
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel:
|
||||
mocks.loadChannelSetupPluginRegistrySnapshotForChannel,
|
||||
}));
|
||||
|
||||
describe("channel-auth", () => {
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const plugin = {
|
||||
id: "whatsapp",
|
||||
auth: { login: mocks.login },
|
||||
gateway: { logoutAccount: mocks.logoutAccount },
|
||||
config: { resolveAccount: mocks.resolveAccount },
|
||||
@@ -46,12 +74,26 @@ describe("channel-auth", () => {
|
||||
vi.clearAllMocks();
|
||||
mocks.normalizeChannelId.mockReturnValue("whatsapp");
|
||||
mocks.getChannelPlugin.mockReturnValue(plugin);
|
||||
mocks.getChannelPluginCatalogEntry.mockReturnValue(undefined);
|
||||
mocks.loadConfig.mockReturnValue({ channels: {} });
|
||||
mocks.writeConfigFile.mockResolvedValue(undefined);
|
||||
mocks.resolveMessageChannelSelection.mockResolvedValue({
|
||||
channel: "whatsapp",
|
||||
configured: ["whatsapp"],
|
||||
});
|
||||
mocks.resolveDefaultAgentId.mockReturnValue("main");
|
||||
mocks.resolveAgentWorkspaceDir.mockReturnValue("/tmp/workspace");
|
||||
mocks.resolveChannelDefaultAccountId.mockReturnValue("default-account");
|
||||
mocks.createClackPrompter.mockReturnValue({} as object);
|
||||
mocks.ensureChannelSetupPluginInstalled.mockResolvedValue({
|
||||
cfg: { channels: {} },
|
||||
installed: true,
|
||||
pluginId: "whatsapp",
|
||||
});
|
||||
mocks.loadChannelSetupPluginRegistrySnapshotForChannel.mockReturnValue({
|
||||
channels: [{ plugin }],
|
||||
channelSetups: [],
|
||||
});
|
||||
mocks.resolveAccount.mockReturnValue({ id: "resolved-account" });
|
||||
mocks.login.mockResolvedValue(undefined);
|
||||
mocks.logoutAccount.mockResolvedValue(undefined);
|
||||
@@ -115,6 +157,52 @@ describe("channel-auth", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("installs a catalog-backed channel plugin on demand for login", async () => {
|
||||
mocks.getChannelPlugin.mockReturnValueOnce(undefined);
|
||||
mocks.getChannelPluginCatalogEntry.mockReturnValueOnce({
|
||||
id: "whatsapp",
|
||||
pluginId: "@openclaw/whatsapp",
|
||||
meta: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "wa",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
},
|
||||
});
|
||||
mocks.loadChannelSetupPluginRegistrySnapshotForChannel
|
||||
.mockReturnValueOnce({
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
channels: [{ plugin }],
|
||||
channelSetups: [],
|
||||
});
|
||||
|
||||
await runChannelLogin({ channel: "whatsapp" }, runtime);
|
||||
|
||||
expect(mocks.ensureChannelSetupPluginInstalled).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entry: expect.objectContaining({ id: "whatsapp" }),
|
||||
runtime,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "whatsapp",
|
||||
pluginId: "whatsapp",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
}),
|
||||
);
|
||||
expect(mocks.writeConfigFile).toHaveBeenCalledWith({ channels: {} });
|
||||
expect(mocks.login).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs logout with resolved account and explicit account id", async () => {
|
||||
await runChannelLogout({ channel: "whatsapp", account: " acct-2 " }, runtime);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveInstallableChannelPlugin } from "../commands/channel-setup/channel-plugin-resolution.js";
|
||||
import { loadConfig, writeConfigFile, type OpenClawConfig } from "../config/config.js";
|
||||
import { setVerbose } from "../globals.js";
|
||||
import { resolveMessageChannelSelection } from "../infra/outbound/channel-selection.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
@@ -18,7 +19,14 @@ async function resolveChannelPluginForMode(
|
||||
opts: ChannelAuthOptions,
|
||||
mode: ChannelAuthMode,
|
||||
cfg: OpenClawConfig,
|
||||
): Promise<{ channelInput: string; channelId: string; plugin: ChannelPlugin }> {
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<{
|
||||
cfg: OpenClawConfig;
|
||||
configChanged: boolean;
|
||||
channelInput: string;
|
||||
channelId: string;
|
||||
plugin: ChannelPlugin;
|
||||
}> {
|
||||
const explicitChannel = opts.channel?.trim();
|
||||
const channelInput = explicitChannel
|
||||
? explicitChannel
|
||||
@@ -27,13 +35,28 @@ async function resolveChannelPluginForMode(
|
||||
if (!channelId) {
|
||||
throw new Error(`Unsupported channel: ${channelInput}`);
|
||||
}
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
|
||||
const resolved = await resolveInstallableChannelPlugin({
|
||||
cfg,
|
||||
runtime,
|
||||
channelId,
|
||||
allowInstall: true,
|
||||
supports: (candidate) =>
|
||||
mode === "login" ? Boolean(candidate.auth?.login) : Boolean(candidate.gateway?.logoutAccount),
|
||||
});
|
||||
const plugin = resolved.plugin;
|
||||
const supportsMode =
|
||||
mode === "login" ? Boolean(plugin?.auth?.login) : Boolean(plugin?.gateway?.logoutAccount);
|
||||
if (!supportsMode) {
|
||||
throw new Error(`Channel ${channelId} does not support ${mode}`);
|
||||
}
|
||||
return { channelInput, channelId, plugin: plugin as ChannelPlugin };
|
||||
return {
|
||||
cfg: resolved.cfg,
|
||||
configChanged: resolved.configChanged,
|
||||
channelInput,
|
||||
channelId,
|
||||
plugin: plugin as ChannelPlugin,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAccountContext(
|
||||
@@ -49,8 +72,16 @@ export async function runChannelLogin(
|
||||
opts: ChannelAuthOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
const cfg = loadConfig();
|
||||
const { channelInput, plugin } = await resolveChannelPluginForMode(opts, "login", cfg);
|
||||
const loadedCfg = loadConfig();
|
||||
const { cfg, configChanged, channelInput, plugin } = await resolveChannelPluginForMode(
|
||||
opts,
|
||||
"login",
|
||||
loadedCfg,
|
||||
runtime,
|
||||
);
|
||||
if (configChanged) {
|
||||
await writeConfigFile(cfg);
|
||||
}
|
||||
const login = plugin.auth?.login;
|
||||
if (!login) {
|
||||
throw new Error(`Channel ${channelInput} does not support login`);
|
||||
@@ -71,8 +102,16 @@ export async function runChannelLogout(
|
||||
opts: ChannelAuthOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
const cfg = loadConfig();
|
||||
const { channelInput, plugin } = await resolveChannelPluginForMode(opts, "logout", cfg);
|
||||
const loadedCfg = loadConfig();
|
||||
const { cfg, configChanged, channelInput, plugin } = await resolveChannelPluginForMode(
|
||||
opts,
|
||||
"logout",
|
||||
loadedCfg,
|
||||
runtime,
|
||||
);
|
||||
if (configChanged) {
|
||||
await writeConfigFile(cfg);
|
||||
}
|
||||
const logoutAccount = plugin.gateway?.logoutAccount;
|
||||
if (!logoutAccount) {
|
||||
throw new Error(`Channel ${channelInput} does not support logout`);
|
||||
|
||||
Reference in New Issue
Block a user