@@ -828,5 +828,89 @@ describe('package-environment', () => {
828828 expect ( result . message ) . toBe ( 'Missing lockfile' )
829829 }
830830 } )
831+
832+ it ( 'detects agent from packageManager field (lines 324-332)' , async ( ) => {
833+ // packageManager: "pnpm@8.15.7" → agent='pnpm' inferred from the field
834+ // before any lockfile lookup.
835+ mockFindUp . mockImplementation ( async files => {
836+ if ( Array . isArray ( files ) && files . includes ( 'pnpm-lock.yaml' ) ) {
837+ return '/project/pnpm-lock.yaml'
838+ }
839+ if ( files === 'package.json' ) {
840+ return '/project/package.json'
841+ }
842+ return undefined
843+ } )
844+ mockExistsSync . mockReturnValue ( true )
845+ mockWhichBin . mockResolvedValue ( '/usr/local/bin/pnpm' )
846+ mockReadPackageJson . mockResolvedValue ( {
847+ name : 'test-project' ,
848+ version : '1.0.0' ,
849+ packageManager : 'pnpm@8.15.7' ,
850+ } )
851+
852+ const result = await detectAndValidatePackageEnvironment ( '/project' )
853+
854+ expect ( result . ok ) . toBe ( true )
855+ if ( result . ok ) {
856+ expect ( result . data . agent ) . toBe ( 'pnpm' )
857+ }
858+ } )
859+
860+ it ( 'falls back to npm when packageManager has no @ separator' , async ( ) => {
861+ mockFindUp . mockImplementation ( async files => {
862+ if ( Array . isArray ( files ) && files . includes ( 'package-lock.json' ) ) {
863+ return '/project/package-lock.json'
864+ }
865+ if ( files === 'package.json' ) {
866+ return '/project/package.json'
867+ }
868+ return undefined
869+ } )
870+ mockExistsSync . mockReturnValue ( true )
871+ mockWhichBin . mockResolvedValue ( '/usr/local/bin/npm' )
872+ mockReadPackageJson . mockResolvedValue ( {
873+ name : 'test-project' ,
874+ version : '1.0.0' ,
875+ // No '@' → atSignIndex < 0 → falls through to lockfile inference.
876+ packageManager : 'invalid-format' ,
877+ } )
878+
879+ const result = await detectAndValidatePackageEnvironment ( '/project' )
880+
881+ // Falls through to LOCKS lookup (package-lock.json → npm).
882+ expect ( result . ok ) . toBe ( true )
883+ if ( result . ok ) {
884+ expect ( result . data . agent ) . toBe ( 'npm' )
885+ }
886+ } )
887+
888+ it ( 'detects yarn-berry when yarn-classic agent has major > 1 (lines 348-349)' , async ( ) => {
889+ mockFindUp . mockImplementation ( async files => {
890+ if ( Array . isArray ( files ) && files . includes ( 'yarn.lock' ) ) {
891+ return '/project/yarn.lock'
892+ }
893+ if ( files === 'package.json' ) {
894+ return '/project/package.json'
895+ }
896+ return undefined
897+ } )
898+ mockExistsSync . mockReturnValue ( true )
899+ mockWhichBin . mockResolvedValue ( '/usr/local/bin/yarn' )
900+ mockReadPackageJson . mockResolvedValue ( {
901+ name : 'test-project' ,
902+ version : '1.0.0' ,
903+ } )
904+ // yarn version 4.x → coerced major 4 > 1 → upgrades classic to berry.
905+ mockSpawn . mockResolvedValue ( { stdout : '4.5.0' , stderr : '' , code : 0 } )
906+
907+ const result = await detectAndValidatePackageEnvironment ( '/project' )
908+
909+ expect ( result . ok ) . toBe ( true )
910+ if ( result . ok ) {
911+ // Agent name uses '/' as the separator between flavor variants.
912+ expect ( result . data . agent ) . toBe ( 'yarn/berry' )
913+ }
914+ } )
831915 } )
832916} )
0 commit comments