From 5723d36b46a808f6bc5bf53ff2703c2fcfeb731e Mon Sep 17 00:00:00 2001 From: TUARAN <729922845@qq.com> Date: Tue, 28 Jul 2026 11:06:01 +0800 Subject: [PATCH] fix(doctor): honor configured plugin load paths (#113200) --- ...issing-configured-plugin-install.health.ts | 10 +-- ...onfigured-plugin-install.load-path.test.ts | 83 +++++++++++++++++++ ...issing-configured-plugin-install.repair.ts | 10 +-- 3 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 src/commands/doctor/shared/missing-configured-plugin-install.load-path.test.ts diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.health.ts b/src/commands/doctor/shared/missing-configured-plugin-install.health.ts index 64749e23ebc9..160ea31f55cc 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.health.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.health.ts @@ -283,19 +283,15 @@ export async function detectConfiguredPluginInstallHealthIssues(params: { ) { continue; } + const hasRecord = Object.hasOwn(records, candidate.pluginId); const hasUsableRecord = - Object.hasOwn(records, candidate.pluginId) && - !isInstalledRecordMissingOnDisk(records[candidate.pluginId], env); + hasRecord && !isInstalledRecordMissingOnDisk(records[candidate.pluginId], env); if ( !shouldReplaceBrokenOfficialInstall && - knownIds.has(candidate.pluginId) && - hasUsableRecord + (hasUsableRecord || (knownIds.has(candidate.pluginId) && !hasRecord)) ) { continue; } - if (!shouldReplaceBrokenOfficialInstall && hasUsableRecord) { - continue; - } const installSpec = resolveCandidateInstallSpec({ candidate, updateChannel, diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.load-path.test.ts b/src/commands/doctor/shared/missing-configured-plugin-install.load-path.test.ts new file mode 100644 index 000000000000..8ac847e35c65 --- /dev/null +++ b/src/commands/doctor/shared/missing-configured-plugin-install.load-path.test.ts @@ -0,0 +1,83 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { clearCurrentPluginMetadataSnapshot } from "../../../plugins/current-plugin-metadata-state.js"; +import { loadManifestMetadataSnapshot } from "../../../plugins/manifest-contract-eligibility.js"; +import { + detectConfiguredPluginInstallHealthIssues, + repairMissingConfiguredPluginInstalls, +} from "./missing-configured-plugin-install.js"; + +const tempDirs: string[] = []; + +afterEach(() => { + clearCurrentPluginMetadataSnapshot(); + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +function writeProviderPlugin(rootDir: string): void { + fs.mkdirSync(path.join(rootDir, "dist"), { recursive: true }); + fs.writeFileSync(path.join(rootDir, "dist", "index.js"), "export default {};\n", "utf8"); + fs.writeFileSync( + path.join(rootDir, "package.json"), + JSON.stringify({ + name: "@openclaw/kilocode-provider", + version: "2026.7.1", + openclaw: { + extensions: ["./index.ts"], + runtimeExtensions: ["./dist/index.js"], + }, + }), + "utf8", + ); + fs.writeFileSync( + path.join(rootDir, "openclaw.plugin.json"), + JSON.stringify({ + id: "kilocode", + enabledByDefault: true, + providers: ["kilocode"], + configSchema: { type: "object", properties: {} }, + }), + "utf8", + ); +} + +describe("configured plugin install health for explicit load paths", () => { + it("does not install a provider plugin already present at a configured load path", async () => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-load-path-provider-")); + tempDirs.push(rootDir); + const pluginDir = path.join(rootDir, "kilocode-provider"); + writeProviderPlugin(pluginDir); + + const cfg = { + plugins: { + load: { paths: [pluginDir] }, + }, + }; + const env = { + KILOCODE_API_KEY: "test-key", + OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(rootDir, "bundled"), + OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", + OPENCLAW_STATE_DIR: path.join(rootDir, "state"), + VITEST: "true", + }; + const snapshot = loadManifestMetadataSnapshot({ config: cfg, env }); + expect(snapshot.plugins.map((plugin) => plugin.id)).toContain("kilocode"); + + const issues = await detectConfiguredPluginInstallHealthIssues({ + cfg, + env, + }); + expect(issues).toStrictEqual([]); + + const repair = await repairMissingConfiguredPluginInstalls({ cfg, env }); + expect(repair).toMatchObject({ + changes: [], + records: {}, + warnings: [], + }); + }); +}); diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.repair.ts b/src/commands/doctor/shared/missing-configured-plugin-install.repair.ts index 67727fcabf01..b3d0c7e5993b 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.repair.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.repair.ts @@ -378,19 +378,15 @@ async function repairMissingPluginInstalls(params: { ) { continue; } + const hasRecord = Object.hasOwn(nextRecords, candidate.pluginId); const hasUsableRecord = - Object.hasOwn(nextRecords, candidate.pluginId) && - !isInstalledRecordMissingOnDisk(nextRecords[candidate.pluginId], env); + hasRecord && !isInstalledRecordMissingOnDisk(nextRecords[candidate.pluginId], env); if ( !shouldReplaceBrokenOfficialInstall && - knownIds.has(candidate.pluginId) && - hasUsableRecord + (hasUsableRecord || (knownIds.has(candidate.pluginId) && !hasRecord)) ) { continue; } - if (!shouldReplaceBrokenOfficialInstall && hasUsableRecord) { - continue; - } const removalPath = shouldReplaceBrokenOfficialInstall ? resolveSafeBrokenOfficialInstallRemovalPath({ pluginId: candidate.pluginId,