@@ -250,12 +250,24 @@ function createCodexAppServerInstallConfig(params: {
250250 } ;
251251}
252252
253- function createInstalledPackageDir ( params : { name ?: string ; version : string } ) : string {
253+ function createInstalledPackageDir ( params : {
254+ name ?: string ;
255+ version : string ;
256+ peerDependencies ?: Record < string , string > ;
257+ } ) : string {
254258 const dir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-plugin-update-test-" ) ) ;
255259 tempDirs . push ( dir ) ;
256260 fs . writeFileSync (
257261 path . join ( dir , "package.json" ) ,
258- JSON . stringify ( { name : params . name ?? "test-plugin" , version : params . version } , null , 2 ) ,
262+ JSON . stringify (
263+ {
264+ name : params . name ?? "test-plugin" ,
265+ version : params . version ,
266+ ...( params . peerDependencies ? { peerDependencies : params . peerDependencies } : { } ) ,
267+ } ,
268+ null ,
269+ 2 ,
270+ ) ,
259271 ) ;
260272 return dir ;
261273}
@@ -708,6 +720,119 @@ describe("updateNpmInstalledPlugins", () => {
708720 ] ) ;
709721 } ) ;
710722
723+ it ( "repairs missing openclaw peer links before skipping unchanged npm plugins" , async ( ) => {
724+ const installPath = createInstalledPackageDir ( {
725+ name : "@openclaw/codex" ,
726+ version : "2026.5.3" ,
727+ peerDependencies : { openclaw : ">=2026.5.3" } ,
728+ } ) ;
729+ mockNpmViewMetadata ( {
730+ name : "@openclaw/codex" ,
731+ version : "2026.5.3" ,
732+ integrity : "sha512-same" ,
733+ shasum : "same" ,
734+ } ) ;
735+ installPluginFromNpmSpecMock . mockResolvedValue (
736+ createSuccessfulNpmUpdateResult ( {
737+ pluginId : "codex" ,
738+ targetDir : installPath ,
739+ version : "2026.5.3" ,
740+ npmResolution : {
741+ name : "@openclaw/codex" ,
742+ version : "2026.5.3" ,
743+ resolvedSpec : "@openclaw/codex@2026.5.3" ,
744+ } ,
745+ } ) ,
746+ ) ;
747+ const config : OpenClawConfig = {
748+ plugins : {
749+ installs : {
750+ codex : {
751+ source : "npm" ,
752+ spec : "@openclaw/codex" ,
753+ installPath,
754+ resolvedName : "@openclaw/codex" ,
755+ resolvedVersion : "2026.5.3" ,
756+ resolvedSpec : "@openclaw/codex@2026.5.3" ,
757+ integrity : "sha512-same" ,
758+ shasum : "same" ,
759+ } ,
760+ } ,
761+ } ,
762+ } ;
763+
764+ const result = await updateNpmInstalledPlugins ( {
765+ config,
766+ pluginIds : [ "codex" ] ,
767+ } ) ;
768+
769+ expect ( installPluginFromNpmSpecMock ) . toHaveBeenCalledWith (
770+ expect . objectContaining ( {
771+ spec : "@openclaw/codex" ,
772+ mode : "update" ,
773+ expectedPluginId : "codex" ,
774+ } ) ,
775+ ) ;
776+ expect ( result . changed ) . toBe ( true ) ;
777+ expect ( result . outcomes ) . toEqual ( [
778+ {
779+ pluginId : "codex" ,
780+ status : "unchanged" ,
781+ currentVersion : "2026.5.3" ,
782+ nextVersion : "2026.5.3" ,
783+ message : "codex already at 2026.5.3." ,
784+ } ,
785+ ] ) ;
786+ } ) ;
787+
788+ it ( "skips unchanged npm plugins when the openclaw peer link already resolves" , async ( ) => {
789+ const installPath = createInstalledPackageDir ( {
790+ name : "@openclaw/codex" ,
791+ version : "2026.5.3" ,
792+ peerDependencies : { openclaw : ">=2026.5.3" } ,
793+ } ) ;
794+ fs . mkdirSync ( path . join ( installPath , "node_modules" , "openclaw" ) , { recursive : true } ) ;
795+ mockNpmViewMetadata ( {
796+ name : "@openclaw/codex" ,
797+ version : "2026.5.3" ,
798+ integrity : "sha512-same" ,
799+ shasum : "same" ,
800+ } ) ;
801+ installPluginFromNpmSpecMock . mockRejectedValue ( new Error ( "installer should not run" ) ) ;
802+
803+ const result = await updateNpmInstalledPlugins ( {
804+ config : {
805+ plugins : {
806+ installs : {
807+ codex : {
808+ source : "npm" ,
809+ spec : "@openclaw/codex" ,
810+ installPath,
811+ resolvedName : "@openclaw/codex" ,
812+ resolvedVersion : "2026.5.3" ,
813+ resolvedSpec : "@openclaw/codex@2026.5.3" ,
814+ integrity : "sha512-same" ,
815+ shasum : "same" ,
816+ } ,
817+ } ,
818+ } ,
819+ } ,
820+ pluginIds : [ "codex" ] ,
821+ } ) ;
822+
823+ expect ( installPluginFromNpmSpecMock ) . not . toHaveBeenCalled ( ) ;
824+ expect ( result . changed ) . toBe ( false ) ;
825+ expect ( result . outcomes ) . toEqual ( [
826+ {
827+ pluginId : "codex" ,
828+ status : "unchanged" ,
829+ currentVersion : "2026.5.3" ,
830+ nextVersion : "2026.5.3" ,
831+ message : "codex is up to date (2026.5.3)." ,
832+ } ,
833+ ] ) ;
834+ } ) ;
835+
711836 it ( "refreshes legacy npm install records before skipping unchanged artifacts" , async ( ) => {
712837 const installPath = createInstalledPackageDir ( {
713838 name : "@martian-engineering/lossless-claw" ,
0 commit comments