gemini-cli/packages/core/src/utils/getFolderStructure.test.ts

344 lines
11 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import fsPromises from 'fs/promises';
import * as nodePath from 'path';
import * as os from 'os';
import { getFolderStructure } from './getFolderStructure.js';
import * as gitUtils from './gitUtils.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import * as path from 'path';
vi.mock('./gitUtils.js');
describe('getFolderStructure', () => {
let testRootDir: string;
const createEmptyDir = async (...pathSegments: string[]) => {
const fullPath = path.join(testRootDir, ...pathSegments);
await fsPromises.mkdir(fullPath, { recursive: true });
};
const createTestFile = async (...pathSegments: string[]) => {
const fullPath = path.join(testRootDir, ...pathSegments);
await fsPromises.mkdir(path.dirname(fullPath), { recursive: true });
await fsPromises.writeFile(fullPath, '');
return fullPath;
};
beforeEach(async () => {
testRootDir = await fsPromises.mkdtemp(
path.join(os.tmpdir(), 'folder-structure-test-'),
);
vi.resetAllMocks();
});
afterEach(async () => {
await fsPromises.rm(testRootDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
it('should return basic folder structure', async () => {
await createTestFile('fileA1.ts');
await createTestFile('fileA2.js');
await createTestFile('subfolderB', 'fileB1.md');
const structure = await getFolderStructure(testRootDir);
expect(structure.trim()).toBe(
`
Showing up to 200 items (files + folders).
${testRootDir}${path.sep}
├───fileA1.ts
├───fileA2.js
└───subfolderB${path.sep}
└───fileB1.md
`.trim(),
);
});
it('should handle an empty folder', async () => {
const structure = await getFolderStructure(testRootDir);
expect(structure.trim()).toBe(
`
Showing up to 200 items (files + folders).
${testRootDir}${path.sep}
`
.trim()
.trim(),
);
});
it('should ignore folders specified in ignoredFolders (default)', async () => {
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(
`
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.
${testRootDir}${path.sep}
├───.hiddenfile
├───file1.txt
├───emptyFolder${path.sep}
├───node_modules${path.sep}...
└───subfolderA${path.sep}
├───fileA1.ts
├───fileA2.js
└───subfolderB${path.sep}
└───fileB1.md
`.trim(),
);
});
it('should ignore folders specified in custom ignoredFolders', async () => {
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, {
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.
${testRootDir}${path.sep}
├───.hiddenfile
├───file1.txt
├───emptyFolder${path.sep}
├───node_modules${path.sep}...
└───subfolderA${path.sep}...
`.trim();
expect(structure.trim()).toBe(expected);
});
it('should filter files by fileIncludePattern', async () => {
await createTestFile('fileA1.ts');
await createTestFile('fileA2.js');
await createTestFile('subfolderB', 'fileB1.md');
const structure = await getFolderStructure(testRootDir, {
fileIncludePattern: /\.ts$/,
});
const expected = `
Showing up to 200 items (files + folders).
${testRootDir}${path.sep}
├───fileA1.ts
└───subfolderB${path.sep}
`.trim();
expect(structure.trim()).toBe(expected);
});
it('should handle maxItems truncation for files within a folder', async () => {
await createTestFile('fileA1.ts');
await createTestFile('fileA2.js');
await createTestFile('subfolderB', 'fileB1.md');
const structure = await getFolderStructure(testRootDir, {
maxItems: 3,
});
const expected = `
Showing up to 3 items (files + folders).
${testRootDir}${path.sep}
├───fileA1.ts
├───fileA2.js
└───subfolderB${path.sep}
`.trim();
expect(structure.trim()).toBe(expected);
});
it('should handle maxItems truncation for subfolders', async () => {
for (let i = 0; i < 5; i++) {
await createTestFile(`folder-${i}`, 'child.txt');
}
const structure = await getFolderStructure(testRootDir, {
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.
${testRootDir}${path.sep}
├───folder-0${path.sep}
├───folder-1${path.sep}
├───folder-2${path.sep}
├───folder-3${path.sep}
└───...
`.trim();
expect(structure.trim()).toBe(expectedRevised);
});
it('should handle maxItems that only allows the root folder itself', async () => {
await createTestFile('fileA1.ts');
await createTestFile('fileA2.ts');
await createTestFile('subfolderB', 'fileB1.ts');
const structure = await getFolderStructure(testRootDir, {
maxItems: 1,
});
const expected = `
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.
${testRootDir}${path.sep}
├───fileA1.ts
├───...
└───...
`.trim();
expect(structure.trim()).toBe(expected);
});
it('should handle non-existent directory', async () => {
const nonExistentPath = path.join(testRootDir, 'non-existent');
const structure = await getFolderStructure(nonExistentPath);
expect(structure).toContain(
`Error: Could not read directory "${nonExistentPath}". Check path and permissions.`,
);
});
it('should handle deep folder structure within limits', async () => {
await createTestFile('level1', 'level2', 'level3', 'file.txt');
const structure = await getFolderStructure(testRootDir, {
maxItems: 10,
});
const expected = `
Showing up to 10 items (files + folders).
${testRootDir}${path.sep}
└───level1${path.sep}
└───level2${path.sep}
└───level3${path.sep}
└───file.txt
`.trim();
expect(structure.trim()).toBe(expected);
});
it('should truncate deep folder structure if maxItems is small', async () => {
await createTestFile('level1', 'level2', 'level3', 'file.txt');
const structure = await getFolderStructure(testRootDir, {
maxItems: 3,
});
const expected = `
Showing up to 3 items (files + folders).
${testRootDir}${path.sep}
└───level1${path.sep}
└───level2${path.sep}
└───level3${path.sep}
`.trim();
expect(structure.trim()).toBe(expected);
});
describe('with gitignore', () => {
beforeEach(() => {
vi.mocked(gitUtils.isGitRepository).mockReturnValue(true);
});
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');
await createTestFile('.gemini', 'config.yaml');
await createTestFile('.gemini', 'logs.json');
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');
});
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');
});
});
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');
await createTestFile('.gemini', 'config.yaml');
await createTestFile('.gemini', 'logs.json');
const fileService = new FileDiscoveryService(testRootDir);
const structure = await getFolderStructure(testRootDir, {
fileService,
});
expect(structure).not.toContain('ignored.txt');
expect(structure).toContain('node_modules/...');
expect(structure).not.toContain('logs.json');
});
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');
await createTestFile('.gemini', 'config.yaml');
await createTestFile('.gemini', 'logs.json');
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
expect(structure).toContain('node_modules/...');
});
});
});