mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
test(plugins): cover missing plugin uninstall teardown
This commit is contained in:
@@ -318,6 +318,126 @@ describe("plugins cli uninstall", () => {
|
||||
expect(runtimeLogs.at(-2)).toContain('Uninstalled plugin "alpha"');
|
||||
});
|
||||
|
||||
it("uninstalls stale enabled entries when plugin is absent from the current registry", async () => {
|
||||
const baseConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
alpha: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const nextConfig = {} as OpenClawConfig;
|
||||
|
||||
loadConfig.mockReturnValue(baseConfig);
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
planPluginUninstall.mockReturnValue({
|
||||
ok: true,
|
||||
config: nextConfig,
|
||||
actions: {
|
||||
entry: true,
|
||||
install: false,
|
||||
allowlist: false,
|
||||
denylist: false,
|
||||
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(refreshPluginRegistry).toHaveBeenCalledWith({
|
||||
config: nextConfig,
|
||||
installRecords: {},
|
||||
reason: "source-changed",
|
||||
});
|
||||
expect(runtimeErrors).not.toContain("Plugin not found: alpha");
|
||||
expect(runtimeLogs.at(-2)).toContain('Uninstalled plugin "alpha"');
|
||||
});
|
||||
|
||||
it("removes installed channel config when plugin code is absent from the current registry", 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,
|
||||
},
|
||||
channels: {
|
||||
alpha: {
|
||||
enabled: true,
|
||||
},
|
||||
discord: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const nextConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
loadConfig.mockReturnValue(baseConfig);
|
||||
setInstalledPluginIndexInstallRecords(installRecords);
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
planPluginUninstall.mockReturnValue({
|
||||
ok: true,
|
||||
config: nextConfig,
|
||||
actions: {
|
||||
entry: true,
|
||||
install: true,
|
||||
allowlist: false,
|
||||
denylist: false,
|
||||
loadPath: false,
|
||||
memorySlot: false,
|
||||
contextEngineSlot: false,
|
||||
channelConfig: true,
|
||||
directory: false,
|
||||
},
|
||||
directoryRemoval: null,
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "uninstall", "alpha", "--force", "--keep-files"]);
|
||||
|
||||
expect(planPluginUninstall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginId: "alpha",
|
||||
channelIds: undefined,
|
||||
deleteFiles: false,
|
||||
}),
|
||||
);
|
||||
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({});
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(nextConfig);
|
||||
expect(runtimeLogs.some((line) => line.includes("channel config (channels.alpha)"))).toBe(true);
|
||||
expect(runtimeLogs.at(-2)).toContain('Uninstalled plugin "alpha"');
|
||||
});
|
||||
|
||||
it("exits when uninstall target is not managed by plugin install records", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
plugins: {
|
||||
|
||||
@@ -735,6 +735,138 @@ describe("uninstallPlugin", () => {
|
||||
expect(runCommandWithTimeoutMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "enabled entry only, no installed code",
|
||||
pluginId: "missing-entry-plugin",
|
||||
config: createPluginConfig({
|
||||
entries: {
|
||||
"missing-entry-plugin": { enabled: true },
|
||||
},
|
||||
}),
|
||||
expectedActions: {
|
||||
entry: true,
|
||||
install: false,
|
||||
allowlist: false,
|
||||
denylist: false,
|
||||
loadPath: false,
|
||||
memorySlot: false,
|
||||
contextEngineSlot: false,
|
||||
channelConfig: false,
|
||||
directory: false,
|
||||
},
|
||||
expectedConfig: {},
|
||||
},
|
||||
{
|
||||
name: "install record and channel config, no runtime plugin",
|
||||
pluginId: "missing-channel-plugin",
|
||||
config: createPluginConfig({
|
||||
installs: {
|
||||
"missing-channel-plugin": createNpmInstallRecord("missing-channel-plugin"),
|
||||
},
|
||||
channels: {
|
||||
"missing-channel-plugin": { enabled: true, token: "stale" },
|
||||
discord: { enabled: true },
|
||||
},
|
||||
}),
|
||||
expectedActions: {
|
||||
entry: false,
|
||||
install: true,
|
||||
allowlist: false,
|
||||
denylist: false,
|
||||
loadPath: false,
|
||||
memorySlot: false,
|
||||
contextEngineSlot: false,
|
||||
channelConfig: true,
|
||||
directory: false,
|
||||
},
|
||||
expectedConfig: {
|
||||
channels: {
|
||||
discord: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linked path record, missing source directory",
|
||||
pluginId: "missing-linked-plugin",
|
||||
config: createPluginConfig({
|
||||
installs: {
|
||||
"missing-linked-plugin": createPathInstallRecord(
|
||||
"/missing/openclaw/plugin",
|
||||
"/missing/openclaw/plugin",
|
||||
),
|
||||
},
|
||||
loadPaths: ["/missing/openclaw/plugin", "/keep/this/plugin"],
|
||||
}),
|
||||
expectedActions: {
|
||||
entry: false,
|
||||
install: true,
|
||||
allowlist: false,
|
||||
denylist: false,
|
||||
loadPath: true,
|
||||
memorySlot: false,
|
||||
contextEngineSlot: false,
|
||||
channelConfig: false,
|
||||
directory: false,
|
||||
},
|
||||
expectedConfig: {
|
||||
plugins: {
|
||||
load: {
|
||||
paths: ["/keep/this/plugin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "policy and slots only, no entry or install record",
|
||||
pluginId: "missing-policy-plugin",
|
||||
config: createPluginConfig({
|
||||
allow: ["missing-policy-plugin", "other-plugin"],
|
||||
deny: ["missing-policy-plugin"],
|
||||
slots: {
|
||||
memory: "missing-policy-plugin",
|
||||
contextEngine: "missing-policy-plugin",
|
||||
},
|
||||
}),
|
||||
expectedActions: {
|
||||
entry: false,
|
||||
install: false,
|
||||
allowlist: true,
|
||||
denylist: true,
|
||||
loadPath: false,
|
||||
memorySlot: true,
|
||||
contextEngineSlot: true,
|
||||
channelConfig: false,
|
||||
directory: false,
|
||||
},
|
||||
expectedConfig: {
|
||||
plugins: {
|
||||
allow: ["other-plugin"],
|
||||
slots: {
|
||||
memory: "memory-core",
|
||||
contextEngine: "legacy",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const)(
|
||||
"uninstall teardown matrix: $name",
|
||||
async ({ pluginId, config, expectedActions, expectedConfig }) => {
|
||||
const result = await uninstallPlugin({
|
||||
config,
|
||||
pluginId,
|
||||
deleteFiles: true,
|
||||
extensionsDir: path.join(tempDir, "extensions"),
|
||||
});
|
||||
|
||||
const successfulResult = expectSuccessfulUninstall(result);
|
||||
expect(successfulResult.actions).toEqual(expectedActions);
|
||||
expect(successfulResult.config).toEqual(expectedConfig);
|
||||
expect(successfulResult.warnings).toEqual([]);
|
||||
expect(runCommandWithTimeoutMock).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("removes config entries", async () => {
|
||||
const config = createPluginConfig({
|
||||
entries: createSinglePluginEntries(),
|
||||
|
||||
Reference in New Issue
Block a user