fix(plugins): reset context engine slot on uninstall

This commit is contained in:
Peter Steinberger
2026-04-26 09:49:53 +01:00
parent 42487d0dac
commit c6b7444d16
8 changed files with 84 additions and 8 deletions

View File

@@ -365,6 +365,22 @@ describe("removePluginFromConfig", () => {
expect(actions.memorySlot).toBe(expectedChanged);
});
it("clears context engine slot when uninstalling active context engine plugin", () => {
const config = createPluginConfig({
entries: {
"context-plugin": { enabled: true },
},
slots: {
contextEngine: "context-plugin",
},
});
const { config: result, actions } = removePluginFromConfig(config, "context-plugin");
expect(result.plugins?.slots?.contextEngine).toBe("legacy");
expect(actions.contextEngineSlot).toBe(true);
});
it("removes plugins object when uninstall leaves only empty slots", () => {
const config = createSinglePluginWithEmptySlotsConfig();

View File

@@ -13,6 +13,7 @@ export type UninstallActions = {
allowlist: boolean;
loadPath: boolean;
memorySlot: boolean;
contextEngineSlot: boolean;
channelConfig: boolean;
directory: boolean;
};
@@ -155,6 +156,7 @@ export function removePluginFromConfig(
allowlist: false,
loadPath: false,
memorySlot: false,
contextEngineSlot: false,
channelConfig: false,
};
@@ -204,7 +206,7 @@ export function removePluginFromConfig(
}
}
// Reset memory slot if this plugin was selected
// Reset slots if this plugin was selected.
let slots = pluginsConfig.slots;
if (slots?.memory === pluginId) {
slots = {
@@ -213,6 +215,13 @@ export function removePluginFromConfig(
};
actions.memorySlot = true;
}
if (slots?.contextEngine === pluginId) {
slots = {
...slots,
contextEngine: defaultSlotIdForKey("contextEngine"),
};
actions.contextEngineSlot = true;
}
if (slots && Object.keys(slots).length === 0) {
slots = undefined;
}

View File

@@ -876,6 +876,41 @@ describe("updateNpmInstalledPlugins", () => {
expect(result.config.plugins?.installs?.["voice-call"]).toBeUndefined();
});
it("migrates context engine slot when a plugin id changes during update", async () => {
installPluginFromNpmSpecMock.mockResolvedValue({
ok: true,
pluginId: "@openclaw/context-engine",
targetDir: "/tmp/openclaw-context-engine",
version: "0.0.2",
extensions: ["index.ts"],
});
const result = await updateNpmInstalledPlugins({
config: {
plugins: {
slots: { contextEngine: "context-engine" },
installs: {
"context-engine": {
source: "npm",
spec: "@openclaw/context-engine",
installPath: "/tmp/context-engine",
},
},
},
} as OpenClawConfig,
pluginIds: ["context-engine"],
});
expect(result.config.plugins?.slots?.contextEngine).toBe("@openclaw/context-engine");
expect(result.config.plugins?.installs?.["@openclaw/context-engine"]).toMatchObject({
source: "npm",
spec: "@openclaw/context-engine",
installPath: "/tmp/openclaw-context-engine",
version: "0.0.2",
});
expect(result.config.plugins?.installs?.["context-engine"]).toBeUndefined();
});
it("checks marketplace installs during dry-run updates", async () => {
installPluginFromMarketplaceMock.mockResolvedValue({
ok: true,

View File

@@ -417,13 +417,13 @@ function migratePluginConfigId(cfg: OpenClawConfig, fromId: string, toId: string
delete nextEntries[fromId];
}
const nextSlots =
slots?.memory === fromId
? {
...slots,
memory: toId,
}
: slots;
const nextSlots = slots
? {
...slots,
...(slots.memory === fromId ? { memory: toId } : {}),
...(slots.contextEngine === fromId ? { contextEngine: toId } : {}),
}
: undefined;
return {
...cfg,