fix(update): clear stale plugin refs after failed updates

This commit is contained in:
JARVIS-Glasses
2026-05-13 21:12:46 +02:00
committed by Peter Steinberger
parent b5c3379097
commit 5214f16e29
2 changed files with 100 additions and 3 deletions

View File

@@ -1729,6 +1729,64 @@ describe("updateNpmInstalledPlugins", () => {
]);
});
it("clears stale plugin policy and slot references when disabling failed updates", async () => {
const warn = vi.fn();
installPluginFromNpmSpecMock.mockResolvedValue({
ok: false,
error: "security scan blocked install",
});
const config = {
plugins: {
allow: ["demo", "keep"],
deny: ["demo", "blocked"],
slots: {
memory: "demo",
contextEngine: "demo",
},
entries: {
demo: {
enabled: true,
},
},
installs: {
demo: {
source: "npm" as const,
spec: "@acme/demo",
installPath: "/tmp/demo",
},
},
},
} satisfies OpenClawConfig;
const result = await updateNpmInstalledPlugins({
config,
disableOnFailure: true,
logger: { warn },
});
const message =
'Disabled "demo" after plugin update failure; OpenClaw will continue without it. Failed to update demo: security scan blocked install';
expect(warn).toHaveBeenCalledWith(message);
expect(result.changed).toBe(true);
expect(result.config.plugins?.entries?.demo).toEqual({
enabled: false,
});
expect(result.config.plugins?.installs?.demo).toEqual(config.plugins.installs.demo);
expect(result.config.plugins?.allow).toEqual(["keep"]);
expect(result.config.plugins?.deny).toEqual(["blocked"]);
expect(result.config.plugins?.slots).toEqual({
memory: "memory-core",
contextEngine: "legacy",
});
expect(result.outcomes).toEqual([
{
pluginId: "demo",
status: "skipped",
message,
},
]);
});
it("aborts exact pinned npm plugin updates on integrity drift by default", async () => {
const warn = vi.fn();
installPluginFromNpmSpecMock.mockImplementation(

View File

@@ -48,6 +48,7 @@ import {
resolveOfficialExternalPluginInstall,
} from "./official-external-plugin-catalog.js";
import { linkOpenClawPeerDependencies } from "./plugin-peer-link.js";
import { defaultSlotIdForKey } from "./slots.js";
export type PluginUpdateLogger = {
info?: (message: string) => void;
@@ -760,14 +761,52 @@ function createPluginUpdateIntegrityDriftHandler(params: {
};
}
function removeDisabledPluginIdFromList(
list: string[] | undefined,
pluginId: string,
): string[] | undefined {
if (!Array.isArray(list) || !list.includes(pluginId)) {
return list;
}
const next = list.filter((id) => id !== pluginId);
return next.length > 0 ? next : undefined;
}
function resetDisabledPluginSlots(
slots: NonNullable<OpenClawConfig["plugins"]>["slots"] | undefined,
pluginId: string,
): NonNullable<OpenClawConfig["plugins"]>["slots"] | undefined {
if (!slots) {
return slots;
}
let next = slots;
if (next.memory === pluginId) {
next = {
...next,
memory: defaultSlotIdForKey("memory"),
};
}
if (next.contextEngine === pluginId) {
next = {
...next,
contextEngine: defaultSlotIdForKey("contextEngine"),
};
}
return next;
}
function disablePluginConfigEntry(config: OpenClawConfig, pluginId: string): OpenClawConfig {
const existingEntry = config.plugins?.entries?.[pluginId];
const pluginsConfig = config.plugins ?? {};
const existingEntry = pluginsConfig.entries?.[pluginId];
return {
...config,
plugins: {
...config.plugins,
...pluginsConfig,
allow: removeDisabledPluginIdFromList(pluginsConfig.allow, pluginId),
deny: removeDisabledPluginIdFromList(pluginsConfig.deny, pluginId),
slots: resetDisabledPluginSlots(pluginsConfig.slots, pluginId),
entries: {
...config.plugins?.entries,
...pluginsConfig.entries,
[pluginId]: {
...existingEntry,
enabled: false,