From d221d7b6a98902e5e3e0c82a48f38a4b9bca7a55 Mon Sep 17 00:00:00 2001 From: pickaxe <54486432+ProspectOre@users.noreply.github.com> Date: Tue, 5 May 2026 19:26:30 -0700 Subject: [PATCH] fix(plugins): isolate peer-link repair failures --- src/plugins/update.test.ts | 81 ++++++++++++++++++++++++++++++++++++++ src/plugins/update.ts | 18 ++++++--- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/plugins/update.test.ts b/src/plugins/update.test.ts index 3357f1a66f6..dd1b944290c 100644 --- a/src/plugins/update.test.ts +++ b/src/plugins/update.test.ts @@ -994,6 +994,87 @@ describe("updateNpmInstalledPlugins", () => { } }); + it("continues repairing sibling openclaw peer links after one recorded npm install cannot be relinked", async () => { + const plugins = [ + { pluginId: "brave", packageName: "@openclaw/brave-plugin" }, + { pluginId: "codex", packageName: "@openclaw/codex" }, + ]; + const { installPaths, peerLinkPath, linkPeer } = createOpenClawPeerLinkFixtures(plugins); + const brokenInstallPath = createInstalledPackageDir({ + name: "@openclaw/broken-plugin", + version: "2026.5.4", + peerDependencies: { openclaw: ">=2026.5.4" }, + }); + fs.writeFileSync(path.join(brokenInstallPath, "node_modules"), "not a directory"); + linkPeer("brave"); + mockNpmViewMetadata({ + name: "@openclaw/codex", + version: "2026.5.4", + integrity: "sha512-same", + shasum: "same", + }); + installPluginFromNpmSpecMock.mockImplementation(() => { + for (const { pluginId } of plugins) { + fs.rmSync(peerLinkPath(pluginId), { recursive: true, force: true }); + } + linkPeer("codex"); + return Promise.resolve( + createSuccessfulNpmUpdateResult({ + pluginId: "codex", + targetDir: installPaths.codex, + version: "2026.5.4", + npmResolution: { + name: "@openclaw/codex", + version: "2026.5.4", + resolvedSpec: "@openclaw/codex@2026.5.4", + }, + }), + ); + }); + const warnMessages: string[] = []; + + await updateNpmInstalledPlugins({ + config: { + plugins: { + installs: { + broken: { + source: "npm", + spec: "@openclaw/broken-plugin", + installPath: brokenInstallPath, + resolvedName: "@openclaw/broken-plugin", + resolvedVersion: "2026.5.4", + resolvedSpec: "@openclaw/broken-plugin@2026.5.4", + }, + ...Object.fromEntries( + plugins.map(({ pluginId, packageName }) => [ + pluginId, + { + source: "npm", + spec: packageName, + installPath: installPaths[pluginId], + resolvedName: packageName, + resolvedVersion: "2026.5.4", + resolvedSpec: `${packageName}@2026.5.4`, + integrity: "sha512-same", + shasum: "same", + }, + ]), + ), + }, + }, + }, + pluginIds: ["codex"], + logger: { warn: (message) => warnMessages.push(message) }, + }); + + expect(installPluginFromNpmSpecMock).toHaveBeenCalledTimes(1); + expect(fs.existsSync(peerLinkPath("brave"))).toBe(true); + expect(fs.existsSync(peerLinkPath("codex"))).toBe(true); + expect(warnMessages).toContainEqual( + expect.stringContaining('Could not repair openclaw peer link for "broken"'), + ); + }); + it("refreshes legacy npm install records before skipping unchanged artifacts", async () => { const installPath = createInstalledPackageDir({ name: "@martian-engineering/lossless-claw", diff --git a/src/plugins/update.ts b/src/plugins/update.ts index 898d90db3b9..59b680c2d6f 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -791,12 +791,18 @@ async function repairOpenClawPeerLinksForNpmInstalls(params: { continue; } - await linkOpenClawPeerDependencies({ - installedDir: installPath, - peerDependencies, - logger: params.logger, - }); - repaired = !installedPackageNeedsOpenClawPeerLinkRepair(installPath) || repaired; + try { + await linkOpenClawPeerDependencies({ + installedDir: installPath, + peerDependencies, + logger: params.logger, + }); + repaired = !installedPackageNeedsOpenClawPeerLinkRepair(installPath) || repaired; + } catch (err) { + params.logger.warn?.( + `Could not repair openclaw peer link for "${pluginId}" at ${installPath}: ${String(err)}`, + ); + } } return repaired; }