11import * as path from 'path' ;
22import which from 'which' ;
33import { getPackageManager } from '../utils' ;
4+ import * as shellUtils from '../utils/shell-utils' ;
45
56jest . mock ( 'which' ) ;
7+ jest . mock ( '../utils/shell-utils' ) ;
68
79describe ( 'packageManager tests' , ( ) => {
810 const baseDirectory = path . join ( __dirname , 'testFiles' ) ;
911 const which_mock = which as jest . Mocked < typeof which > ;
10-
11- beforeEach ( ( ) => jest . clearAllMocks ( ) ) ;
12+ const shellUtils_mock = shellUtils as jest . Mocked < typeof shellUtils > ;
13+
14+ beforeEach ( ( ) => {
15+ jest . resetAllMocks ( ) ;
16+ // Default to returning undefined (executable not found)
17+ // Tests that need specific executables should set them explicitly
18+ ( which_mock . sync as jest . Mock ) . mockReturnValue ( undefined ) ;
19+ // Mock yarn --version to return a valid version
20+ shellUtils_mock . execWithOutputAsString . mockResolvedValue ( '4.0.0' ) ;
21+ } ) ;
1222
1323 test ( 'returns null when no package.json found' , async ( ) => {
1424 const testDirectory = path . join ( baseDirectory , 'packageManager-null' ) ;
@@ -19,31 +29,36 @@ describe('packageManager tests', () => {
1929 } ) ;
2030
2131 test ( 'detects yarn2 correctly' , async ( ) => {
22- which_mock . sync . mockReturnValue ( '/path/to/yarn' ) ;
32+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
33+ if ( executable === 'yarn' ) return '/path/to/yarn' ;
34+ return undefined ;
35+ } ) ;
2336
2437 const testDirectory = path . join ( baseDirectory , 'packageManager-yarn2' ) ;
2538
2639 const packageManager = await getPackageManager ( testDirectory ) ;
2740
28- expect ( which_mock . sync ) . toBeCalledTimes ( 1 ) ;
2941 expect ( packageManager ) . toBeDefined ( ) ;
3042 expect ( packageManager ?. packageManager ) . toEqual ( 'yarn' ) ;
3143 expect ( packageManager ?. version ?. major ) . toBeGreaterThanOrEqual ( 2 ) ;
3244 } ) ;
3345
3446 test ( 'detects yarn correctly' , async ( ) => {
35- which_mock . sync . mockReturnValue ( '/path/to/yarn' ) ;
47+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
48+ if ( executable === 'yarn' ) return '/path/to/yarn' ;
49+ return undefined ;
50+ } ) ;
3651
3752 const testDirectory = path . join ( baseDirectory , 'packageManager-yarn' ) ;
3853
3954 const packageManager = await getPackageManager ( testDirectory ) ;
4055
41- expect ( which_mock . sync ) . toBeCalledTimes ( 1 ) ;
4256 expect ( packageManager ) . toBeDefined ( ) ;
4357 expect ( packageManager ! . packageManager ) . toEqual ( 'yarn' ) ;
4458 } ) ;
4559
4660 test ( 'detects npm correctly' , async ( ) => {
61+ // No executables in PATH - should fall back to npm via lock file
4762 const testDirectory = path . join ( baseDirectory , 'packageManager-npm' ) ;
4863
4964 const packageManager = await getPackageManager ( testDirectory ) ;
@@ -53,26 +68,121 @@ describe('packageManager tests', () => {
5368 } ) ;
5469
5570 test ( 'detects yarn fallback correctly when yarn in path' , async ( ) => {
56- which_mock . sync . mockReturnValue ( '/path/to/yarn' ) ;
71+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
72+ if ( executable === 'yarn' ) return '/path/to/yarn' ;
73+ return undefined ;
74+ } ) ;
5775
5876 const testDirectory = path . join ( baseDirectory , 'packageManager-fallback' ) ;
5977
6078 const packageManager = await getPackageManager ( testDirectory ) ;
6179
62- expect ( which_mock . sync ) . toBeCalledTimes ( 1 ) ;
6380 expect ( packageManager ) . toBeDefined ( ) ;
6481 expect ( packageManager ! . packageManager ) . toEqual ( 'yarn' ) ;
6582 } ) ;
6683
6784 test ( 'detects npm fallback correctly when yarn is not in path' , async ( ) => {
68- ( which_mock . sync as any ) . mockReturnValue ( undefined ) ;
69-
85+ // No executables in PATH (already set in beforeEach)
7086 const testDirectory = path . join ( baseDirectory , 'packageManager-fallback' ) ;
7187
7288 const packageManager = await getPackageManager ( testDirectory ) ;
7389
74- expect ( which_mock . sync ) . toBeCalledTimes ( 2 ) ;
7590 expect ( packageManager ) . toBeDefined ( ) ;
7691 expect ( packageManager ! . packageManager ) . toEqual ( 'npm' ) ;
7792 } ) ;
93+
94+ describe ( 'packageManager field detection (corepack convention)' , ( ) => {
95+ test ( 'detects pnpm from packageManager field in package.json' , async ( ) => {
96+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
97+ if ( executable === 'pnpm' ) return '/path/to/pnpm' ;
98+ return undefined ;
99+ } ) ;
100+
101+ const testDirectory = path . join ( baseDirectory , 'packageManager-pnpm-field' ) ;
102+
103+ const packageManager = await getPackageManager ( testDirectory ) ;
104+
105+ expect ( packageManager ) . toBeDefined ( ) ;
106+ expect ( packageManager ! . packageManager ) . toEqual ( 'pnpm' ) ;
107+ } ) ;
108+
109+ test ( 'detects npm from packageManager field in package.json' , async ( ) => {
110+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
111+ if ( executable === 'npm' ) return '/path/to/npm' ;
112+ return undefined ;
113+ } ) ;
114+
115+ const testDirectory = path . join ( baseDirectory , 'packageManager-npm-field' ) ;
116+
117+ const packageManager = await getPackageManager ( testDirectory ) ;
118+
119+ expect ( packageManager ) . toBeDefined ( ) ;
120+ expect ( packageManager ! . packageManager ) . toEqual ( 'npm' ) ;
121+ } ) ;
122+
123+ test ( 'detects yarn from packageManager field in package.json' , async ( ) => {
124+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
125+ if ( executable === 'yarn' ) return '/path/to/yarn' ;
126+ return undefined ;
127+ } ) ;
128+
129+ const testDirectory = path . join ( baseDirectory , 'packageManager-yarn-field' ) ;
130+
131+ const packageManager = await getPackageManager ( testDirectory ) ;
132+
133+ expect ( packageManager ) . toBeDefined ( ) ;
134+ expect ( packageManager ! . packageManager ) . toEqual ( 'yarn' ) ;
135+ } ) ;
136+
137+ test ( 'detects packageManager field from parent directory in hierarchy' , async ( ) => {
138+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
139+ if ( executable === 'pnpm' ) return '/path/to/pnpm' ;
140+ return undefined ;
141+ } ) ;
142+
143+ // subdir has package.json but no packageManager field
144+ // parent directory has packageManager: pnpm@10.17.0
145+ const testDirectory = path . join ( baseDirectory , 'packageManager-hierarchy' , 'subdir' ) ;
146+
147+ const packageManager = await getPackageManager ( testDirectory ) ;
148+
149+ expect ( packageManager ) . toBeDefined ( ) ;
150+ expect ( packageManager ! . packageManager ) . toEqual ( 'pnpm' ) ;
151+ } ) ;
152+
153+ test ( 'packageManager field takes precedence over lock files' , async ( ) => {
154+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
155+ if ( executable === 'pnpm' ) return '/path/to/pnpm' ;
156+ if ( executable === 'yarn' ) return '/path/to/yarn' ;
157+ return undefined ;
158+ } ) ;
159+
160+ // Directory has yarn.lock but package.json has packageManager: pnpm@10.17.0
161+ const testDirectory = path . join ( baseDirectory , 'packageManager-field-with-lockfile' ) ;
162+
163+ const packageManager = await getPackageManager ( testDirectory ) ;
164+
165+ expect ( packageManager ) . toBeDefined ( ) ;
166+ expect ( packageManager ! . packageManager ) . toEqual ( 'pnpm' ) ;
167+ } ) ;
168+
169+ test ( 'falls back to lock file detection when packageManager executable not found' , async ( ) => {
170+ ( which_mock . sync as jest . Mock ) . mockImplementation ( ( executable : string ) => {
171+ // pnpm not in PATH, but yarn is
172+ if ( executable === 'pnpm' ) return undefined ;
173+ if ( executable === 'yarn' ) return '/path/to/yarn' ;
174+ return undefined ;
175+ } ) ;
176+
177+ // Parent directory has packageManager: pnpm but pnpm not in PATH
178+ // Subdirectory has yarn.lock (no packageManager field to avoid corepack blocking yarn)
179+ // yarn is in PATH, so should fall back to yarn via lock file
180+ const testDirectory = path . join ( baseDirectory , 'packageManager-hierarchy-fallback' , 'subdir' ) ;
181+
182+ const packageManager = await getPackageManager ( testDirectory ) ;
183+
184+ expect ( packageManager ) . toBeDefined ( ) ;
185+ expect ( packageManager ! . packageManager ) . toEqual ( 'yarn' ) ;
186+ } ) ;
187+ } ) ;
78188} ) ;
0 commit comments