@@ -444,7 +444,7 @@ describe("installPluginFromNpmSpec e2e", () => {
444444 ) . resolves . toBeTruthy ( ) ;
445445 } ) ;
446446
447- it ( "does not attribute repaired pre-existing peer dependencies to later installs" , async ( ) => {
447+ it ( "scans repaired pre-existing peer dependencies during later installs" , async ( ) => {
448448 const rootDir = await makeTempDir ( "npm-plugin-repaired-peer-scan-e2e" ) ;
449449 const npmRoot = path . join ( rootDir , "managed-npm" ) ;
450450 const pluginWithRuntimePeer = `existing-peer-plugin-${ crypto . randomUUID ( ) . replace ( / - / g, "" ) . slice ( 0 , 12 ) } ` ;
@@ -531,13 +531,27 @@ describe("installPluginFromNpmSpec e2e", () => {
531531 logger : { info : ( ) => { } , warn : ( ) => { } } ,
532532 timeoutMs : 120_000 ,
533533 } ) ;
534- if ( ! later . ok ) {
535- throw new Error ( later . error ) ;
534+ expect ( later . ok ) . toBe ( false ) ;
535+ if ( later . ok ) {
536+ throw new Error ( "expected repaired peer dependency scan to block installation" ) ;
536537 }
538+ expect ( later . error ) . toContain ( "Dynamic code execution detected" ) ;
537539
540+ await expect (
541+ fs . lstat ( path . join ( npmRoot , "node_modules" , laterPlugin , "package.json" ) ) ,
542+ ) . rejects . toHaveProperty ( "code" , "ENOENT" ) ;
538543 await expect (
539544 fs . lstat ( path . join ( npmRoot , "node_modules" , runtimePeer , "package.json" ) ) ,
540- ) . resolves . toBeTruthy ( ) ;
545+ ) . rejects . toHaveProperty ( "code" , "ENOENT" ) ;
546+ const rootManifest = JSON . parse (
547+ await fs . readFile ( path . join ( npmRoot , "package.json" ) , "utf8" ) ,
548+ ) as {
549+ dependencies ?: Record < string , string > ;
550+ openclaw ?: { managedPeerDependencies ?: string [ ] } ;
551+ } ;
552+ expect ( rootManifest . dependencies ?. [ laterPlugin ] ) . toBeUndefined ( ) ;
553+ expect ( rootManifest . dependencies ?. [ runtimePeer ] ) . toBeUndefined ( ) ;
554+ expect ( rootManifest . openclaw ?. managedPeerDependencies ?? [ ] ) . not . toContain ( runtimePeer ) ;
541555 } ) ;
542556
543557 it ( "bounds peer dependency discovery across repeated nested package realpaths" , async ( ) => {
@@ -680,6 +694,119 @@ describe("installPluginFromNpmSpec e2e", () => {
680694 ) . rejects . toHaveProperty ( "code" , "ENOENT" ) ;
681695 } ) ;
682696
697+ it ( "does not take ownership of an existing root dependency observed as a peer" , async ( ) => {
698+ const rootDir = await makeTempDir ( "npm-plugin-peer-existing-root-e2e" ) ;
699+ const npmRoot = path . join ( rootDir , "managed-npm" ) ;
700+ const existingRootDependency = `existing-root-${ crypto . randomUUID ( ) . replace ( / - / g, "" ) . slice ( 0 , 12 ) } ` ;
701+ const blockedPlugin = `blocked-plugin-${ crypto . randomUUID ( ) . replace ( / - / g, "" ) . slice ( 0 , 12 ) } ` ;
702+ const runtimePeer = `runtime-peer-${ crypto . randomUUID ( ) . replace ( / - / g, "" ) . slice ( 0 , 12 ) } ` ;
703+ const registry = await startStaticRegistry ( [
704+ {
705+ packageName : existingRootDependency ,
706+ latest : "1.0.0" ,
707+ versions : [
708+ await packPlugin ( {
709+ packageName : existingRootDependency ,
710+ pluginId : existingRootDependency ,
711+ version : "1.0.0" ,
712+ rootDir,
713+ } ) ,
714+ ] ,
715+ } ,
716+ {
717+ packageName : blockedPlugin ,
718+ latest : "1.0.0" ,
719+ versions : [
720+ await packPlugin ( {
721+ packageName : blockedPlugin ,
722+ peerDependencies : {
723+ [ existingRootDependency ] : "^1.0.0" ,
724+ [ runtimePeer ] : "^1.0.0" ,
725+ } ,
726+ peerDependenciesMeta : { } ,
727+ pluginId : blockedPlugin ,
728+ version : "1.0.0" ,
729+ rootDir,
730+ } ) ,
731+ ] ,
732+ } ,
733+ {
734+ packageName : runtimePeer ,
735+ latest : "1.0.0" ,
736+ versions : [
737+ await packPlugin ( {
738+ indexJs : "eval('1');\n" ,
739+ packageName : runtimePeer ,
740+ pluginId : runtimePeer ,
741+ version : "1.0.0" ,
742+ rootDir,
743+ } ) ,
744+ ] ,
745+ } ,
746+ ] ) ;
747+ process . env . NPM_CONFIG_REGISTRY = registry ;
748+ process . env . npm_config_registry = registry ;
749+
750+ await fs . mkdir ( npmRoot , { recursive : true } ) ;
751+ await fs . writeFile (
752+ path . join ( npmRoot , "package.json" ) ,
753+ `${ JSON . stringify (
754+ {
755+ private : true ,
756+ dependencies : { [ existingRootDependency ] : "1.0.0" } ,
757+ } ,
758+ null ,
759+ 2 ,
760+ ) } \n`,
761+ "utf8" ,
762+ ) ;
763+ await execFileAsync (
764+ "npm" ,
765+ [
766+ "install" ,
767+ "--omit=dev" ,
768+ "--omit=peer" ,
769+ "--legacy-peer-deps" ,
770+ "--loglevel=error" ,
771+ "--ignore-scripts" ,
772+ "--no-audit" ,
773+ "--no-fund" ,
774+ ] ,
775+ { cwd : npmRoot } ,
776+ ) ;
777+
778+ const result = await installPluginFromNpmSpec ( {
779+ spec : `${ blockedPlugin } @1.0.0` ,
780+ npmDir : npmRoot ,
781+ logger : { info : ( ) => { } , warn : ( ) => { } } ,
782+ timeoutMs : 120_000 ,
783+ } ) ;
784+
785+ expect ( result . ok ) . toBe ( false ) ;
786+ const rootManifest = JSON . parse (
787+ await fs . readFile ( path . join ( npmRoot , "package.json" ) , "utf8" ) ,
788+ ) as {
789+ dependencies ?: Record < string , string > ;
790+ openclaw ?: { managedPeerDependencies ?: string [ ] } ;
791+ } ;
792+ expect ( rootManifest . dependencies ?. [ existingRootDependency ] ) . toBe ( "1.0.0" ) ;
793+ expect ( rootManifest . dependencies ?. [ blockedPlugin ] ) . toBeUndefined ( ) ;
794+ expect ( rootManifest . dependencies ?. [ runtimePeer ] ) . toBeUndefined ( ) ;
795+ expect ( rootManifest . openclaw ?. managedPeerDependencies ?? [ ] ) . not . toContain (
796+ existingRootDependency ,
797+ ) ;
798+ expect ( rootManifest . openclaw ?. managedPeerDependencies ?? [ ] ) . not . toContain ( runtimePeer ) ;
799+ await expect (
800+ fs . lstat ( path . join ( npmRoot , "node_modules" , existingRootDependency , "package.json" ) ) ,
801+ ) . resolves . toBeTruthy ( ) ;
802+ await expect (
803+ fs . lstat ( path . join ( npmRoot , "node_modules" , blockedPlugin , "package.json" ) ) ,
804+ ) . rejects . toHaveProperty ( "code" , "ENOENT" ) ;
805+ await expect (
806+ fs . lstat ( path . join ( npmRoot , "node_modules" , runtimePeer , "package.json" ) ) ,
807+ ) . rejects . toHaveProperty ( "code" , "ENOENT" ) ;
808+ } ) ;
809+
683810 it ( "scrubs host peers when installing beside an existing host-peer plugin" , async ( ) => {
684811 const rootDir = await makeTempDir ( "npm-plugin-sibling-peer-e2e" ) ;
685812 const npmRoot = path . join ( rootDir , "managed-npm" ) ;
0 commit comments