diff --git a/src/plugins/installed-plugin-index-store.test.ts b/src/plugins/installed-plugin-index-store.test.ts index 5686e0c1e0e..617f539d594 100644 --- a/src/plugins/installed-plugin-index-store.test.ts +++ b/src/plugins/installed-plugin-index-store.test.ts @@ -254,4 +254,55 @@ describe("installed plugin index persistence", () => { plugins: [expect.objectContaining({ pluginId: "demo" })], }); }); + + it("preserves existing install records when refreshing the manifest cache", async () => { + const stateDir = makeTempDir(); + await writePersistedInstalledPluginIndex( + createIndex({ + installRecords: { + missing: { + source: "npm", + spec: "missing-plugin@1.0.0", + installPath: path.join(stateDir, "plugins", "missing"), + }, + }, + plugins: [], + }), + { stateDir }, + ); + + const index = await refreshPersistedInstalledPluginIndex({ + reason: "manual", + stateDir, + candidates: [], + env: { + OPENCLAW_BUNDLED_PLUGINS_DIR: undefined, + OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1", + OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1", + OPENCLAW_VERSION: "2026.4.25", + VITEST: "true", + }, + }); + + expect(index).toMatchObject({ + installRecords: { + missing: { + source: "npm", + spec: "missing-plugin@1.0.0", + installPath: path.join(stateDir, "plugins", "missing"), + }, + }, + plugins: [], + }); + await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({ + installRecords: { + missing: { + source: "npm", + spec: "missing-plugin@1.0.0", + installPath: path.join(stateDir, "plugins", "missing"), + }, + }, + plugins: [], + }); + }); }); diff --git a/src/plugins/installed-plugin-index-store.ts b/src/plugins/installed-plugin-index-store.ts index 199118827b8..930f79c7bfb 100644 --- a/src/plugins/installed-plugin-index-store.ts +++ b/src/plugins/installed-plugin-index-store.ts @@ -198,7 +198,12 @@ export async function inspectPersistedInstalledPluginIndex( export async function refreshPersistedInstalledPluginIndex( params: RefreshInstalledPluginIndexParams & InstalledPluginIndexStoreOptions, ): Promise { - const index = refreshInstalledPluginIndex(params); + const persisted = params.installRecords ? null : await readPersistedInstalledPluginIndex(params); + const index = refreshInstalledPluginIndex({ + ...params, + installRecords: + params.installRecords ?? extractPluginInstallRecordsFromInstalledPluginIndex(persisted), + }); await writePersistedInstalledPluginIndex(index, params); return index; } @@ -206,7 +211,12 @@ export async function refreshPersistedInstalledPluginIndex( export function refreshPersistedInstalledPluginIndexSync( params: RefreshInstalledPluginIndexParams & InstalledPluginIndexStoreOptions, ): InstalledPluginIndex { - const index = refreshInstalledPluginIndex(params); + const persisted = params.installRecords ? null : readPersistedInstalledPluginIndexSync(params); + const index = refreshInstalledPluginIndex({ + ...params, + installRecords: + params.installRecords ?? extractPluginInstallRecordsFromInstalledPluginIndex(persisted), + }); writePersistedInstalledPluginIndexSync(index, params); return index; } diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index d962b6db94c..0a29750a1a9 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -2,7 +2,10 @@ import fs from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import type { PluginCandidate } from "./discovery.js"; -import { writePersistedInstalledPluginIndex } from "./installed-plugin-index-store.js"; +import { + readPersistedInstalledPluginIndex, + writePersistedInstalledPluginIndex, +} from "./installed-plugin-index-store.js"; import { resolveInstalledPluginIndexPolicyHash, type InstalledPluginIndex, @@ -385,4 +388,39 @@ describe("plugin registry facade", () => { }, }); }); + + it("preserves install records when refreshing the persisted registry", async () => { + const stateDir = makeTempDir(); + await writePersistedInstalledPluginIndex( + createIndex("missing", { + installRecords: { + missing: { + source: "npm", + spec: "missing-plugin@1.0.0", + installPath: path.join(stateDir, "plugins", "missing"), + }, + }, + plugins: [], + }), + { stateDir }, + ); + + await refreshPluginRegistry({ + reason: "manual", + stateDir, + candidates: [], + env: hermeticEnv(), + }); + + await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({ + installRecords: { + missing: { + source: "npm", + spec: "missing-plugin@1.0.0", + installPath: path.join(stateDir, "plugins", "missing"), + }, + }, + plugins: [], + }); + }); });