import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; const mocks = vi.hoisted(() => ({ memoryRegister: vi.fn(), otherRegister: vi.fn(), memoryListAction: vi.fn(), loadOpenClawPluginCliRegistry: vi.fn(), loadOpenClawPlugins: vi.fn(), applyPluginAutoEnable: vi.fn(), })); vi.mock("./loader.js", () => ({ loadOpenClawPluginCliRegistry: (...args: unknown[]) => mocks.loadOpenClawPluginCliRegistry(...args), loadOpenClawPlugins: (...args: unknown[]) => mocks.loadOpenClawPlugins(...args), })); vi.mock("../config/plugin-auto-enable.js", () => ({ applyPluginAutoEnable: (...args: unknown[]) => mocks.applyPluginAutoEnable(...args), })); import { getPluginCliCommandDescriptors, registerPluginCliCommands } from "./cli.js"; function createProgram(existingCommandName?: string) { const program = new Command(); if (existingCommandName) { program.command(existingCommandName); } return program; } function createCliRegistry(params?: { memoryCommands?: string[]; memoryDescriptors?: Array<{ name: string; description: string; hasSubcommands: boolean; }>; }) { return { cliRegistrars: [ { pluginId: "memory-core", register: mocks.memoryRegister, commands: params?.memoryCommands ?? ["memory"], descriptors: params?.memoryDescriptors ?? [ { name: "memory", description: "Memory commands", hasSubcommands: true, }, ], source: "bundled", }, { pluginId: "other", register: mocks.otherRegister, commands: ["other"], descriptors: [], source: "bundled", }, ], }; } function createEmptyCliRegistry(params?: { diagnostics?: Array<{ message: string }> }) { return { cliRegistrars: [], diagnostics: params?.diagnostics ?? [], }; } function expectPluginLoaderConfig(config: OpenClawConfig) { expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( expect.objectContaining({ config, }), ); } function createAutoEnabledCliFixture() { const rawConfig = { plugins: {}, channels: { demo: { enabled: true } }, } as OpenClawConfig; const autoEnabledConfig = { ...rawConfig, plugins: { entries: { demo: { enabled: true }, }, }, } as OpenClawConfig; return { rawConfig, autoEnabledConfig }; } function expectAutoEnabledCliLoad(params: { rawConfig: OpenClawConfig; autoEnabledConfig: OpenClawConfig; }) { expect(mocks.applyPluginAutoEnable).toHaveBeenCalledWith({ config: params.rawConfig, env: process.env, }); expectPluginLoaderConfig(params.autoEnabledConfig); } describe("registerPluginCliCommands", () => { beforeEach(() => { mocks.memoryRegister.mockReset(); mocks.memoryRegister.mockImplementation(({ program }: { program: Command }) => { const memory = program.command("memory").description("Memory commands"); memory.command("list").action(mocks.memoryListAction); }); mocks.otherRegister.mockReset(); mocks.otherRegister.mockImplementation(({ program }: { program: Command }) => { program.command("other").description("Other commands"); }); mocks.memoryListAction.mockReset(); mocks.loadOpenClawPluginCliRegistry.mockReset(); mocks.loadOpenClawPluginCliRegistry.mockResolvedValue(createCliRegistry()); mocks.loadOpenClawPlugins.mockReset(); mocks.loadOpenClawPlugins.mockReturnValue({ ...createCliRegistry(), diagnostics: [], }); mocks.applyPluginAutoEnable.mockReset(); mocks.applyPluginAutoEnable.mockImplementation(({ config }) => ({ config, changes: [] })); }); it("skips plugin CLI registrars when commands already exist", async () => { const program = createProgram("memory"); await registerPluginCliCommands(program, {} as OpenClawConfig); expect(mocks.memoryRegister).not.toHaveBeenCalled(); expect(mocks.otherRegister).toHaveBeenCalledTimes(1); }); it("forwards an explicit env to plugin loading", async () => { const env = { OPENCLAW_HOME: "/srv/openclaw-home" } as NodeJS.ProcessEnv; await registerPluginCliCommands(createProgram(), {} as OpenClawConfig, env); expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( expect.objectContaining({ env, }), ); }); it("loads plugin CLI commands from the auto-enabled config snapshot", async () => { const { rawConfig, autoEnabledConfig } = createAutoEnabledCliFixture(); mocks.applyPluginAutoEnable.mockReturnValue({ config: autoEnabledConfig, changes: [] }); await registerPluginCliCommands(createProgram(), rawConfig); expectAutoEnabledCliLoad({ rawConfig, autoEnabledConfig }); expect(mocks.memoryRegister).toHaveBeenCalledWith( expect.objectContaining({ config: autoEnabledConfig, }), ); }); it("loads root-help descriptors through the dedicated non-activating CLI collector", async () => { const { rawConfig, autoEnabledConfig } = createAutoEnabledCliFixture(); mocks.applyPluginAutoEnable.mockReturnValue({ config: autoEnabledConfig, changes: [] }); mocks.loadOpenClawPluginCliRegistry.mockResolvedValue({ cliRegistrars: [ { pluginId: "matrix", register: vi.fn(), commands: ["matrix"], descriptors: [ { name: "matrix", description: "Matrix channel utilities", hasSubcommands: true, }, ], source: "bundled", }, { pluginId: "duplicate-matrix", register: vi.fn(), commands: ["matrix"], descriptors: [ { name: "matrix", description: "Duplicate Matrix channel utilities", hasSubcommands: true, }, ], source: "bundled", }, ], }); await expect(getPluginCliCommandDescriptors(rawConfig)).resolves.toEqual([ { name: "matrix", description: "Matrix channel utilities", hasSubcommands: true, }, ]); expect(mocks.loadOpenClawPluginCliRegistry).toHaveBeenCalledWith( expect.objectContaining({ config: autoEnabledConfig, }), ); }); it("keeps runtime CLI command registration on the full plugin loader for legacy channel plugins", async () => { const { rawConfig, autoEnabledConfig } = createAutoEnabledCliFixture(); mocks.applyPluginAutoEnable.mockReturnValue({ config: autoEnabledConfig, changes: [] }); mocks.loadOpenClawPlugins.mockReturnValue( createCliRegistry({ memoryCommands: ["legacy-channel"], memoryDescriptors: [ { name: "legacy-channel", description: "Legacy channel commands", hasSubcommands: true, }, ], }), ); await registerPluginCliCommands(createProgram(), rawConfig, undefined, undefined, { mode: "lazy", }); expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith( expect.objectContaining({ config: autoEnabledConfig, }), ); expect(mocks.loadOpenClawPluginCliRegistry).not.toHaveBeenCalled(); }); it("falls back to awaited CLI metadata collection when runtime loading ignored async registration", async () => { const asyncRegistrar = vi.fn(async ({ program }: { program: Command }) => { const asyncCommand = program.command("async-cli").description("Async CLI"); asyncCommand.command("run").action(mocks.memoryListAction); }); mocks.loadOpenClawPlugins.mockReturnValue( createEmptyCliRegistry({ diagnostics: [ { message: "plugin register returned a promise; async registration is ignored", }, ], }), ); mocks.loadOpenClawPluginCliRegistry.mockResolvedValue({ cliRegistrars: [ { pluginId: "async-plugin", register: asyncRegistrar, commands: ["async-cli"], descriptors: [ { name: "async-cli", description: "Async CLI", hasSubcommands: true, }, ], source: "bundled", }, ], diagnostics: [], }); const program = createProgram(); program.exitOverride(); await registerPluginCliCommands(program, {} as OpenClawConfig, undefined, undefined, { mode: "lazy", }); expect(mocks.loadOpenClawPluginCliRegistry).toHaveBeenCalledTimes(1); await program.parseAsync(["async-cli", "run"], { from: "user" }); expect(asyncRegistrar).toHaveBeenCalledTimes(1); expect(mocks.memoryListAction).toHaveBeenCalledTimes(1); }); it("lazy-registers descriptor-backed plugin commands on first invocation", async () => { const program = createProgram(); program.exitOverride(); await registerPluginCliCommands(program, {} as OpenClawConfig, undefined, undefined, { mode: "lazy", }); expect(program.commands.map((command) => command.name())).toEqual(["memory", "other"]); expect(mocks.memoryRegister).not.toHaveBeenCalled(); expect(mocks.otherRegister).toHaveBeenCalledTimes(1); await program.parseAsync(["memory", "list"], { from: "user" }); expect(mocks.memoryRegister).toHaveBeenCalledTimes(1); expect(mocks.memoryListAction).toHaveBeenCalledTimes(1); }); it("falls back to eager registration when descriptors do not cover every command root", async () => { mocks.loadOpenClawPlugins.mockReturnValue( createCliRegistry({ memoryCommands: ["memory", "memory-admin"], memoryDescriptors: [ { name: "memory", description: "Memory commands", hasSubcommands: true, }, ], }), ); mocks.memoryRegister.mockImplementation(({ program }: { program: Command }) => { program.command("memory"); program.command("memory-admin"); }); await registerPluginCliCommands(createProgram(), {} as OpenClawConfig, undefined, undefined, { mode: "lazy", }); expect(mocks.memoryRegister).toHaveBeenCalledTimes(1); }); it("registers a selected plugin primary eagerly during lazy startup", async () => { const program = createProgram(); program.exitOverride(); await registerPluginCliCommands(program, {} as OpenClawConfig, undefined, undefined, { mode: "lazy", primary: "memory", }); expect(program.commands.filter((command) => command.name() === "memory")).toHaveLength(1); await program.parseAsync(["memory", "list"], { from: "user" }); expect(mocks.memoryRegister).toHaveBeenCalledTimes(1); expect(mocks.memoryListAction).toHaveBeenCalledTimes(1); }); });