fix(plugins): make uninstall teardown idempotent

This commit is contained in:
Vincent Koc
2026-05-02 10:43:32 -07:00
parent edfef73ffc
commit 336303e48b
4 changed files with 121 additions and 23 deletions

View File

@@ -271,6 +271,53 @@ describe("plugins cli uninstall", () => {
});
});
it("cleans stale policy refs even when plugin is absent from the current registry", async () => {
const baseConfig = {
plugins: {
allow: ["alpha", "beta"],
deny: ["alpha"],
},
} as OpenClawConfig;
const nextConfig = {
plugins: {
allow: ["beta"],
},
} as OpenClawConfig;
loadConfig.mockReturnValue(baseConfig);
buildPluginSnapshotReport.mockReturnValue({
plugins: [],
diagnostics: [],
});
planPluginUninstall.mockReturnValue({
ok: true,
config: nextConfig,
actions: {
entry: false,
install: false,
allowlist: true,
denylist: true,
loadPath: false,
memorySlot: false,
contextEngineSlot: false,
channelConfig: false,
directory: false,
},
directoryRemoval: null,
});
await runPluginsCommand(["plugins", "uninstall", "alpha", "--force"]);
expect(planPluginUninstall).toHaveBeenCalledWith(
expect.objectContaining({
pluginId: "alpha",
deleteFiles: true,
}),
);
expect(writeConfigFile).toHaveBeenCalledWith(nextConfig);
expect(runtimeLogs.at(-2)).toContain('Uninstalled plugin "alpha"');
});
it("exits when uninstall target is not managed by plugin install records", async () => {
loadConfig.mockReturnValue({
plugins: {
@@ -282,12 +329,16 @@ describe("plugins cli uninstall", () => {
plugins: [{ id: "alpha", name: "alpha" }],
diagnostics: [],
});
planPluginUninstall.mockReturnValue({
ok: false,
error: "Plugin not found: alpha",
});
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();
expect(planPluginUninstall).toHaveBeenCalled();
});
});

View File

@@ -74,21 +74,6 @@ export async function runPluginUninstallCommand(
config: cfg,
plugins: report.plugins,
});
const hasEntry = pluginId in (cfg.plugins?.entries ?? {});
const hasInstall = pluginId in (cfg.plugins?.installs ?? {});
if (!hasEntry && !hasInstall) {
if (plugin) {
runtime.error(
`Plugin "${pluginId}" is not managed by plugins config/install records and cannot be uninstalled.`,
);
} else {
runtime.error(`Plugin not found: ${id}`);
}
runtime.exit(1);
return;
}
const channelIds = plugin?.status === "loaded" ? plugin.channelIds : undefined;
const plan = planPluginUninstall({
config: cfg,
@@ -98,10 +83,17 @@ export async function runPluginUninstallCommand(
extensionsDir,
});
if (!plan.ok) {
runtime.error(plan.error);
if (plugin) {
runtime.error(
`Plugin "${pluginId}" is not managed by plugins config/install records and cannot be uninstalled.`,
);
} else {
runtime.error(plan.error);
}
runtime.exit(1);
return;
}
const hasInstall = pluginId in (cfg.plugins?.installs ?? {});
const preview: string[] = [];
if (plan.actions.entry) {