@@ -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