2025-05-22 10:47:21 -07:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-07-22 17:18:57 -07:00
import { describe , it , expect , beforeEach , afterEach } from 'vitest' ;
2025-08-25 22:11:27 +02:00
import fsPromises from 'node:fs/promises' ;
import * as nodePath from 'node:path' ;
import * as os from 'node:os' ;
2025-05-22 10:47:21 -07:00
import { getFolderStructure } from './getFolderStructure.js' ;
2025-06-13 14:26:31 -04:00
import { FileDiscoveryService } from '../services/fileDiscoveryService.js' ;
2025-08-25 22:11:27 +02:00
import * as path from 'node:path' ;
2025-10-14 02:31:39 +09:00
import { GEMINI_DIR } from './paths.js' ;
2025-05-22 10:47:21 -07:00
2025-07-21 22:21:37 -07:00
describe ( 'getFolderStructure' , ( ) = > {
let testRootDir : string ;
2025-05-22 10:47:21 -07:00
2025-07-22 17:18:57 -07:00
async function createEmptyDir ( . . . pathSegments : string [ ] ) {
2025-07-21 22:21:37 -07:00
const fullPath = path . join ( testRootDir , . . . pathSegments ) ;
await fsPromises . mkdir ( fullPath , { recursive : true } ) ;
2025-07-22 17:18:57 -07:00
}
2025-05-22 10:47:21 -07:00
2025-07-22 17:18:57 -07:00
async function createTestFile ( . . . pathSegments : string [ ] ) {
2025-07-21 22:21:37 -07:00
const fullPath = path . join ( testRootDir , . . . pathSegments ) ;
await fsPromises . mkdir ( path . dirname ( fullPath ) , { recursive : true } ) ;
await fsPromises . writeFile ( fullPath , '' ) ;
return fullPath ;
2025-07-22 17:18:57 -07:00
}
2025-05-22 10:47:21 -07:00
2025-07-21 22:21:37 -07:00
beforeEach ( async ( ) = > {
testRootDir = await fsPromises . mkdtemp (
path . join ( os . tmpdir ( ) , 'folder-structure-test-' ) ,
2025-05-22 10:47:21 -07:00
) ;
} ) ;
2025-07-21 22:21:37 -07:00
afterEach ( async ( ) = > {
await fsPromises . rm ( testRootDir , { recursive : true , force : true } ) ;
2025-05-22 10:47:21 -07:00
} ) ;
it ( 'should return basic folder structure' , async ( ) = > {
2025-07-21 22:21:37 -07:00
await createTestFile ( 'fileA1.ts' ) ;
await createTestFile ( 'fileA2.js' ) ;
await createTestFile ( 'subfolderB' , 'fileB1.md' ) ;
const structure = await getFolderStructure ( testRootDir ) ;
expect ( structure . trim ( ) ) . toBe (
`
2025-05-22 10:47:21 -07:00
Showing up to 200 items (files + folders).
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
2025-05-22 10:47:21 -07:00
├───fileA1.ts
├───fileA2.js
2025-07-21 22:21:37 -07:00
└───subfolderB ${ path . sep }
2025-05-22 10:47:21 -07:00
└───fileB1.md
2025-07-21 22:21:37 -07:00
` . trim ( ) ,
) ;
2025-05-22 10:47:21 -07:00
} ) ;
it ( 'should handle an empty folder' , async ( ) = > {
2025-07-21 22:21:37 -07:00
const structure = await getFolderStructure ( testRootDir ) ;
expect ( structure . trim ( ) ) . toBe (
`
2025-05-22 10:47:21 -07:00
Showing up to 200 items (files + folders).
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
`
. trim ( )
. trim ( ) ,
) ;
2025-05-22 10:47:21 -07:00
} ) ;
it ( 'should ignore folders specified in ignoredFolders (default)' , async ( ) = > {
2025-07-21 22:21:37 -07:00
await createTestFile ( '.hiddenfile' ) ;
await createTestFile ( 'file1.txt' ) ;
await createEmptyDir ( 'emptyFolder' ) ;
await createTestFile ( 'node_modules' , 'somepackage' , 'index.js' ) ;
await createTestFile ( 'subfolderA' , 'fileA1.ts' ) ;
await createTestFile ( 'subfolderA' , 'fileA2.js' ) ;
await createTestFile ( 'subfolderA' , 'subfolderB' , 'fileB1.md' ) ;
const structure = await getFolderStructure ( testRootDir ) ;
expect ( structure . trim ( ) ) . toBe (
`
2025-05-22 10:47:21 -07:00
Showing up to 200 items (files + folders). Folders or files indicated with ... contain more items not shown, were ignored, or the display limit (200 items) was reached.
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
2025-05-22 10:47:21 -07:00
├───.hiddenfile
├───file1.txt
2025-07-21 22:21:37 -07:00
├───emptyFolder ${ path . sep }
├───node_modules ${ path . sep } ...
└───subfolderA ${ path . sep }
2025-05-22 10:47:21 -07:00
├───fileA1.ts
├───fileA2.js
2025-07-21 22:21:37 -07:00
└───subfolderB ${ path . sep }
2025-05-22 10:47:21 -07:00
└───fileB1.md
2025-07-21 22:21:37 -07:00
` . trim ( ) ,
) ;
2025-05-22 10:47:21 -07:00
} ) ;
it ( 'should ignore folders specified in custom ignoredFolders' , async ( ) = > {
2025-07-21 22:21:37 -07:00
await createTestFile ( '.hiddenfile' ) ;
await createTestFile ( 'file1.txt' ) ;
await createEmptyDir ( 'emptyFolder' ) ;
await createTestFile ( 'node_modules' , 'somepackage' , 'index.js' ) ;
await createTestFile ( 'subfolderA' , 'fileA1.ts' ) ;
const structure = await getFolderStructure ( testRootDir , {
2025-05-22 10:47:21 -07:00
ignoredFolders : new Set ( [ 'subfolderA' , 'node_modules' ] ) ,
} ) ;
const expected = `
Showing up to 200 items (files + folders). Folders or files indicated with ... contain more items not shown, were ignored, or the display limit (200 items) was reached.
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
2025-05-22 10:47:21 -07:00
├───.hiddenfile
├───file1.txt
2025-07-21 22:21:37 -07:00
├───emptyFolder ${ path . sep }
├───node_modules ${ path . sep } ...
└───subfolderA ${ path . sep } ...
2025-05-22 10:47:21 -07:00
` . trim ( ) ;
expect ( structure . trim ( ) ) . toBe ( expected ) ;
} ) ;
it ( 'should filter files by fileIncludePattern' , async ( ) = > {
2025-07-21 22:21:37 -07:00
await createTestFile ( 'fileA1.ts' ) ;
await createTestFile ( 'fileA2.js' ) ;
await createTestFile ( 'subfolderB' , 'fileB1.md' ) ;
const structure = await getFolderStructure ( testRootDir , {
2025-05-22 10:47:21 -07:00
fileIncludePattern : /\.ts$/ ,
} ) ;
const expected = `
Showing up to 200 items (files + folders).
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
2025-05-22 10:47:21 -07:00
├───fileA1.ts
2025-07-21 22:21:37 -07:00
└───subfolderB ${ path . sep }
2025-05-22 10:47:21 -07:00
` . trim ( ) ;
expect ( structure . trim ( ) ) . toBe ( expected ) ;
} ) ;
it ( 'should handle maxItems truncation for files within a folder' , async ( ) = > {
2025-07-21 22:21:37 -07:00
await createTestFile ( 'fileA1.ts' ) ;
await createTestFile ( 'fileA2.js' ) ;
await createTestFile ( 'subfolderB' , 'fileB1.md' ) ;
const structure = await getFolderStructure ( testRootDir , {
2025-05-22 10:47:21 -07:00
maxItems : 3 ,
} ) ;
const expected = `
Showing up to 3 items (files + folders).
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
2025-05-22 10:47:21 -07:00
├───fileA1.ts
├───fileA2.js
2025-07-21 22:21:37 -07:00
└───subfolderB ${ path . sep }
2025-05-22 10:47:21 -07:00
` . trim ( ) ;
expect ( structure . trim ( ) ) . toBe ( expected ) ;
} ) ;
it ( 'should handle maxItems truncation for subfolders' , async ( ) = > {
2025-07-21 22:21:37 -07:00
for ( let i = 0 ; i < 5 ; i ++ ) {
await createTestFile ( ` folder- ${ i } ` , 'child.txt' ) ;
}
const structure = await getFolderStructure ( testRootDir , {
2025-05-22 10:47:21 -07:00
maxItems : 4 ,
} ) ;
const expectedRevised = `
Showing up to 4 items (files + folders). Folders or files indicated with ... contain more items not shown, were ignored, or the display limit (4 items) was reached.
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
├───folder-0 ${ path . sep }
├───folder-1 ${ path . sep }
├───folder-2 ${ path . sep }
├───folder-3 ${ path . sep }
2025-05-22 10:47:21 -07:00
└───...
` . trim ( ) ;
expect ( structure . trim ( ) ) . toBe ( expectedRevised ) ;
} ) ;
it ( 'should handle maxItems that only allows the root folder itself' , async ( ) = > {
2025-07-21 22:21:37 -07:00
await createTestFile ( 'fileA1.ts' ) ;
await createTestFile ( 'fileA2.ts' ) ;
await createTestFile ( 'subfolderB' , 'fileB1.ts' ) ;
const structure = await getFolderStructure ( testRootDir , {
2025-05-22 10:47:21 -07:00
maxItems : 1 ,
} ) ;
2025-07-21 22:21:37 -07:00
const expected = `
2025-05-22 10:47:21 -07:00
Showing up to 1 items (files + folders). Folders or files indicated with ... contain more items not shown, were ignored, or the display limit (1 items) was reached.
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
2025-05-22 10:47:21 -07:00
├───fileA1.ts
├───...
└───...
` . trim ( ) ;
2025-07-21 22:21:37 -07:00
expect ( structure . trim ( ) ) . toBe ( expected ) ;
2025-05-22 10:47:21 -07:00
} ) ;
2025-07-21 22:21:37 -07:00
it ( 'should handle non-existent directory' , async ( ) = > {
const nonExistentPath = path . join ( testRootDir , 'non-existent' ) ;
const structure = await getFolderStructure ( nonExistentPath ) ;
2025-05-22 10:47:21 -07:00
expect ( structure ) . toContain (
2025-07-21 22:21:37 -07:00
` Error: Could not read directory " ${ nonExistentPath } ". Check path and permissions. ` ,
2025-05-22 10:47:21 -07:00
) ;
} ) ;
it ( 'should handle deep folder structure within limits' , async ( ) = > {
2025-07-21 22:21:37 -07:00
await createTestFile ( 'level1' , 'level2' , 'level3' , 'file.txt' ) ;
const structure = await getFolderStructure ( testRootDir , {
2025-05-22 10:47:21 -07:00
maxItems : 10 ,
} ) ;
const expected = `
Showing up to 10 items (files + folders).
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
└───level1 ${ path . sep }
└───level2 ${ path . sep }
└───level3 ${ path . sep }
2025-05-22 10:47:21 -07:00
└───file.txt
` . trim ( ) ;
expect ( structure . trim ( ) ) . toBe ( expected ) ;
} ) ;
it ( 'should truncate deep folder structure if maxItems is small' , async ( ) = > {
2025-07-21 22:21:37 -07:00
await createTestFile ( 'level1' , 'level2' , 'level3' , 'file.txt' ) ;
const structure = await getFolderStructure ( testRootDir , {
2025-05-22 10:47:21 -07:00
maxItems : 3 ,
} ) ;
const expected = `
Showing up to 3 items (files + folders).
2025-07-21 22:21:37 -07:00
${ testRootDir } ${ path . sep }
└───level1 ${ path . sep }
└───level2 ${ path . sep }
└───level3 ${ path . sep }
2025-05-22 10:47:21 -07:00
` . trim ( ) ;
expect ( structure . trim ( ) ) . toBe ( expected ) ;
} ) ;
2025-06-08 18:42:38 -07:00
2025-07-21 22:21:37 -07:00
describe ( 'with gitignore' , ( ) = > {
2025-07-22 17:18:57 -07:00
beforeEach ( async ( ) = > {
await fsPromises . mkdir ( path . join ( testRootDir , '.git' ) , {
recursive : true ,
} ) ;
2025-06-08 18:42:38 -07:00
} ) ;
2025-07-21 22:21:37 -07:00
it ( 'should ignore files and folders specified in .gitignore' , async ( ) = > {
await fsPromises . writeFile (
nodePath . join ( testRootDir , '.gitignore' ) ,
'ignored.txt\nnode_modules/\n.gemini/*\n!/.gemini/config.yaml' ,
) ;
await createTestFile ( 'file1.txt' ) ;
await createTestFile ( 'node_modules' , 'some-package' , 'index.js' ) ;
await createTestFile ( 'ignored.txt' ) ;
2025-10-14 02:31:39 +09:00
await createTestFile ( GEMINI_DIR , 'config.yaml' ) ;
await createTestFile ( GEMINI_DIR , 'logs.json' ) ;
2025-07-21 22:21:37 -07:00
const fileService = new FileDiscoveryService ( testRootDir ) ;
const structure = await getFolderStructure ( testRootDir , {
fileService ,
} ) ;
expect ( structure ) . not . toContain ( 'ignored.txt' ) ;
expect ( structure ) . toContain ( ` node_modules ${ path . sep } ... ` ) ;
expect ( structure ) . not . toContain ( 'logs.json' ) ;
expect ( structure ) . toContain ( 'config.yaml' ) ;
expect ( structure ) . toContain ( 'file1.txt' ) ;
2025-06-08 18:42:38 -07:00
} ) ;
2025-07-21 22:21:37 -07:00
it ( 'should not ignore files if respectGitIgnore is false' , async ( ) = > {
await fsPromises . writeFile (
nodePath . join ( testRootDir , '.gitignore' ) ,
'ignored.txt' ,
) ;
await createTestFile ( 'file1.txt' ) ;
await createTestFile ( 'ignored.txt' ) ;
const fileService = new FileDiscoveryService ( testRootDir ) ;
const structure = await getFolderStructure ( testRootDir , {
fileService ,
fileFilteringOptions : {
respectGeminiIgnore : false ,
respectGitIgnore : false ,
} ,
} ) ;
expect ( structure ) . toContain ( 'ignored.txt' ) ;
expect ( structure ) . toContain ( 'file1.txt' ) ;
2025-06-08 18:42:38 -07:00
} ) ;
} ) ;
2025-07-21 22:21:37 -07:00
describe ( 'with geminiignore' , ( ) = > {
it ( 'should ignore geminiignore files by default' , async ( ) = > {
await fsPromises . writeFile (
nodePath . join ( testRootDir , '.geminiignore' ) ,
'ignored.txt\nnode_modules/\n.gemini/\n!/.gemini/config.yaml' ,
) ;
await createTestFile ( 'file1.txt' ) ;
await createTestFile ( 'node_modules' , 'some-package' , 'index.js' ) ;
await createTestFile ( 'ignored.txt' ) ;
2025-10-14 02:31:39 +09:00
await createTestFile ( GEMINI_DIR , 'config.yaml' ) ;
await createTestFile ( GEMINI_DIR , 'logs.json' ) ;
2025-07-21 22:21:37 -07:00
const fileService = new FileDiscoveryService ( testRootDir ) ;
const structure = await getFolderStructure ( testRootDir , {
fileService ,
} ) ;
expect ( structure ) . not . toContain ( 'ignored.txt' ) ;
2025-07-23 15:07:19 -07:00
expect ( structure ) . toContain ( ` node_modules ${ path . sep } ... ` ) ;
2025-07-21 22:21:37 -07:00
expect ( structure ) . not . toContain ( 'logs.json' ) ;
2025-06-08 18:42:38 -07:00
} ) ;
2025-07-20 00:55:33 -07:00
2025-07-21 22:21:37 -07:00
it ( 'should not ignore files if respectGeminiIgnore is false' , async ( ) = > {
await fsPromises . writeFile (
nodePath . join ( testRootDir , '.geminiignore' ) ,
'ignored.txt\nnode_modules/\n.gemini/\n!/.gemini/config.yaml' ,
) ;
await createTestFile ( 'file1.txt' ) ;
await createTestFile ( 'node_modules' , 'some-package' , 'index.js' ) ;
await createTestFile ( 'ignored.txt' ) ;
2025-10-14 02:31:39 +09:00
await createTestFile ( GEMINI_DIR , 'config.yaml' ) ;
await createTestFile ( GEMINI_DIR , 'logs.json' ) ;
2025-07-21 22:21:37 -07:00
const fileService = new FileDiscoveryService ( testRootDir ) ;
const structure = await getFolderStructure ( testRootDir , {
fileService ,
fileFilteringOptions : {
respectGeminiIgnore : false ,
respectGitIgnore : true , // Explicitly disable gemini ignore only
} ,
} ) ;
expect ( structure ) . toContain ( 'ignored.txt' ) ;
// node_modules is still ignored by default
2025-07-23 15:07:19 -07:00
expect ( structure ) . toContain ( ` node_modules ${ path . sep } ... ` ) ;
2025-07-20 00:55:33 -07:00
} ) ;
} ) ;
2025-06-08 18:42:38 -07:00
} ) ;