From 9e7acd4b2b61d738be53f2f68f4607ee1d77cd16 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 07:52:34 +0100 Subject: [PATCH] fix: tighten stale plugin diagnostic registry checks (#80134) --- CHANGELOG.md | 1 + src/plugins/plugin-registry-snapshot.test.ts | 48 +++++++++++++++++++- src/plugins/plugin-registry-snapshot.ts | 8 +++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30df6dfc865..f37f4f7f58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Media/host-read: allow buffer-verified gzip, tar, and 7z archives in the shared host-local media validator alongside ZIP and document attachments. +- Plugins/doctor: invalidate persisted plugin registry snapshots when plugin diagnostics point at deleted source paths, so `openclaw doctor` stops repeating stale warnings after a local extension is replaced by a managed npm plugin. Fixes #80087. (#80134) Thanks @hclsys. - Cron: let isolated self-cleanup runs inspect their own job run history while keeping other cron jobs and mutation actions blocked. Fixes #80019. Thanks @hclsys. - Cron: report isolated agent-turn setup and pre-model stalls with phase-specific timeout errors instead of waiting for the full job budget when no model call starts. Fixes #74803. Thanks @jeffsteinbok-openclaw and @dgkim311. - CLI/config: persist explicit `config set` and `config patch` values that equal runtime defaults instead of reporting success while dropping them. Fixes #79856. (#80106) Thanks @abodanty and @hclsys. diff --git a/src/plugins/plugin-registry-snapshot.test.ts b/src/plugins/plugin-registry-snapshot.test.ts index d0280780424..52adb1f713c 100644 --- a/src/plugins/plugin-registry-snapshot.test.ts +++ b/src/plugins/plugin-registry-snapshot.test.ts @@ -309,12 +309,22 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { ); }); - it("treats persisted registry as stale when a diagnostic source path no longer exists", () => { + it("treats persisted registry as stale when a plugin diagnostic source path no longer exists", () => { const tempRoot = makeTempDir(); const stateDir = path.join(tempRoot, "state"); - const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; + const env = { + ...createHermeticEnv(tempRoot), + OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", + OPENCLAW_STATE_DIR: stateDir, + }; const config = {}; const ghostDir = path.join(tempRoot, "extensions", "lossless-claw"); + const npmPluginDir = writeManagedNpmPlugin({ + stateDir, + packageName: "@martian-engineering/lossless-claw", + pluginId: "lossless-claw", + version: "0.9.4", + }); const staleIndex: InstalledPluginIndex = { ...loadInstalledPluginIndex({ config, env, stateDir, installRecords: {} }), diagnostics: [ @@ -335,8 +345,42 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { expect(result.snapshot.diagnostics).not.toContainEqual( expect.objectContaining({ source: ghostDir }), ); + expect(result.snapshot.plugins).toContainEqual( + expect.objectContaining({ + pluginId: "lossless-claw", + origin: "global", + source: fs.realpathSync(path.join(npmPluginDir, "dist", "index.js")), + }), + ); expect(result.diagnostics).toContainEqual( expect.objectContaining({ code: "persisted-registry-stale-source" }), ); }); + + it("keeps persisted registry when a non-plugin diagnostic source path still does not exist", () => { + const tempRoot = makeTempDir(); + const stateDir = path.join(tempRoot, "state"); + const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; + const config = {}; + const missingConfiguredPath = path.join(tempRoot, "missing-configured-plugin"); + const index: InstalledPluginIndex = { + ...loadInstalledPluginIndex({ config, env, stateDir, installRecords: {} }), + diagnostics: [ + { + level: "error", + message: `plugin path not found: ${missingConfiguredPath}`, + source: missingConfiguredPath, + }, + ], + }; + writePersistedInstalledPluginIndexSync(index, { stateDir }); + + const result = loadPluginRegistrySnapshotWithMetadata({ config, env, stateDir }); + + expect(result.source).toBe("persisted"); + expect(result.snapshot.diagnostics).toContainEqual( + expect.objectContaining({ source: missingConfiguredPath }), + ); + expect(result.diagnostics).toStrictEqual([]); + }); }); diff --git a/src/plugins/plugin-registry-snapshot.ts b/src/plugins/plugin-registry-snapshot.ts index 1deafa93bf3..3e4e319888b 100644 --- a/src/plugins/plugin-registry-snapshot.ts +++ b/src/plugins/plugin-registry-snapshot.ts @@ -134,7 +134,13 @@ function resolveRecordPackageJsonPath(plugin: InstalledPluginIndexRecord): strin function hasStalePersistedPluginDiagnostics(index: InstalledPluginIndex): boolean { return index.diagnostics.some((diag) => { const source = diag.source; - return typeof source === "string" && path.isAbsolute(source) && !fs.existsSync(source); + return ( + typeof diag.pluginId === "string" && + diag.pluginId.trim().length > 0 && + typeof source === "string" && + path.isAbsolute(source) && + !fs.existsSync(source) + ); }); }