fix(plugins): refresh stale source plugin registry (#96046) (#96080)

* fix(plugins): refresh stale source plugin registry

* fix(plugins): port registry snapshot to core manifest loader

---------

Co-authored-by: tangtaizong666 <212687958+tangtaizong666@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
tangtaizong666
2026-08-12 06:05:01 +08:00
committed by GitHub
parent bc030544ee
commit 4ccbdf58bf
2 changed files with 102 additions and 3 deletions
@@ -499,6 +499,49 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => {
expect(whatsappPlugin.origin).toBe("global");
});
it("recovers configured global source plugins missing from a stale persisted registry", () => {
const tempRoot = makeTempDir();
const stateDir = path.join(tempRoot, "state");
const env = {
...createHermeticEnv(tempRoot),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
OPENCLAW_STATE_DIR: stateDir,
};
const config = {
plugins: {
entries: {
"memory-demo": { enabled: true },
},
allow: ["memory-demo"],
slots: {
memory: "memory-demo",
},
},
};
const staleIndex = loadInstalledPluginIndex({
config,
env,
stateDir,
installRecords: {},
});
expect(staleIndex.plugins.map((plugin) => plugin.pluginId)).not.toContain("memory-demo");
writePersistedInstalledPluginIndexSync(staleIndex, { stateDir });
writePackagePlugin(path.join(stateDir, "extensions", "memory-demo-source"), {
pluginId: "memory-demo",
});
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
const memoryPlugin = requirePluginRecord(result.snapshot.plugins, "memory-demo");
expect(memoryPlugin.origin).toBe("global");
});
it("does not recover retained managed npm generations as install records", async () => {
const tempRoot = makeTempDir();
const stateDir = path.join(tempRoot, "state");
+59 -3
View File
@@ -9,7 +9,7 @@ import { buildLegacyBundledRootPath } from "./bundled-load-path-aliases.js";
import { listBundledSourceOverlayDirs } from "./bundled-source-overlays.js";
import { normalizePluginsConfig } from "./config-state.js";
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
import type { PluginDiscoveryResult } from "./discovery.js";
import { discoverConfiguredPluginLoadPaths, type PluginDiscoveryResult } from "./discovery.js";
import { resolvePluginDoctorContractArtifactPath } from "./doctor-contract-artifact.js";
import { safeFileSignature, safeHashFile } from "./installed-plugin-index-hash.js";
import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-manifest.js";
@@ -22,6 +22,7 @@ import {
type InstalledPluginIndexStoreOptions,
} from "./installed-plugin-index-store.js";
import {
extractPluginInstallRecordsFromInstalledPluginIndex,
getInstalledPluginRecord,
hasMissingConfigPathActivationMetadata,
isInstalledPluginEnabled,
@@ -32,10 +33,14 @@ import {
type LoadInstalledPluginIndexParams,
type RefreshInstalledPluginIndexParams,
} from "./installed-plugin-index.js";
import type { PluginManifestRegistry } from "./manifest-registry.js";
import {
loadPluginManifestRegistryCore,
type PluginManifestRegistry,
} from "./manifest-registry.js";
import { getPackageManifestMetadata, type PackageManifest } from "./manifest.js";
import { isPathInside, safeRealpathSync } from "./path-safety.js";
import type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js";
import { resolvePluginSourceRoots } from "./roots.js";
function resolvePluginRegistryContent(
index: InstalledPluginIndex,
@@ -405,10 +410,61 @@ function requiresDerivedRegistryValidation(
) ||
hasMismatchedPersistedBundledRoot(index, env) ||
hasStalePluginFiles() ||
hasRecoveredInstallRecordsMissingFromPersistedIndex(index, params, env)
hasRecoveredInstallRecordsMissingFromPersistedIndex(index, params, env) ||
hasConfiguredGlobalSourcePluginMissingFromPersistedIndex(params, index, env)
);
}
function collectConfiguredPluginIds(config: LoadPluginRegistryParams["config"]): Set<string> {
const plugins = normalizePluginsConfig(config?.plugins);
const pluginIds = new Set<string>();
for (const pluginId of Object.keys(plugins.entries)) {
pluginIds.add(pluginId);
}
for (const pluginId of plugins.allow) {
pluginIds.add(pluginId);
}
for (const pluginId of Object.values(plugins.slots)) {
if (typeof pluginId === "string" && pluginId.trim() && pluginId !== "none") {
pluginIds.add(pluginId);
}
}
return pluginIds;
}
function hasConfiguredGlobalSourcePluginMissingFromPersistedIndex(
params: LoadPluginRegistryParams,
index: InstalledPluginIndex,
env: NodeJS.ProcessEnv,
): boolean {
const configuredPluginIds = collectConfiguredPluginIds(params.config);
const persistedPluginIds = new Set(index.plugins.map((plugin) => plugin.pluginId));
const missingConfiguredPluginIds = new Set(
[...configuredPluginIds].filter((pluginId) => !persistedPluginIds.has(pluginId)),
);
if (missingConfiguredPluginIds.size === 0) {
return false;
}
const globalExtensionsRoot = resolvePluginSourceRoots({
workspaceDir: params.workspaceDir,
env,
}).global;
const discovery = discoverConfiguredPluginLoadPaths({
loadPaths: [globalExtensionsRoot],
workspaceDir: params.workspaceDir,
env,
});
const registry = loadPluginManifestRegistryCore({
config: params.config,
workspaceDir: params.workspaceDir,
env,
candidates: discovery.candidates,
diagnostics: discovery.diagnostics,
installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(index),
});
return registry.plugins.some((plugin) => missingConfiguredPluginIds.has(plugin.id));
}
export function loadPluginRegistrySnapshotWithMetadata(
params: LoadPluginRegistryParams = {},
): PluginRegistrySnapshotResult {