mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-17 17:10:53 +00:00
fix: harden plugin install and uninstall transactions
This commit is contained in:
@@ -2,8 +2,10 @@ 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,
|
||||
@@ -12,7 +14,6 @@ import {
|
||||
runtimeErrors,
|
||||
runtimeLogs,
|
||||
setInstalledPluginIndexInstallRecords,
|
||||
uninstallPlugin,
|
||||
writeConfigFile,
|
||||
writePersistedInstalledPluginIndexInstallRecords,
|
||||
} from "./plugins-cli-test-helpers.js";
|
||||
@@ -49,10 +50,25 @@ describe("plugins cli uninstall", () => {
|
||||
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(uninstallPlugin).not.toHaveBeenCalled();
|
||||
expect(planPluginUninstall).toHaveBeenCalled();
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(refreshPluginRegistry).not.toHaveBeenCalled();
|
||||
expect(runtimeLogs.some((line) => line.includes("Dry run, no changes made."))).toBe(true);
|
||||
@@ -87,25 +103,26 @@ describe("plugins cli uninstall", () => {
|
||||
plugins: [{ id: "alpha", name: "alpha" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
uninstallPlugin.mockResolvedValue({
|
||||
planPluginUninstall.mockReturnValue({
|
||||
ok: true,
|
||||
config: nextConfig,
|
||||
warnings: [],
|
||||
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(uninstallPlugin).toHaveBeenCalledWith(
|
||||
expect(planPluginUninstall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginId: "alpha",
|
||||
deleteFiles: false,
|
||||
@@ -157,19 +174,20 @@ describe("plugins cli uninstall", () => {
|
||||
plugins: [{ id: "alpha", name: "alpha" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
uninstallPlugin.mockResolvedValue({
|
||||
planPluginUninstall.mockReturnValue({
|
||||
ok: true,
|
||||
config: nextConfig,
|
||||
warnings: [],
|
||||
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"));
|
||||
|
||||
@@ -183,6 +201,68 @@ describe("plugins cli uninstall", () => {
|
||||
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;
|
||||
expect(configWriteOrder).toBeGreaterThan(0);
|
||||
expect(deleteOrder).toBeGreaterThan(configWriteOrder);
|
||||
expect(applyPluginUninstallDirectoryRemoval).toHaveBeenCalledWith({
|
||||
target: ALPHA_INSTALL_PATH,
|
||||
});
|
||||
});
|
||||
|
||||
it("exits when uninstall target is not managed by plugin install records", async () => {
|
||||
@@ -202,6 +282,6 @@ describe("plugins cli uninstall", () => {
|
||||
);
|
||||
|
||||
expect(runtimeErrors.at(-1)).toContain("is not managed by plugins config/install records");
|
||||
expect(uninstallPlugin).not.toHaveBeenCalled();
|
||||
expect(planPluginUninstall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user