From 3743d6bdebbd1d899301914ce4fed1cd7a2e42be Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 20 May 2026 12:25:01 +0100 Subject: [PATCH] fix(update): prefer existing npm plugins during repair --- .../missing-configured-plugin-install.test.ts | 62 +++++++++++++++++++ .../missing-configured-plugin-install.ts | 1 + 2 files changed, 63 insertions(+) diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts index 8c206dbdbf7a..392121593e74 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts @@ -1139,6 +1139,68 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(result.records.discord?.installPath).toBe(packageDir); }); + it("prefers an existing npm payload over ClawHub during post-core repair", async () => { + const npmRoot = makeTempDir(); + const packageDir = path.join(npmRoot, "node_modules", "@openclaw", "matrix"); + fs.mkdirSync(packageDir, { recursive: true }); + mocks.resolveDefaultPluginNpmDir.mockReturnValue(npmRoot); + mocks.listChannelPluginCatalogEntries.mockReturnValue([ + { + id: "matrix", + pluginId: "matrix", + meta: { label: "Matrix" }, + install: { + clawhubSpec: "clawhub:@openclaw/matrix", + npmSpec: "@openclaw/matrix", + }, + }, + ]); + mocks.installPluginFromClawHub.mockResolvedValue({ + ok: false, + error: 'Plugin "@openclaw/matrix" requires plugin API >=2026.5.18.', + }); + mocks.installPluginFromNpmSpec.mockResolvedValue({ + ok: true, + pluginId: "matrix", + targetDir: packageDir, + version: "1.2.3", + npmResolution: { + name: "@openclaw/matrix", + version: "1.2.3", + resolvedSpec: "@openclaw/matrix@1.2.3", + integrity: "sha512-matrix", + resolvedAt: "2026-05-01T00:00:00.000Z", + }, + }); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + plugins: { + entries: { + matrix: { enabled: true }, + }, + }, + channels: { + matrix: { enabled: true }, + }, + }, + env: { + OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1", + }, + }); + + expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); + expectRecordFields(mockCallArg(mocks.installPluginFromNpmSpec), { + spec: expectedNpmInstallSpec("@openclaw/matrix"), + npmDir: npmRoot, + mode: "update", + }); + expect(result.warnings).toEqual([]); + expect(result.records.matrix?.installPath).toBe(packageDir); + }); + it("repairs missing external payload during post-core convergence even with OPENCLAW_UPDATE_IN_PROGRESS=1", async () => { const records = { discord: { diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ts index eec524235554..8861b3fba44d 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ts @@ -788,6 +788,7 @@ async function installCandidate(params: { : null; const shouldTryClawHub = clawhubInstallSpec && + !existingNpmPackagePath && !(params.preferNpm && npmInstallSpec) && candidate.defaultChoice !== "npm"; if (shouldTryClawHub) {