import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { withTempHome } from "../../config/home-env.test-harness.js"; import { handleCommands } from "./commands-core.js"; import { createCommandWorkspaceHarness } from "./commands-filesystem.test-support.js"; import { buildCommandTestParams } from "./commands.test-harness.js"; const installPluginFromPathMock = vi.fn(); const installPluginFromClawHubMock = vi.fn(); const persistPluginInstallMock = vi.fn(); vi.mock("../../plugins/install.js", async () => { const actual = await vi.importActual( "../../plugins/install.js", ); return { ...actual, installPluginFromPath: installPluginFromPathMock, }; }); vi.mock("../../plugins/clawhub.js", async () => { const actual = await vi.importActual( "../../plugins/clawhub.js", ); return { ...actual, installPluginFromClawHub: installPluginFromClawHubMock, }; }); vi.mock("../../cli/plugins-install-persist.js", () => ({ persistPluginInstall: persistPluginInstallMock, })); const workspaceHarness = createCommandWorkspaceHarness("openclaw-command-plugins-install-"); describe("handleCommands /plugins install", () => { afterEach(async () => { installPluginFromPathMock.mockReset(); installPluginFromClawHubMock.mockReset(); persistPluginInstallMock.mockReset(); await workspaceHarness.cleanupWorkspaces(); }); it("installs a plugin from a local path", async () => { installPluginFromPathMock.mockResolvedValue({ ok: true, pluginId: "path-install-plugin", targetDir: "/tmp/path-install-plugin", version: "0.0.1", extensions: ["index.js"], }); persistPluginInstallMock.mockResolvedValue({}); await withTempHome("openclaw-command-plugins-home-", async () => { const workspaceDir = await workspaceHarness.createWorkspace(); const pluginDir = path.join(workspaceDir, "fixtures", "path-install-plugin"); await fs.mkdir(pluginDir, { recursive: true }); const params = buildCommandTestParams( `/plugins install ${pluginDir}`, { commands: { text: true, plugins: true, }, }, undefined, { workspaceDir }, ); params.command.senderIsOwner = true; const result = await handleCommands(params); expect(result.reply?.text).toContain('Installed plugin "path-install-plugin"'); expect(installPluginFromPathMock).toHaveBeenCalledWith( expect.objectContaining({ path: pluginDir, }), ); expect(persistPluginInstallMock).toHaveBeenCalledWith( expect.objectContaining({ pluginId: "path-install-plugin", install: expect.objectContaining({ source: "path", sourcePath: pluginDir, installPath: "/tmp/path-install-plugin", version: "0.0.1", }), }), ); }); }); it("installs from an explicit clawhub: spec", async () => { installPluginFromClawHubMock.mockResolvedValue({ ok: true, pluginId: "clawhub-demo", targetDir: "/tmp/clawhub-demo", version: "1.2.3", extensions: ["index.js"], packageName: "@openclaw/clawhub-demo", clawhub: { source: "clawhub", clawhubUrl: "https://clawhub.ai", clawhubPackage: "@openclaw/clawhub-demo", clawhubFamily: "code-plugin", clawhubChannel: "official", version: "1.2.3", integrity: "sha512-demo", resolvedAt: "2026-03-22T12:00:00.000Z", }, }); persistPluginInstallMock.mockResolvedValue({}); await withTempHome("openclaw-command-plugins-home-", async () => { const workspaceDir = await workspaceHarness.createWorkspace(); const params = buildCommandTestParams( "/plugins install clawhub:@openclaw/clawhub-demo@1.2.3", { commands: { text: true, plugins: true, }, }, undefined, { workspaceDir }, ); params.command.senderIsOwner = true; const result = await handleCommands(params); expect(result.reply?.text).toContain('Installed plugin "clawhub-demo"'); expect(installPluginFromClawHubMock).toHaveBeenCalledWith( expect.objectContaining({ spec: "clawhub:@openclaw/clawhub-demo@1.2.3", }), ); expect(persistPluginInstallMock).toHaveBeenCalledWith( expect.objectContaining({ pluginId: "clawhub-demo", install: expect.objectContaining({ source: "clawhub", spec: "clawhub:@openclaw/clawhub-demo@1.2.3", installPath: "/tmp/clawhub-demo", version: "1.2.3", integrity: "sha512-demo", clawhubPackage: "@openclaw/clawhub-demo", clawhubChannel: "official", }), }), ); }); }); it("treats /plugin add as an install alias", async () => { installPluginFromClawHubMock.mockResolvedValue({ ok: true, pluginId: "alias-demo", targetDir: "/tmp/alias-demo", version: "1.0.0", extensions: ["index.js"], packageName: "@openclaw/alias-demo", clawhub: { source: "clawhub", clawhubUrl: "https://clawhub.ai", clawhubPackage: "@openclaw/alias-demo", clawhubFamily: "code-plugin", clawhubChannel: "official", version: "1.0.0", integrity: "sha512-alias", resolvedAt: "2026-03-23T12:00:00.000Z", }, }); persistPluginInstallMock.mockResolvedValue({}); await withTempHome("openclaw-command-plugins-home-", async () => { const workspaceDir = await workspaceHarness.createWorkspace(); const params = buildCommandTestParams( "/plugin add clawhub:@openclaw/alias-demo@1.0.0", { commands: { text: true, plugins: true, }, }, undefined, { workspaceDir }, ); params.command.senderIsOwner = true; const result = await handleCommands(params); expect(result.reply?.text).toContain('Installed plugin "alias-demo"'); expect(installPluginFromClawHubMock).toHaveBeenCalledWith( expect.objectContaining({ spec: "clawhub:@openclaw/alias-demo@1.0.0", }), ); }); }); });