fix: fingerprint plugin metadata index reuse

This commit is contained in:
Shakker
2026-04-27 16:46:23 +01:00
parent 58b4407cda
commit 94591c3cb3
3 changed files with 119 additions and 61 deletions

View File

@@ -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),

View File

@@ -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);
});
});

View File

@@ -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: {