344 lines
11 KiB
TypeScript
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/...');
|
|
});
|
|
});
|
|
});
|