1+ import { existsSync } from 'node:fs' ;
2+ import path from 'node:path' ;
13import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest' ;
24import { ServerManager } from './server-manager' ;
35
@@ -6,6 +8,15 @@ vi.mock('../server/mcp-manager', () => ({
68 MCPManager : vi . fn ( ) ,
79} ) ) ;
810
11+ // Mock node:fs
12+ vi . mock ( 'node:fs' , async ( importOriginal ) => {
13+ const actual = await importOriginal < typeof import ( 'node:fs' ) > ( ) ;
14+ return {
15+ ...actual ,
16+ existsSync : vi . fn ( ) ,
17+ } ;
18+ } ) ;
19+
920// Mock logger
1021vi . mock ( '../server/utils' , async ( importOriginal ) => ( {
1122 ...( await importOriginal ( ) ) ,
@@ -222,4 +233,257 @@ describe('ServerManager', () => {
222233 await expect ( serverManager . stop ( ) ) . resolves . toBeUndefined ( ) ;
223234 } ) ;
224235 } ) ;
236+
237+ describe ( 'resolveProjectRoot' , ( ) => {
238+ let originalCwd : string ;
239+ let originalArgv : string [ ] ;
240+ const mockedExistsSync = vi . mocked ( existsSync ) ;
241+
242+ beforeEach ( ( ) => {
243+ originalCwd = process . cwd ( ) ;
244+ originalArgv = [ ...process . argv ] ;
245+ mockedExistsSync . mockReset ( ) ;
246+ } ) ;
247+
248+ afterEach ( ( ) => {
249+ vi . spyOn ( process , 'cwd' ) . mockRestore ( ) ;
250+ process . argv = originalArgv ;
251+ } ) ;
252+
253+ /**
254+ * Helper to invoke private resolveProjectRoot method
255+ */
256+ function invokeResolveProjectRoot ( manager : ServerManager ) : string {
257+ return ( manager as any ) . resolveProjectRoot ( ) ;
258+ }
259+
260+ describe ( 'when cwd is a valid project root' , ( ) => {
261+ it ( 'should return cwd when swarm.config.json exists' , ( ) => {
262+ const projectDir = '/users/dev/my-project' ;
263+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( projectDir ) ;
264+
265+ mockedExistsSync . mockImplementation ( ( filePath ) => {
266+ return filePath === path . join ( projectDir , 'swarm.config.json' ) ;
267+ } ) ;
268+
269+ const result = invokeResolveProjectRoot ( serverManager ) ;
270+
271+ expect ( result ) . toBe ( projectDir ) ;
272+ } ) ;
273+
274+ it ( 'should return cwd when package.json exists' , ( ) => {
275+ const projectDir = '/users/dev/my-project' ;
276+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( projectDir ) ;
277+
278+ mockedExistsSync . mockImplementation ( ( filePath ) => {
279+ return filePath === path . join ( projectDir , 'package.json' ) ;
280+ } ) ;
281+
282+ const result = invokeResolveProjectRoot ( serverManager ) ;
283+
284+ expect ( result ) . toBe ( projectDir ) ;
285+ } ) ;
286+
287+ it ( 'should prioritize swarm.config.json check (short-circuit)' , ( ) => {
288+ const projectDir = '/users/dev/my-project' ;
289+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( projectDir ) ;
290+
291+ mockedExistsSync . mockImplementation ( ( filePath ) => {
292+ // Both files exist
293+ return (
294+ filePath === path . join ( projectDir , 'swarm.config.json' ) ||
295+ filePath === path . join ( projectDir , 'package.json' )
296+ ) ;
297+ } ) ;
298+
299+ const result = invokeResolveProjectRoot ( serverManager ) ;
300+
301+ expect ( result ) . toBe ( projectDir ) ;
302+ } ) ;
303+ } ) ;
304+
305+ describe ( 'when cwd is not a project root (binary path fallback)' , ( ) => {
306+ it ( 'should resolve from node_modules parent when binary is locally installed' , ( ) => {
307+ const cwdDir = '/tmp/random-dir' ;
308+ const projectRoot = '/users/dev/my-project' ;
309+ const binaryPath = `${ projectRoot } /node_modules/.bin/swarm` ;
310+
311+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
312+ process . argv = [ 'node' , binaryPath ] ;
313+
314+ mockedExistsSync . mockImplementation ( ( filePath ) => {
315+ // cwd has no project files
316+ if (
317+ filePath === path . join ( cwdDir , 'swarm.config.json' ) ||
318+ filePath === path . join ( cwdDir , 'package.json' )
319+ ) {
320+ return false ;
321+ }
322+ // project root has swarm.config.json
323+ return filePath === path . join ( projectRoot , 'swarm.config.json' ) ;
324+ } ) ;
325+
326+ const result = invokeResolveProjectRoot ( serverManager ) ;
327+
328+ expect ( result ) . toBe ( projectRoot ) ;
329+ } ) ;
330+
331+ it ( 'should resolve from deep node_modules path' , ( ) => {
332+ const cwdDir = '/tmp/random-dir' ;
333+ const projectRoot = '/users/dev/monorepo/packages/my-app' ;
334+ const binaryPath = `${ projectRoot } /node_modules/@ingenyus/swarm/bin/swarm` ;
335+
336+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
337+ process . argv = [ 'node' , binaryPath ] ;
338+
339+ mockedExistsSync . mockImplementation ( ( filePath ) => {
340+ // cwd has no project files
341+ if (
342+ filePath === path . join ( cwdDir , 'swarm.config.json' ) ||
343+ filePath === path . join ( cwdDir , 'package.json' )
344+ ) {
345+ return false ;
346+ }
347+ // project root has package.json
348+ return filePath === path . join ( projectRoot , 'package.json' ) ;
349+ } ) ;
350+
351+ const result = invokeResolveProjectRoot ( serverManager ) ;
352+
353+ expect ( result ) . toBe ( projectRoot ) ;
354+ } ) ;
355+
356+ it ( 'should use lastIndexOf to find the correct node_modules (nested case)' , ( ) => {
357+ const cwdDir = '/tmp/random-dir' ;
358+ // Simulates a monorepo where binary is in a nested package's node_modules
359+ const projectRoot = '/users/dev/monorepo/packages/app' ;
360+ const binaryPath = `${ projectRoot } /node_modules/.pnpm/@ingenyus+swarm@1.0.0/node_modules/@ingenyus/swarm/bin/swarm` ;
361+
362+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
363+ process . argv = [ 'node' , binaryPath ] ;
364+
365+ mockedExistsSync . mockImplementation ( ( filePath ) => {
366+ if (
367+ filePath === path . join ( cwdDir , 'swarm.config.json' ) ||
368+ filePath === path . join ( cwdDir , 'package.json' )
369+ ) {
370+ return false ;
371+ }
372+ // The project root (before the last node_modules) has swarm.config.json
373+ return filePath === path . join ( projectRoot , 'swarm.config.json' ) ;
374+ } ) ;
375+
376+ const result = invokeResolveProjectRoot ( serverManager ) ;
377+
378+ // Should find the parent of the LAST node_modules (pnpm structure)
379+ // In this case: /users/dev/monorepo/packages/app/node_modules/.pnpm/@ingenyus +swarm@1.0.0
380+ // But that's not a valid project root, so it falls back to cwd
381+ expect ( result ) . toBe ( cwdDir ) ;
382+ } ) ;
383+ } ) ;
384+
385+ describe ( 'when binary path does not contain node_modules' , ( ) => {
386+ it ( 'should fall back to cwd when binary is run directly (development)' , ( ) => {
387+ const cwdDir = '/users/dev/swarm' ;
388+ const binaryPath = '/users/dev/swarm/bin/swarm' ;
389+
390+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
391+ process . argv = [ 'node' , binaryPath ] ;
392+
393+ mockedExistsSync . mockReturnValue ( false ) ;
394+
395+ const result = invokeResolveProjectRoot ( serverManager ) ;
396+
397+ expect ( result ) . toBe ( cwdDir ) ;
398+ } ) ;
399+
400+ it ( 'should fall back to cwd when using npx remotely' , ( ) => {
401+ const cwdDir = '/users/dev/my-project' ;
402+ // npx downloads to a temp cache location without node_modules in project path
403+ const binaryPath =
404+ '/home/user/.npm/_npx/abc123/node_modules/@ingenyus/swarm/bin/swarm' ;
405+
406+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
407+ process . argv = [ 'node' , binaryPath ] ;
408+
409+ mockedExistsSync . mockImplementation ( ( filePath ) => {
410+ // cwd has no project files
411+ if (
412+ filePath === path . join ( cwdDir , 'swarm.config.json' ) ||
413+ filePath === path . join ( cwdDir , 'package.json' )
414+ ) {
415+ return false ;
416+ }
417+ // npx cache parent is not a valid project root
418+ return false ;
419+ } ) ;
420+
421+ const result = invokeResolveProjectRoot ( serverManager ) ;
422+
423+ expect ( result ) . toBe ( cwdDir ) ;
424+ } ) ;
425+ } ) ;
426+
427+ describe ( 'when binary path candidate is not a valid project root' , ( ) => {
428+ it ( 'should fall back to cwd when node_modules parent has no project files' , ( ) => {
429+ const cwdDir = '/users/dev/working-dir' ;
430+ const notProjectRoot = '/some/random/location' ;
431+ const binaryPath = `${ notProjectRoot } /node_modules/@ingenyus/swarm/bin/swarm` ;
432+
433+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
434+ process . argv = [ 'node' , binaryPath ] ;
435+
436+ // Neither cwd nor the node_modules parent have project files
437+ mockedExistsSync . mockReturnValue ( false ) ;
438+
439+ const result = invokeResolveProjectRoot ( serverManager ) ;
440+
441+ expect ( result ) . toBe ( cwdDir ) ;
442+ } ) ;
443+ } ) ;
444+
445+ describe ( 'edge cases' , ( ) => {
446+ it ( 'should handle missing argv[1]' , ( ) => {
447+ const cwdDir = '/users/dev/my-project' ;
448+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
449+ process . argv = [ 'node' ] ; // No argv[1]
450+
451+ mockedExistsSync . mockReturnValue ( false ) ;
452+
453+ const result = invokeResolveProjectRoot ( serverManager ) ;
454+
455+ expect ( result ) . toBe ( cwdDir ) ;
456+ } ) ;
457+
458+ it ( 'should handle empty argv' , ( ) => {
459+ const cwdDir = '/users/dev/my-project' ;
460+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
461+ process . argv = [ ] ;
462+
463+ mockedExistsSync . mockReturnValue ( false ) ;
464+
465+ const result = invokeResolveProjectRoot ( serverManager ) ;
466+
467+ expect ( result ) . toBe ( cwdDir ) ;
468+ } ) ;
469+
470+ it ( 'should handle Windows-style paths' , ( ) => {
471+ const cwdDir = 'C:\\Users\\dev\\my-project' ;
472+ vi . spyOn ( process , 'cwd' ) . mockReturnValue ( cwdDir ) ;
473+
474+ mockedExistsSync . mockImplementation ( ( filePath ) => {
475+ // Normalize for comparison (path.join handles this)
476+ const normalizedPath = String ( filePath ) ;
477+ return (
478+ normalizedPath === path . join ( cwdDir , 'swarm.config.json' ) ||
479+ normalizedPath === path . join ( cwdDir , 'package.json' )
480+ ) ;
481+ } ) ;
482+
483+ const result = invokeResolveProjectRoot ( serverManager ) ;
484+
485+ expect ( result ) . toBe ( cwdDir ) ;
486+ } ) ;
487+ } ) ;
488+ } ) ;
225489} ) ;
0 commit comments