Files
moltbot/src/cli/plugins-cli.uninstall.test.ts
2026-04-26 11:28:18 +01:00

291 lines
8.2 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { installedPluginRoot } from "../../test/helpers/bundled-plugin-paths.js";
import type { OpenClawConfig } from "../config/config.js";
import {
applyPluginUninstallDirectoryRemoval,
buildPluginDiagnosticsReport,
loadConfig,
planPluginUninstall,
promptYesNo,
refreshPluginRegistry,
replaceConfigFile,
resetPluginsCliTestState,
runPluginsCommand,
runtimeErrors,
runtimeLogs,
setInstalledPluginIndexInstallRecords,
writeConfigFile,
writePersistedInstalledPluginIndexInstallRecords,
} from "./plugins-cli-test-helpers.js";
const CLI_STATE_ROOT = "/tmp/openclaw-state";
const ALPHA_INSTALL_PATH = installedPluginRoot(CLI_STATE_ROOT, "alpha");
describe("plugins cli uninstall", () => {
beforeEach(() => {
resetPluginsCliTestState();
});
it("shows uninstall dry-run preview without mutating config", async () => {
loadConfig.mockReturnValue({
plugins: {
entries: {
alpha: {
enabled: true,
},
},
installs: {
alpha: {
source: "path",
sourcePath: ALPHA_INSTALL_PATH,
installPath: ALPHA_INSTALL_PATH,
},
},
slots: {
contextEngine: "alpha",
},
},
} as OpenClawConfig);
buildPluginDiagnosticsReport.mockReturnValue({
plugins: [{ id: "alpha", name: "alpha" }],
diagnostics: [],
});
planPluginUninstall.mockReturnValue({
ok: true,
config: {} as OpenClawConfig,
actions: {
entry: true,
install: true,
allowlist: false,
denylist: false,
loadPath: false,
memorySlot: false,
contextEngineSlot: true,
directory: false,
},
directoryRemoval: null,
});
await runPluginsCommand(["plugins", "uninstall", "alpha", "--dry-run"]);
expect(planPluginUninstall).toHaveBeenCalled();
expect(writeConfigFile).not.toHaveBeenCalled();
expect(refreshPluginRegistry).not.toHaveBeenCalled();
expect(runtimeLogs.some((line) => line.includes("Dry run, no changes made."))).toBe(true);
expect(runtimeLogs.some((line) => line.includes("context engine slot"))).toBe(true);
});
it("uninstalls with --force and --keep-files without prompting", async () => {
const baseConfig = {
plugins: {
entries: {
alpha: { enabled: true },
},
installs: {
alpha: {
source: "path",
sourcePath: ALPHA_INSTALL_PATH,
installPath: ALPHA_INSTALL_PATH,
},
},
},
} as OpenClawConfig;
const nextConfig = {
plugins: {
entries: {},
installs: {},
},
} as OpenClawConfig;
loadConfig.mockReturnValue(baseConfig);
setInstalledPluginIndexInstallRecords(baseConfig.plugins?.installs ?? {});
buildPluginDiagnosticsReport.mockReturnValue({
plugins: [{ id: "alpha", name: "alpha" }],
diagnostics: [],
});
planPluginUninstall.mockReturnValue({
ok: true,
config: nextConfig,
actions: {
entry: true,
install: true,
allowlist: false,
denylist: false,
loadPath: false,
memorySlot: false,
contextEngineSlot: false,
directory: false,
},
directoryRemoval: null,
});
await runPluginsCommand(["plugins", "uninstall", "alpha", "--force", "--keep-files"]);
expect(promptYesNo).not.toHaveBeenCalled();
expect(planPluginUninstall).toHaveBeenCalledWith(
expect.objectContaining({
pluginId: "alpha",
deleteFiles: false,
}),
);
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({});
expect(writeConfigFile).toHaveBeenCalledWith({
plugins: {
entries: {},
},
});
expect(refreshPluginRegistry).toHaveBeenCalledWith({
config: {
plugins: {
entries: {},
},
},
installRecords: {},
reason: "source-changed",
});
});
it("restores install records when the config write rejects during uninstall", async () => {
const installRecords = {
alpha: {
source: "path",
sourcePath: ALPHA_INSTALL_PATH,
installPath: ALPHA_INSTALL_PATH,
},
} as const;
const baseConfig = {
plugins: {
entries: {
alpha: { enabled: true },
},
installs: installRecords,
},
} as OpenClawConfig;
const nextConfig = {
plugins: {
entries: {},
installs: {},
},
} as OpenClawConfig;
loadConfig.mockReturnValue(baseConfig);
setInstalledPluginIndexInstallRecords(installRecords);
buildPluginDiagnosticsReport.mockReturnValue({
plugins: [{ id: "alpha", name: "alpha" }],
diagnostics: [],
});
planPluginUninstall.mockReturnValue({
ok: true,
config: nextConfig,
actions: {
entry: true,
install: true,
allowlist: false,
denylist: false,
loadPath: false,
memorySlot: false,
contextEngineSlot: false,
directory: false,
},
directoryRemoval: null,
});
replaceConfigFile.mockRejectedValueOnce(new Error("config changed"));
await expect(
runPluginsCommand(["plugins", "uninstall", "alpha", "--force", "--keep-files"]),
).rejects.toThrow("config changed");
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenNthCalledWith(1, {});
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenNthCalledWith(
2,
installRecords,
);
expect(refreshPluginRegistry).not.toHaveBeenCalled();
expect(applyPluginUninstallDirectoryRemoval).not.toHaveBeenCalled();
});
it("removes plugin files only after config and index commit succeeds", async () => {
const installRecords = {
alpha: {
source: "npm",
spec: "alpha@1.0.0",
installPath: ALPHA_INSTALL_PATH,
},
} as const;
const baseConfig = {
plugins: {
entries: {
alpha: { enabled: true },
},
installs: installRecords,
},
} as OpenClawConfig;
const nextConfig = {
plugins: {
entries: {},
installs: {},
},
} as OpenClawConfig;
loadConfig.mockReturnValue(baseConfig);
setInstalledPluginIndexInstallRecords(installRecords);
buildPluginDiagnosticsReport.mockReturnValue({
plugins: [{ id: "alpha", name: "alpha" }],
diagnostics: [],
});
planPluginUninstall.mockReturnValue({
ok: true,
config: nextConfig,
actions: {
entry: true,
install: true,
allowlist: false,
denylist: false,
loadPath: false,
memorySlot: false,
contextEngineSlot: false,
directory: false,
},
directoryRemoval: { target: ALPHA_INSTALL_PATH },
});
applyPluginUninstallDirectoryRemoval.mockResolvedValue({
directoryRemoved: true,
warnings: [],
});
await runPluginsCommand(["plugins", "uninstall", "alpha", "--force"]);
const configWriteOrder = writeConfigFile.mock.invocationCallOrder[0] ?? 0;
const deleteOrder =
applyPluginUninstallDirectoryRemoval.mock.invocationCallOrder[0] ?? Number.MAX_SAFE_INTEGER;
const refreshOrder =
refreshPluginRegistry.mock.invocationCallOrder[0] ?? Number.MAX_SAFE_INTEGER;
expect(configWriteOrder).toBeGreaterThan(0);
expect(deleteOrder).toBeGreaterThan(configWriteOrder);
expect(refreshOrder).toBeGreaterThan(deleteOrder);
expect(applyPluginUninstallDirectoryRemoval).toHaveBeenCalledWith({
target: ALPHA_INSTALL_PATH,
});
});
it("exits when uninstall target is not managed by plugin install records", async () => {
loadConfig.mockReturnValue({
plugins: {
entries: {},
installs: {},
},
} as OpenClawConfig);
buildPluginDiagnosticsReport.mockReturnValue({
plugins: [{ id: "alpha", name: "alpha" }],
diagnostics: [],
});
await expect(runPluginsCommand(["plugins", "uninstall", "alpha", "--force"])).rejects.toThrow(
"__exit__:1",
);
expect(runtimeErrors.at(-1)).toContain("is not managed by plugins config/install records");
expect(planPluginUninstall).not.toHaveBeenCalled();
});
});