Skip to content

Commit 1e609cf

Browse files
committed
fix(plugins): refresh managed npm update skips
1 parent 9900f16 commit 1e609cf

2 files changed

Lines changed: 340 additions & 8 deletions

File tree

src/plugins/update.test.ts

Lines changed: 259 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ describe("updateNpmInstalledPlugins", () => {
724724
expect(npmInstallCall()?.trustedSourceLinkedOfficialInstall).not.toBe(true);
725725
});
726726

727-
it("skips npm reinstall and config rewrite when the installed artifact is unchanged", async () => {
727+
it("refreshes managed npm root state without config rewrite when the installed artifact is unchanged", async () => {
728728
const installPath = createInstalledPackageDir({
729729
name: "@martian-engineering/lossless-claw",
730730
version: "0.9.0",
@@ -735,7 +735,18 @@ describe("updateNpmInstalledPlugins", () => {
735735
integrity: "sha512-same",
736736
shasum: "same",
737737
});
738-
installPluginFromNpmSpecMock.mockRejectedValue(new Error("installer should not run"));
738+
installPluginFromNpmSpecMock.mockResolvedValue(
739+
createSuccessfulNpmUpdateResult({
740+
pluginId: "lossless-claw",
741+
targetDir: installPath,
742+
version: "0.9.0",
743+
npmResolution: {
744+
name: "@martian-engineering/lossless-claw",
745+
version: "0.9.0",
746+
resolvedSpec: "@martian-engineering/[email protected]",
747+
},
748+
}),
749+
);
739750
const config: OpenClawConfig = {
740751
plugins: {
741752
installs: {
@@ -771,7 +782,9 @@ describe("updateNpmInstalledPlugins", () => {
771782
if (npmViewCall()?.[1] === undefined) {
772783
throw new Error("Expected npm view command options");
773784
}
774-
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
785+
expect(npmInstallCall()?.spec).toBe("@martian-engineering/lossless-claw");
786+
expect(npmInstallCall()?.mode).toBe("update");
787+
expect(npmInstallCall()?.expectedPluginId).toBe("lossless-claw");
775788
expect(result.changed).toBe(false);
776789
expect(result.config).toBe(config);
777790
expect(result.outcomes).toEqual([
@@ -785,6 +798,219 @@ describe("updateNpmInstalledPlugins", () => {
785798
]);
786799
});
787800

801+
it("records the installer result when unchanged npm maintenance resolves a newer artifact", async () => {
802+
const installPath = createInstalledPackageDir({
803+
name: "@martian-engineering/lossless-claw",
804+
version: "0.9.0",
805+
});
806+
mockNpmViewMetadata({
807+
name: "@martian-engineering/lossless-claw",
808+
version: "0.9.0",
809+
integrity: "sha512-same",
810+
shasum: "same",
811+
});
812+
installPluginFromNpmSpecMock.mockResolvedValue(
813+
createSuccessfulNpmUpdateResult({
814+
pluginId: "lossless-claw",
815+
targetDir: installPath,
816+
version: "0.9.1",
817+
npmResolution: {
818+
name: "@martian-engineering/lossless-claw",
819+
version: "0.9.1",
820+
resolvedSpec: "@martian-engineering/[email protected]",
821+
},
822+
}),
823+
);
824+
825+
const result = await updateNpmInstalledPlugins({
826+
config: createNpmInstallConfig({
827+
pluginId: "lossless-claw",
828+
spec: "@martian-engineering/lossless-claw",
829+
installPath,
830+
resolvedName: "@martian-engineering/lossless-claw",
831+
resolvedVersion: "0.9.0",
832+
resolvedSpec: "@martian-engineering/[email protected]",
833+
integrity: "sha512-same",
834+
shasum: "same",
835+
}),
836+
pluginIds: ["lossless-claw"],
837+
});
838+
839+
expect(result.changed).toBe(true);
840+
expectRecordFields(result.config.plugins?.installs?.["lossless-claw"], {
841+
source: "npm",
842+
resolvedName: "@martian-engineering/lossless-claw",
843+
resolvedVersion: "0.9.1",
844+
resolvedSpec: "@martian-engineering/[email protected]",
845+
});
846+
expectRecordFields(result.outcomes[0], {
847+
pluginId: "lossless-claw",
848+
status: "updated",
849+
currentVersion: "0.9.0",
850+
nextVersion: "0.9.1",
851+
});
852+
});
853+
854+
it("migrates plugin config ids when unchanged npm maintenance resolves a new id", async () => {
855+
const installPath = createInstalledPackageDir({
856+
name: "@legacy/lossless-claw",
857+
version: "0.9.0",
858+
});
859+
mockNpmViewMetadata({
860+
name: "@legacy/lossless-claw",
861+
version: "0.9.0",
862+
integrity: "sha512-same",
863+
shasum: "same",
864+
});
865+
installPluginFromNpmSpecMock.mockResolvedValue(
866+
createSuccessfulNpmUpdateResult({
867+
pluginId: "lossless-claw",
868+
targetDir: installPath,
869+
version: "0.9.1",
870+
npmResolution: {
871+
name: "@legacy/lossless-claw",
872+
version: "0.9.1",
873+
resolvedSpec: "@legacy/[email protected]",
874+
},
875+
}),
876+
);
877+
878+
const result = await updateNpmInstalledPlugins({
879+
config: {
880+
plugins: {
881+
allow: ["legacy-lossless"],
882+
entries: {
883+
"legacy-lossless": {
884+
enabled: true,
885+
},
886+
},
887+
installs: {
888+
"legacy-lossless": {
889+
source: "npm",
890+
spec: "@legacy/lossless-claw",
891+
installPath,
892+
resolvedName: "@legacy/lossless-claw",
893+
resolvedVersion: "0.9.0",
894+
resolvedSpec: "@legacy/[email protected]",
895+
integrity: "sha512-same",
896+
shasum: "same",
897+
},
898+
},
899+
},
900+
},
901+
pluginIds: ["legacy-lossless"],
902+
});
903+
904+
expect(result.config.plugins?.installs?.["legacy-lossless"]).toBeUndefined();
905+
expect(result.config.plugins?.entries?.["legacy-lossless"]).toBeUndefined();
906+
expect(result.config.plugins?.allow).toEqual(["lossless-claw"]);
907+
expectRecordFields(result.config.plugins?.installs?.["lossless-claw"], {
908+
source: "npm",
909+
resolvedName: "@legacy/lossless-claw",
910+
resolvedVersion: "0.9.1",
911+
resolvedSpec: "@legacy/[email protected]",
912+
});
913+
});
914+
915+
it("migrates plugin config ids when unchanged npm maintenance keeps the same version", async () => {
916+
const installPath = createInstalledPackageDir({
917+
name: "@legacy/lossless-claw",
918+
version: "0.9.0",
919+
});
920+
mockNpmViewMetadata({
921+
name: "@legacy/lossless-claw",
922+
version: "0.9.0",
923+
integrity: "sha512-same",
924+
shasum: "same",
925+
});
926+
installPluginFromNpmSpecMock.mockResolvedValue(
927+
createSuccessfulNpmUpdateResult({
928+
pluginId: "lossless-claw",
929+
targetDir: installPath,
930+
version: "0.9.0",
931+
npmResolution: {
932+
name: "@legacy/lossless-claw",
933+
version: "0.9.0",
934+
resolvedSpec: "@legacy/[email protected]",
935+
},
936+
}),
937+
);
938+
939+
const result = await updateNpmInstalledPlugins({
940+
config: {
941+
plugins: {
942+
allow: ["legacy-lossless"],
943+
installs: {
944+
"legacy-lossless": {
945+
source: "npm",
946+
spec: "@legacy/lossless-claw",
947+
installPath,
948+
resolvedName: "@legacy/lossless-claw",
949+
resolvedVersion: "0.9.0",
950+
resolvedSpec: "@legacy/[email protected]",
951+
integrity: "sha512-same",
952+
shasum: "same",
953+
},
954+
},
955+
},
956+
},
957+
pluginIds: ["legacy-lossless"],
958+
});
959+
960+
expect(result.changed).toBe(true);
961+
expect(result.config.plugins?.installs?.["legacy-lossless"]).toBeUndefined();
962+
expect(result.config.plugins?.allow).toEqual(["lossless-claw"]);
963+
expectRecordFields(result.config.plugins?.installs?.["lossless-claw"], {
964+
source: "npm",
965+
resolvedName: "@legacy/lossless-claw",
966+
resolvedVersion: "0.9.0",
967+
resolvedSpec: "@legacy/[email protected]",
968+
});
969+
expectRecordFields(result.outcomes[0], {
970+
pluginId: "legacy-lossless",
971+
status: "unchanged",
972+
currentVersion: "0.9.0",
973+
nextVersion: "0.9.0",
974+
});
975+
});
976+
977+
it("records unchanged npm maintenance exceptions as plugin failures", async () => {
978+
const installPath = createInstalledPackageDir({
979+
name: "@martian-engineering/lossless-claw",
980+
version: "0.9.0",
981+
});
982+
mockNpmViewMetadata({
983+
name: "@martian-engineering/lossless-claw",
984+
version: "0.9.0",
985+
integrity: "sha512-same",
986+
shasum: "same",
987+
});
988+
installPluginFromNpmSpecMock.mockRejectedValue(new Error("installer boom"));
989+
990+
const result = await updateNpmInstalledPlugins({
991+
config: createNpmInstallConfig({
992+
pluginId: "lossless-claw",
993+
spec: "@martian-engineering/lossless-claw",
994+
installPath,
995+
resolvedName: "@martian-engineering/lossless-claw",
996+
resolvedVersion: "0.9.0",
997+
resolvedSpec: "@martian-engineering/[email protected]",
998+
integrity: "sha512-same",
999+
shasum: "same",
1000+
}),
1001+
pluginIds: ["lossless-claw"],
1002+
});
1003+
1004+
expect(result.changed).toBe(false);
1005+
expect(result.outcomes).toEqual([
1006+
{
1007+
pluginId: "lossless-claw",
1008+
status: "error",
1009+
message: "Failed to update lossless-claw: Error: installer boom",
1010+
},
1011+
]);
1012+
});
1013+
7881014
it("repairs missing openclaw peer links before skipping unchanged npm plugins", async () => {
7891015
const installPath = createInstalledPackageDir({
7901016
name: "@openclaw/codex",
@@ -846,7 +1072,7 @@ describe("updateNpmInstalledPlugins", () => {
8461072
]);
8471073
});
8481074

849-
it("skips unchanged npm plugins when the openclaw peer link already resolves", async () => {
1075+
it("refreshes unchanged npm plugins when the openclaw peer link already resolves", async () => {
8501076
const installPath = createInstalledPackageDir({
8511077
name: "@openclaw/codex",
8521078
version: "2026.5.3",
@@ -859,7 +1085,18 @@ describe("updateNpmInstalledPlugins", () => {
8591085
integrity: "sha512-same",
8601086
shasum: "same",
8611087
});
862-
installPluginFromNpmSpecMock.mockRejectedValue(new Error("installer should not run"));
1088+
installPluginFromNpmSpecMock.mockResolvedValue(
1089+
createSuccessfulNpmUpdateResult({
1090+
pluginId: "codex",
1091+
targetDir: installPath,
1092+
version: "2026.5.3",
1093+
npmResolution: {
1094+
name: "@openclaw/codex",
1095+
version: "2026.5.3",
1096+
resolvedSpec: "@openclaw/[email protected]",
1097+
},
1098+
}),
1099+
);
8631100

8641101
const result = await updateNpmInstalledPlugins({
8651102
config: {
@@ -881,7 +1118,9 @@ describe("updateNpmInstalledPlugins", () => {
8811118
pluginIds: ["codex"],
8821119
});
8831120

884-
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
1121+
expect(npmInstallCall()?.spec).toBe("@openclaw/codex");
1122+
expect(npmInstallCall()?.mode).toBe("update");
1123+
expect(npmInstallCall()?.expectedPluginId).toBe("codex");
8851124
expect(result.changed).toBe(false);
8861125
expect(result.outcomes).toEqual([
8871126
{
@@ -1179,7 +1418,18 @@ describe("updateNpmInstalledPlugins", () => {
11791418
integrity: "sha512-same",
11801419
shasum: "same",
11811420
});
1182-
installPluginFromNpmSpecMock.mockRejectedValue(new Error("installer should not run"));
1421+
installPluginFromNpmSpecMock.mockResolvedValue(
1422+
createSuccessfulNpmUpdateResult({
1423+
pluginId: "lossless-claw",
1424+
targetDir: installPath,
1425+
version: "0.9.0",
1426+
npmResolution: {
1427+
name: "@martian-engineering/lossless-claw",
1428+
version: "0.9.0",
1429+
resolvedSpec: "@martian-engineering/[email protected]",
1430+
},
1431+
}),
1432+
);
11831433

11841434
const result = await updateNpmInstalledPlugins({
11851435
config: createNpmInstallConfig({
@@ -1195,7 +1445,8 @@ describe("updateNpmInstalledPlugins", () => {
11951445
pluginIds: ["lossless-claw"],
11961446
});
11971447

1198-
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
1448+
expect(npmInstallCall()?.spec).toBe("@martian-engineering/lossless-claw");
1449+
expect(npmInstallCall()?.extensionsDir).toBe(path.join(home, ".openclaw", "extensions"));
11991450
expect(result.changed).toBe(false);
12001451
expect(result.outcomes).toHaveLength(1);
12011452
expectRecordFields(result.outcomes[0], {

0 commit comments

Comments
 (0)