From 94591c3cb3b43f0ebfb858508b4c4cc2564e74e8 Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 27 Apr 2026 16:46:23 +0100 Subject: [PATCH] fix: fingerprint plugin metadata index reuse --- src/plugins/manifest-registry-installed.ts | 84 ++++++++++++---------- src/plugins/plugin-lookup-table.test.ts | 64 +++++++++++++++++ src/plugins/plugin-metadata-snapshot.ts | 32 +++------ 3 files changed, 119 insertions(+), 61 deletions(-) diff --git a/src/plugins/manifest-registry-installed.ts b/src/plugins/manifest-registry-installed.ts index d4290c1ed6b..b9817b2f708 100644 --- a/src/plugins/manifest-registry-installed.ts +++ b/src/plugins/manifest-registry-installed.ts @@ -73,6 +73,52 @@ function shouldUseInstalledManifestRegistryCache(params: { return !params.env.OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE?.trim(); } +function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) { + return { + version: index.version, + hostContractVersion: index.hostContractVersion, + compatRegistryVersion: index.compatRegistryVersion, + migrationVersion: index.migrationVersion, + policyHash: index.policyHash, + installRecords: index.installRecords, + diagnostics: index.diagnostics, + plugins: index.plugins.map((record) => { + const packageJsonPath = resolvePackageJsonPath(record); + return { + pluginId: record.pluginId, + packageName: record.packageName, + packageVersion: record.packageVersion, + installRecord: record.installRecord, + installRecordHash: record.installRecordHash, + packageInstall: record.packageInstall, + packageChannel: record.packageChannel, + manifestPath: record.manifestPath, + manifestHash: record.manifestHash, + manifestFile: safeFileSignature(record.manifestPath), + format: record.format, + bundleFormat: record.bundleFormat, + source: record.source, + setupSource: record.setupSource, + packageJson: record.packageJson, + packageJsonFile: safeFileSignature(packageJsonPath), + rootDir: record.rootDir, + origin: record.origin, + enabled: record.enabled, + enabledByDefault: record.enabledByDefault, + syntheticAuthRefs: record.syntheticAuthRefs, + startup: record.startup, + compat: record.compat, + }; + }), + }; +} + +export function resolveInstalledManifestRegistryIndexFingerprint( + index: InstalledPluginIndex, +): string { + return hashJson(buildInstalledManifestRegistryIndexKey(index)); +} + function buildInstalledManifestRegistryCacheKey(params: { index: InstalledPluginIndex; config?: OpenClawConfig; @@ -82,43 +128,7 @@ function buildInstalledManifestRegistryCacheKey(params: { includeDisabled?: boolean; }): string { return hashJson({ - index: { - version: params.index.version, - hostContractVersion: params.index.hostContractVersion, - compatRegistryVersion: params.index.compatRegistryVersion, - migrationVersion: params.index.migrationVersion, - policyHash: params.index.policyHash, - installRecords: params.index.installRecords, - diagnostics: params.index.diagnostics, - plugins: params.index.plugins.map((record) => { - const packageJsonPath = resolvePackageJsonPath(record); - return { - pluginId: record.pluginId, - packageName: record.packageName, - packageVersion: record.packageVersion, - installRecord: record.installRecord, - installRecordHash: record.installRecordHash, - packageInstall: record.packageInstall, - packageChannel: record.packageChannel, - manifestPath: record.manifestPath, - manifestHash: record.manifestHash, - manifestFile: safeFileSignature(record.manifestPath), - format: record.format, - bundleFormat: record.bundleFormat, - source: record.source, - setupSource: record.setupSource, - packageJson: record.packageJson, - packageJsonFile: safeFileSignature(packageJsonPath), - rootDir: record.rootDir, - origin: record.origin, - enabled: record.enabled, - enabledByDefault: record.enabledByDefault, - syntheticAuthRefs: record.syntheticAuthRefs, - startup: record.startup, - compat: record.compat, - }; - }), - }, + index: buildInstalledManifestRegistryIndexKey(params.index), request: { workspaceDir: params.workspaceDir, pluginIds: normalizePluginIdFilter(params.pluginIds), diff --git a/src/plugins/plugin-lookup-table.test.ts b/src/plugins/plugin-lookup-table.test.ts index 150794fa24b..05254571b0d 100644 --- a/src/plugins/plugin-lookup-table.test.ts +++ b/src/plugins/plugin-lookup-table.test.ts @@ -355,4 +355,68 @@ describe("loadPluginLookUpTable", () => { ); expect(table.manifestRegistry).toBe(requestedRegistry); }); + + it("rebuilds when a provided metadata snapshot has stale plugin paths", async () => { + const snapshotPlugins = [ + createManifestRecord({ + id: "telegram", + origin: "bundled", + channels: ["telegram"], + }), + ]; + const requestedPlugins = [ + createManifestRecord({ + id: "telegram", + origin: "bundled", + channels: ["telegram"], + rootDir: "/plugins-moved/telegram", + source: "/plugins-moved/telegram/index.js", + manifestPath: "/plugins-moved/telegram/openclaw.plugin.json", + }), + ]; + const config = { + channels: { + telegram: { token: "configured" }, + }, + } as OpenClawConfig; + const policyHash = resolveInstalledPluginIndexPolicyHash(config); + const snapshotIndex = createIndex(snapshotPlugins, { policyHash }); + const requestedIndex = createIndex(requestedPlugins, { policyHash }); + const snapshotRegistry: PluginManifestRegistry = { + plugins: snapshotPlugins, + diagnostics: [], + }; + const requestedRegistry: PluginManifestRegistry = { + plugins: requestedPlugins, + diagnostics: [], + }; + loadPluginManifestRegistryForInstalledIndex + .mockReturnValueOnce(snapshotRegistry) + .mockReturnValueOnce(requestedRegistry); + const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); + const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); + + const metadataSnapshot = loadPluginMetadataSnapshot({ + config, + env: {}, + index: snapshotIndex, + }); + loadPluginManifestRegistryForInstalledIndex.mockClear(); + + const table = loadPluginLookUpTable({ + config, + env: {}, + index: requestedIndex, + metadataSnapshot, + }); + + expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); + expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith( + expect.objectContaining({ + index: requestedIndex, + config, + }), + ); + expect(table.manifestRegistry).toBe(requestedRegistry); + }); }); diff --git a/src/plugins/plugin-metadata-snapshot.ts b/src/plugins/plugin-metadata-snapshot.ts index a4ce694a053..b6087a205f5 100644 --- a/src/plugins/plugin-metadata-snapshot.ts +++ b/src/plugins/plugin-metadata-snapshot.ts @@ -1,6 +1,9 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js"; -import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; +import { + loadPluginManifestRegistryForInstalledIndex, + resolveInstalledManifestRegistryIndexFingerprint, +} from "./manifest-registry-installed.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; import type { PluginDiagnostic } from "./manifest-types.js"; import { createPluginRegistryIdNormalizer } from "./plugin-registry-contributions.js"; @@ -58,29 +61,10 @@ function indexesMatch( if (!left || !right) { return true; } - if ( - left.version !== right.version || - left.hostContractVersion !== right.hostContractVersion || - left.compatRegistryVersion !== right.compatRegistryVersion || - left.migrationVersion !== right.migrationVersion || - left.policyHash !== right.policyHash || - left.plugins.length !== right.plugins.length - ) { - return false; - } - for (let index = 0; index < left.plugins.length; index += 1) { - const leftPlugin = left.plugins[index]; - const rightPlugin = right.plugins[index]; - if ( - !rightPlugin || - leftPlugin.pluginId !== rightPlugin.pluginId || - leftPlugin.manifestHash !== rightPlugin.manifestHash || - leftPlugin.installRecordHash !== rightPlugin.installRecordHash - ) { - return false; - } - } - return true; + return ( + resolveInstalledManifestRegistryIndexFingerprint(left) === + resolveInstalledManifestRegistryIndexFingerprint(right) + ); } export function isPluginMetadataSnapshotCompatible(params: {