From f22a2f7e8b5863ce47fa46692207d59187ba1420 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 03:59:48 -0700 Subject: [PATCH] fix(plugins): migrate only enabled registry entries --- .../shared/plugin-registry-migration.test.ts | 49 +++++++++++++++++-- .../shared/plugin-registry-migration.ts | 20 +++++--- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/commands/doctor/shared/plugin-registry-migration.test.ts b/src/commands/doctor/shared/plugin-registry-migration.test.ts index ddc8745c405..641e6902d3a 100644 --- a/src/commands/doctor/shared/plugin-registry-migration.test.ts +++ b/src/commands/doctor/shared/plugin-registry-migration.test.ts @@ -34,7 +34,7 @@ function hermeticEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { }; } -function createCandidate(rootDir: string): PluginCandidate { +function createCandidate(rootDir: string, id = "demo"): PluginCandidate { fs.writeFileSync( path.join(rootDir, "index.ts"), "throw new Error('runtime entry should not load while migrating plugin registry');\n", @@ -43,15 +43,15 @@ function createCandidate(rootDir: string): PluginCandidate { fs.writeFileSync( path.join(rootDir, "openclaw.plugin.json"), JSON.stringify({ - id: "demo", - name: "Demo", + id, + name: id, configSchema: { type: "object" }, - providers: ["demo"], + providers: [id], }), "utf8", ); return { - idHint: "demo", + idHint: id, source: path.join(rootDir, "index.ts"), rootDir, origin: "global", @@ -83,6 +83,45 @@ describe("plugin registry install migration", () => { expect(readConfig).not.toHaveBeenCalled(); }); + it("persists only plugins enabled by the central config policy", async () => { + const stateDir = makeTempDir(); + const enabledDir = path.join(stateDir, "plugins", "enabled-demo"); + const disabledDir = path.join(stateDir, "plugins", "disabled-demo"); + fs.mkdirSync(enabledDir, { recursive: true }); + fs.mkdirSync(disabledDir, { recursive: true }); + + await expect( + migratePluginRegistryForInstall({ + stateDir, + candidates: [ + createCandidate(enabledDir, "enabled-demo"), + createCandidate(disabledDir, "disabled-demo"), + ], + readConfig: async () => ({ + plugins: { + entries: { + "disabled-demo": { + enabled: false, + }, + }, + }, + }), + env: hermeticEnv(), + }), + ).resolves.toMatchObject({ + status: "migrated", + current: { + plugins: [expect.objectContaining({ pluginId: "enabled-demo" })], + }, + }); + + await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({ + plugins: [expect.objectContaining({ pluginId: "enabled-demo" })], + }); + const persisted = await readPersistedInstalledPluginIndex({ stateDir }); + expect(persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual(["enabled-demo"]); + }); + it("supports dry-run preflight without reading config or writing the registry", async () => { const stateDir = makeTempDir(); const readConfig = vi.fn(async () => ({})); diff --git a/src/commands/doctor/shared/plugin-registry-migration.ts b/src/commands/doctor/shared/plugin-registry-migration.ts index 80ae3a4575c..53411376af3 100644 --- a/src/commands/doctor/shared/plugin-registry-migration.ts +++ b/src/commands/doctor/shared/plugin-registry-migration.ts @@ -2,14 +2,16 @@ import fs from "node:fs"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { inspectPersistedInstalledPluginIndex, - refreshPersistedInstalledPluginIndex, resolveInstalledPluginIndexStorePath, + writePersistedInstalledPluginIndex, type InstalledPluginIndexStoreInspection, type InstalledPluginIndexStoreOptions, } from "../../../plugins/installed-plugin-index-store.js"; -import type { - InstalledPluginIndex, - LoadInstalledPluginIndexParams, +import { + listEnabledInstalledPluginRecords, + loadInstalledPluginIndex, + type InstalledPluginIndex, + type LoadInstalledPluginIndexParams, } from "../../../plugins/installed-plugin-index.js"; export const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION"; @@ -121,10 +123,16 @@ export async function migratePluginRegistryForInstall( config, }; const inspection = await inspectPersistedInstalledPluginIndex(migrationParams); - const current = await refreshPersistedInstalledPluginIndex({ + const candidateIndex = loadInstalledPluginIndex({ ...migrationParams, - reason: "migration", + cache: false, }); + const current: InstalledPluginIndex = { + ...candidateIndex, + refreshReason: "migration", + plugins: listEnabledInstalledPluginRecords(candidateIndex, config), + }; + await writePersistedInstalledPluginIndex(current, params); return { status: "migrated", migrated: true,