BMAD-METHOD/test/unit/file-ops/get-file-list.test.js

284 lines
11 KiB
JavaScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { FileOps } from '../../../tools/cli/lib/file-ops.js';
import { createTempDir, cleanupTempDir, createTestFile, createTestDirs } from '../../helpers/temp-dir.js';
import path from 'node:path';
describe('FileOps', () => {
describe('getFileList()', () => {
const fileOps = new FileOps();
let tmpDir;
beforeEach(async () => {
tmpDir = await createTempDir();
});
afterEach(async () => {
await cleanupTempDir(tmpDir);
});
describe('basic functionality', () => {
it('should return empty array for empty directory', async () => {
const files = await fileOps.getFileList(tmpDir);
expect(files).toEqual([]);
});
it('should return single file in directory', async () => {
await createTestFile(tmpDir, 'test.txt', 'content');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(1);
expect(files[0]).toBe('test.txt');
});
it('should return multiple files in directory', async () => {
await createTestFile(tmpDir, 'file1.txt', 'content1');
await createTestFile(tmpDir, 'file2.md', 'content2');
await createTestFile(tmpDir, 'file3.json', 'content3');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(3);
expect(files).toContain('file1.txt');
expect(files).toContain('file2.md');
expect(files).toContain('file3.json');
});
});
describe('recursive directory walking', () => {
it('should recursively find files in nested directories', async () => {
await createTestFile(tmpDir, 'root.txt', 'root');
await createTestFile(tmpDir, 'level1/file1.txt', 'level1');
await createTestFile(tmpDir, 'level1/level2/file2.txt', 'level2');
await createTestFile(tmpDir, 'level1/level2/level3/file3.txt', 'level3');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(4);
expect(files).toContain('root.txt');
expect(files).toContain(path.join('level1', 'file1.txt'));
expect(files).toContain(path.join('level1', 'level2', 'file2.txt'));
expect(files).toContain(path.join('level1', 'level2', 'level3', 'file3.txt'));
});
it('should handle multiple subdirectories at same level', async () => {
await createTestFile(tmpDir, 'dir1/file1.txt', 'content');
await createTestFile(tmpDir, 'dir2/file2.txt', 'content');
await createTestFile(tmpDir, 'dir3/file3.txt', 'content');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(3);
expect(files).toContain(path.join('dir1', 'file1.txt'));
expect(files).toContain(path.join('dir2', 'file2.txt'));
expect(files).toContain(path.join('dir3', 'file3.txt'));
});
it('should not include empty directories in results', async () => {
await createTestDirs(tmpDir, ['empty1', 'empty2', 'has-file']);
await createTestFile(tmpDir, 'has-file/file.txt', 'content');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(1);
expect(files[0]).toBe(path.join('has-file', 'file.txt'));
});
});
describe('ignore filtering', () => {
it('should ignore .git directories', async () => {
await createTestFile(tmpDir, 'normal.txt', 'content');
await createTestFile(tmpDir, '.git/config', 'git config');
await createTestFile(tmpDir, '.git/hooks/pre-commit', 'hook');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(1);
expect(files[0]).toBe('normal.txt');
});
it('should ignore node_modules directories', async () => {
await createTestFile(tmpDir, 'package.json', '{}');
await createTestFile(tmpDir, 'node_modules/package/index.js', 'code');
await createTestFile(tmpDir, 'node_modules/package/lib/util.js', 'util');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(1);
expect(files[0]).toBe('package.json');
});
it('should ignore .DS_Store files', async () => {
await createTestFile(tmpDir, 'file.txt', 'content');
await createTestFile(tmpDir, '.DS_Store', 'mac metadata');
await createTestFile(tmpDir, 'subdir/.DS_Store', 'mac metadata');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(1);
expect(files[0]).toBe('file.txt');
});
it('should ignore *.swp and *.tmp files', async () => {
await createTestFile(tmpDir, 'document.txt', 'content');
await createTestFile(tmpDir, 'document.txt.swp', 'vim swap');
await createTestFile(tmpDir, 'temp.tmp', 'temporary');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(1);
expect(files[0]).toBe('document.txt');
});
it('should ignore multiple ignored patterns together', async () => {
await createTestFile(tmpDir, 'src/index.js', 'source code');
await createTestFile(tmpDir, 'node_modules/lib/code.js', 'dependency');
await createTestFile(tmpDir, '.git/config', 'git config');
await createTestFile(tmpDir, '.DS_Store', 'mac file');
await createTestFile(tmpDir, 'file.swp', 'swap file');
await createTestFile(tmpDir, '.idea/workspace.xml', 'ide');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(1);
expect(files[0]).toBe(path.join('src', 'index.js'));
});
});
describe('relative path handling', () => {
it('should return paths relative to base directory', async () => {
await createTestFile(tmpDir, 'a/b/c/deep.txt', 'deep');
const files = await fileOps.getFileList(tmpDir);
expect(files[0]).toBe(path.join('a', 'b', 'c', 'deep.txt'));
expect(path.isAbsolute(files[0])).toBe(false);
});
it('should handle subdirectory as base', async () => {
await createTestFile(tmpDir, 'root.txt', 'root');
await createTestFile(tmpDir, 'sub/file1.txt', 'sub1');
await createTestFile(tmpDir, 'sub/file2.txt', 'sub2');
const subDir = path.join(tmpDir, 'sub');
const files = await fileOps.getFileList(subDir);
expect(files).toHaveLength(2);
expect(files).toContain('file1.txt');
expect(files).toContain('file2.txt');
// Should not include root.txt
expect(files).not.toContain('root.txt');
});
});
describe('edge cases', () => {
it('should handle directory with special characters', async () => {
await createTestFile(tmpDir, 'folder with spaces/file.txt', 'content');
await createTestFile(tmpDir, 'special-chars!@#/data.json', 'data');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(2);
expect(files).toContain(path.join('folder with spaces', 'file.txt'));
expect(files).toContain(path.join('special-chars!@#', 'data.json'));
});
it('should handle Unicode filenames', async () => {
await createTestFile(tmpDir, '文档/测试.txt', 'chinese');
await createTestFile(tmpDir, 'файл/данные.json', 'russian');
await createTestFile(tmpDir, 'ファイル/データ.yaml', 'japanese');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(3);
expect(files.some((f) => f.includes('测试.txt'))).toBe(true);
expect(files.some((f) => f.includes('данные.json'))).toBe(true);
expect(files.some((f) => f.includes('データ.yaml'))).toBe(true);
});
it('should return empty array for non-existent directory', async () => {
const nonExistent = path.join(tmpDir, 'does-not-exist');
const files = await fileOps.getFileList(nonExistent);
expect(files).toEqual([]);
});
it('should handle very deep directory nesting', async () => {
// Create a deeply nested structure (10 levels)
const deepPath = Array.from({ length: 10 }, (_, i) => `level${i}`).join('/');
await createTestFile(tmpDir, `${deepPath}/deep.txt`, 'very deep');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(1);
expect(files[0]).toBe(path.join(...deepPath.split('/'), 'deep.txt'));
});
it('should handle directory with many files', async () => {
// Create 100 files
const promises = Array.from({ length: 100 }, (_, i) => createTestFile(tmpDir, `file${i}.txt`, `content ${i}`));
await Promise.all(promises);
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(100);
expect(files.every((f) => f.startsWith('file') && f.endsWith('.txt'))).toBe(true);
});
it('should handle mixed ignored and non-ignored files', async () => {
await createTestFile(tmpDir, 'src/main.js', 'code');
await createTestFile(tmpDir, 'src/main.js.swp', 'swap');
await createTestFile(tmpDir, 'lib/utils.js', 'utils');
await createTestFile(tmpDir, 'node_modules/dep/index.js', 'dep');
await createTestFile(tmpDir, 'test/test.js', 'test');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(3);
expect(files).toContain(path.join('src', 'main.js'));
expect(files).toContain(path.join('lib', 'utils.js'));
expect(files).toContain(path.join('test', 'test.js'));
});
});
describe('file types', () => {
it('should include files with no extension', async () => {
await createTestFile(tmpDir, 'README', 'readme content');
await createTestFile(tmpDir, 'LICENSE', 'license text');
await createTestFile(tmpDir, 'Makefile', 'make commands');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(3);
expect(files).toContain('README');
expect(files).toContain('LICENSE');
expect(files).toContain('Makefile');
});
it('should include dotfiles (except ignored ones)', async () => {
await createTestFile(tmpDir, '.gitignore', 'ignore patterns');
await createTestFile(tmpDir, '.env', 'environment');
await createTestFile(tmpDir, '.eslintrc', 'eslint config');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(3);
expect(files).toContain('.gitignore');
expect(files).toContain('.env');
expect(files).toContain('.eslintrc');
});
it('should include files with multiple extensions', async () => {
await createTestFile(tmpDir, 'archive.tar.gz', 'archive');
await createTestFile(tmpDir, 'backup.sql.bak', 'backup');
await createTestFile(tmpDir, 'config.yaml.sample', 'sample config');
const files = await fileOps.getFileList(tmpDir);
expect(files).toHaveLength(3);
});
});
});
});