@@ -994,6 +994,87 @@ describe("updateNpmInstalledPlugins", () => {
994994 }
995995 } ) ;
996996
997+ it ( "continues repairing sibling openclaw peer links after one recorded npm install cannot be relinked" , async ( ) => {
998+ const plugins = [
999+ { pluginId : "brave" , packageName : "@openclaw/brave-plugin" } ,
1000+ { pluginId : "codex" , packageName : "@openclaw/codex" } ,
1001+ ] ;
1002+ const { installPaths, peerLinkPath, linkPeer } = createOpenClawPeerLinkFixtures ( plugins ) ;
1003+ const brokenInstallPath = createInstalledPackageDir ( {
1004+ name : "@openclaw/broken-plugin" ,
1005+ version : "2026.5.4" ,
1006+ peerDependencies : { openclaw : ">=2026.5.4" } ,
1007+ } ) ;
1008+ fs . writeFileSync ( path . join ( brokenInstallPath , "node_modules" ) , "not a directory" ) ;
1009+ linkPeer ( "brave" ) ;
1010+ mockNpmViewMetadata ( {
1011+ name : "@openclaw/codex" ,
1012+ version : "2026.5.4" ,
1013+ integrity : "sha512-same" ,
1014+ shasum : "same" ,
1015+ } ) ;
1016+ installPluginFromNpmSpecMock . mockImplementation ( ( ) => {
1017+ for ( const { pluginId } of plugins ) {
1018+ fs . rmSync ( peerLinkPath ( pluginId ) , { recursive : true , force : true } ) ;
1019+ }
1020+ linkPeer ( "codex" ) ;
1021+ return Promise . resolve (
1022+ createSuccessfulNpmUpdateResult ( {
1023+ pluginId : "codex" ,
1024+ targetDir : installPaths . codex ,
1025+ version : "2026.5.4" ,
1026+ npmResolution : {
1027+ name : "@openclaw/codex" ,
1028+ version : "2026.5.4" ,
1029+ resolvedSpec : "@openclaw/codex@2026.5.4" ,
1030+ } ,
1031+ } ) ,
1032+ ) ;
1033+ } ) ;
1034+ const warnMessages : string [ ] = [ ] ;
1035+
1036+ await updateNpmInstalledPlugins ( {
1037+ config : {
1038+ plugins : {
1039+ installs : {
1040+ broken : {
1041+ source : "npm" ,
1042+ spec : "@openclaw/broken-plugin" ,
1043+ installPath : brokenInstallPath ,
1044+ resolvedName : "@openclaw/broken-plugin" ,
1045+ resolvedVersion : "2026.5.4" ,
1046+ resolvedSpec : "@openclaw/broken-plugin@2026.5.4" ,
1047+ } ,
1048+ ...Object . fromEntries (
1049+ plugins . map ( ( { pluginId, packageName } ) => [
1050+ pluginId ,
1051+ {
1052+ source : "npm" ,
1053+ spec : packageName ,
1054+ installPath : installPaths [ pluginId ] ,
1055+ resolvedName : packageName ,
1056+ resolvedVersion : "2026.5.4" ,
1057+ resolvedSpec : `${ packageName } @2026.5.4` ,
1058+ integrity : "sha512-same" ,
1059+ shasum : "same" ,
1060+ } ,
1061+ ] ) ,
1062+ ) ,
1063+ } ,
1064+ } ,
1065+ } ,
1066+ pluginIds : [ "codex" ] ,
1067+ logger : { warn : ( message ) => warnMessages . push ( message ) } ,
1068+ } ) ;
1069+
1070+ expect ( installPluginFromNpmSpecMock ) . toHaveBeenCalledTimes ( 1 ) ;
1071+ expect ( fs . existsSync ( peerLinkPath ( "brave" ) ) ) . toBe ( true ) ;
1072+ expect ( fs . existsSync ( peerLinkPath ( "codex" ) ) ) . toBe ( true ) ;
1073+ expect ( warnMessages ) . toContainEqual (
1074+ expect . stringContaining ( 'Could not repair openclaw peer link for "broken"' ) ,
1075+ ) ;
1076+ } ) ;
1077+
9971078 it ( "refreshes legacy npm install records before skipping unchanged artifacts" , async ( ) => {
9981079 const installPath = createInstalledPackageDir ( {
9991080 name : "@martian-engineering/lossless-claw" ,
0 commit comments