






















@@ -272,6 +272,28 @@ function createInstalledPackageDir(params: {
272272return dir;
273273}
274274275+function createOpenClawPeerLinkFixtures(plugins: Array<{ pluginId: string; packageName: string }>) {
276+const peerTarget = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-peer-target-"));
277+tempDirs.push(peerTarget);
278+const installPaths = Object.fromEntries(
279+plugins.map(({ pluginId, packageName }) => [
280+pluginId,
281+createInstalledPackageDir({
282+name: packageName,
283+version: "2026.5.4",
284+peerDependencies: { openclaw: ">=2026.5.4" },
285+}),
286+]),
287+);
288+const peerLinkPath = (pluginId: string) =>
289+path.join(installPaths[pluginId]!, "node_modules", "openclaw");
290+const linkPeer = (pluginId: string) => {
291+fs.mkdirSync(path.dirname(peerLinkPath(pluginId)), { recursive: true });
292+fs.symlinkSync(peerTarget, peerLinkPath(pluginId), "junction");
293+};
294+return { installPaths, peerLinkPath, linkPeer };
295+}
296+275297function mockNpmViewMetadata(params: {
276298name: string;
277299version: string;
@@ -833,6 +855,145 @@ describe("updateNpmInstalledPlugins", () => {
833855]);
834856});
835857858+it("repairs openclaw peer links after batch npm updates prune earlier plugin links", async () => {
859+const plugins = [
860+{ pluginId: "brave", packageName: "@openclaw/brave-plugin" },
861+{ pluginId: "codex", packageName: "@openclaw/codex" },
862+{ pluginId: "discord", packageName: "@openclaw/discord" },
863+];
864+const { installPaths, peerLinkPath, linkPeer } = createOpenClawPeerLinkFixtures(plugins);
865+for (const { packageName } of plugins) {
866+mockNpmViewMetadata({
867+name: packageName,
868+version: "2026.5.4",
869+integrity: "sha512-same",
870+shasum: "same",
871+});
872+}
873+installPluginFromNpmSpecMock.mockImplementation(
874+(params: { expectedPluginId?: string; spec: string }) => {
875+const pluginId = params.expectedPluginId!;
876+for (const { pluginId: installedPluginId } of plugins) {
877+fs.rmSync(peerLinkPath(installedPluginId), { recursive: true, force: true });
878+}
879+linkPeer(pluginId);
880+const packageName = plugins.find((plugin) => plugin.pluginId === pluginId)!.packageName;
881+return Promise.resolve(
882+createSuccessfulNpmUpdateResult({
883+ pluginId,
884+targetDir: installPaths[pluginId],
885+version: "2026.5.4",
886+npmResolution: {
887+name: packageName,
888+version: "2026.5.4",
889+resolvedSpec: `${packageName}@2026.5.4`,
890+},
891+}),
892+);
893+},
894+);
895+896+const result = await updateNpmInstalledPlugins({
897+config: {
898+plugins: {
899+installs: Object.fromEntries(
900+plugins.map(({ pluginId, packageName }) => [
901+pluginId,
902+{
903+source: "npm",
904+spec: packageName,
905+installPath: installPaths[pluginId],
906+resolvedName: packageName,
907+resolvedVersion: "2026.5.4",
908+resolvedSpec: `${packageName}@2026.5.4`,
909+integrity: "sha512-same",
910+shasum: "same",
911+},
912+]),
913+),
914+},
915+},
916+pluginIds: plugins.map((plugin) => plugin.pluginId),
917+});
918+919+expect(installPluginFromNpmSpecMock).toHaveBeenCalledTimes(3);
920+for (const { pluginId } of plugins) {
921+expect(fs.existsSync(peerLinkPath(pluginId))).toBe(true);
922+}
923+expect(result.outcomes).toEqual(
924+plugins.map(({ pluginId }) => ({
925+ pluginId,
926+status: "unchanged",
927+currentVersion: "2026.5.4",
928+nextVersion: "2026.5.4",
929+message: `${pluginId} already at 2026.5.4.`,
930+})),
931+);
932+});
933+934+it("repairs sibling openclaw peer links after a targeted npm update prunes the shared install tree", async () => {
935+const plugins = [
936+{ pluginId: "brave", packageName: "@openclaw/brave-plugin" },
937+{ pluginId: "codex", packageName: "@openclaw/codex" },
938+{ pluginId: "discord", packageName: "@openclaw/discord" },
939+];
940+const { installPaths, peerLinkPath, linkPeer } = createOpenClawPeerLinkFixtures(plugins);
941+linkPeer("brave");
942+linkPeer("discord");
943+mockNpmViewMetadata({
944+name: "@openclaw/codex",
945+version: "2026.5.4",
946+integrity: "sha512-same",
947+shasum: "same",
948+});
949+installPluginFromNpmSpecMock.mockImplementation(() => {
950+for (const { pluginId } of plugins) {
951+fs.rmSync(peerLinkPath(pluginId), { recursive: true, force: true });
952+}
953+linkPeer("codex");
954+return Promise.resolve(
955+createSuccessfulNpmUpdateResult({
956+pluginId: "codex",
957+targetDir: installPaths.codex,
958+version: "2026.5.4",
959+npmResolution: {
960+name: "@openclaw/codex",
961+version: "2026.5.4",
962+resolvedSpec: "@openclaw/codex@2026.5.4",
963+},
964+}),
965+);
966+});
967+968+await updateNpmInstalledPlugins({
969+config: {
970+plugins: {
971+installs: Object.fromEntries(
972+plugins.map(({ pluginId, packageName }) => [
973+pluginId,
974+{
975+source: "npm",
976+spec: packageName,
977+installPath: installPaths[pluginId],
978+resolvedName: packageName,
979+resolvedVersion: "2026.5.4",
980+resolvedSpec: `${packageName}@2026.5.4`,
981+integrity: "sha512-same",
982+shasum: "same",
983+},
984+]),
985+),
986+},
987+},
988+pluginIds: ["codex"],
989+});
990+991+expect(installPluginFromNpmSpecMock).toHaveBeenCalledTimes(1);
992+for (const { pluginId } of plugins) {
993+expect(fs.existsSync(peerLinkPath(pluginId))).toBe(true);
994+}
995+});
996+836997it("refreshes legacy npm install records before skipping unchanged artifacts", async () => {
837998const installPath = createInstalledPackageDir({
838999name: "@martian-engineering/lossless-claw",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。