cmte
Version:
Design by Committee™ except it's just you and LLMs
229 lines (199 loc) • 9.53 kB
JavaScript
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { FileCollectionManager } from '../file-collection-manager.js';
import { glob } from 'glob';
import path from 'path';
import fs from 'fs/promises';
// Mock the fs/promises module
vi.mock('fs/promises');
// Mock the glob module
vi.mock('glob');
vi.mock('fs'); // Also mock fs if needed for readFile later
describe('FileCollectionManager', () => {
let manager;
let mockGlobImplementation; // Declare here
const basePath = '/test/base/path';
const resolvePath = (p) => path.resolve(basePath, p);
beforeEach(async () => {
vi.clearAllMocks();
// Define realistic absolute paths based on basePath
const absolutePaths = {
'file1.js': resolvePath('file1.js'),
'file2.ts': resolvePath('file2.ts'),
'src/file3.js': resolvePath('src/file3.js'),
'test/file4.spec.js': resolvePath('test/file4.spec.js'),
'dir/file2.js': resolvePath('dir/file2.js'),
};
// More robust mock implementation
mockGlobImplementation = vi.fn((pattern, options) => {
// Default to empty array
let results = [];
const resolvedPattern = path.resolve(options.cwd || basePath, pattern);
// Handle specific patterns used in tests
if (pattern === 'file*.js') {
results = [absolutePaths['file1.js']];
} else if (pattern === 'file?.ts') {
results = [absolutePaths['file2.ts']];
} else if (pattern === 'src/file*.js') {
results = [absolutePaths['src/file3.js']];
} else if (pattern === 'test/*.spec.js') {
results = [absolutePaths['test/file4.spec.js']];
} else if (pattern === 'dir/file*.js') {
results = [absolutePaths['dir/file2.js']];
} else if (pattern === '*.js') { // Used in filtering test
results = [absolutePaths['file1.js'], absolutePaths['src/file3.js'], absolutePaths['test/file4.spec.js']];
} else if (pattern === 'file1.js') { // Handle exact path from registration test
results = [absolutePaths['file1.js']];
}
// Ensure options.absolute is respected (though we always return absolute here)
if (options?.absolute) {
return Promise.resolve(results);
} else {
// If absolute:false was intended, we'd need to make relative, but seems unlikely here
return Promise.resolve(results.map(p => path.relative(options.cwd || basePath, p)));
}
});
glob.mockImplementation(mockGlobImplementation);
manager = new FileCollectionManager(basePath);
await manager.registerCollection('test', {
include: [
'file*.js',
'file?.ts',
'src/file*.js',
'test/*.spec.js'
]
});
await manager.registerCollection('another', { include: ['another.txt'] });
});
describe('registerCollection', () => {
test('registers files and allows retrieval', async () => {
const filesDef = { include: ['file1.js', 'dir/file*.js'] }; // Includes an exact path and a pattern
await manager.registerCollection('normalized_test', filesDef);
expect(manager.hasCollection('normalized_test')).toBe(true);
// Expect relative paths based on basePath
const expectedFiles = ['file1.js', 'dir/file2.js'];
expect(manager.getFiles('normalized_test')).toEqual(expectedFiles);
// Check that glob was called correctly for both includes
expect(mockGlobImplementation).toHaveBeenCalledWith('file1.js', expect.objectContaining({ absolute: true, cwd: basePath }));
expect(mockGlobImplementation).toHaveBeenCalledWith('dir/file*.js', expect.objectContaining({ absolute: true, cwd: basePath }));
});
test('throws error if definition is not an object', async () => {
const invalidCall = async () => manager.registerCollection('test', 'not-an-object');
await expect(invalidCall()).rejects.toThrow("Invalid definition for file collection 'test'. Must be an object.");
});
test('throws error if include is not an array', async () => {
await expect(manager.registerCollection('test_include_err', { include: 'not-an-array' }))
.rejects.toThrow("Invalid definition for file collection 'test_include_err'. 'include' and 'exclude' must be arrays.");
});
test('throws error if exclude is not an array', async () => {
await expect(manager.registerCollection('test_exclude_err', { include: ['*.js'], exclude: 'not-an-array' }))
.rejects.toThrow("Invalid definition for file collection 'test_exclude_err'. 'include' and 'exclude' must be arrays.");
});
});
describe('getFiles', () => {
test('returns all files when no pattern is provided', () => {
const expectedFiles = [
'file1.js',
'file2.ts',
'src/file3.js',
'test/file4.spec.js'
];
expect(manager.getFiles('test')).toEqual(expectedFiles);
});
test('filters files by pattern', () => {
const expectedFiles = [
'file1.js',
'src/file3.js',
'test/file4.spec.js'
];
// The getFiles filtering uses micromatch on the *relative* paths stored
expect(manager.getFiles('test', '**/*.js')).toEqual(expectedFiles);
});
test('returns empty array for pattern with no matches', () => {
expect(manager.getFiles('test', '*.css')).toEqual([]);
});
test('throws error for non-existent collection', () => {
expect(() => manager.getFiles('missing')).toThrow('File collection not found: missing');
});
});
describe('hasCollection', () => {
test('returns true for existing collection', () => {
expect(manager.hasCollection('test')).toBe(true);
});
test('returns false for non-existent collection', () => {
expect(manager.hasCollection('missing')).toBe(false);
});
});
describe('getCollectionNames', () => {
test('returns all collection names', () => {
expect(manager.getCollectionNames()).toEqual(['test', 'another']);
});
});
describe('readFileRelative', () => {
beforeEach(() => {
// Configure mock for fs.readFile
fs.readFile.mockImplementation(async (filePath, encoding) => {
const expectedPath1 = resolvePath('src/file3.js');
const expectedPath2 = resolvePath('nonexistent.txt');
if (filePath === expectedPath1 && encoding === 'utf8') {
return Promise.resolve('Content of file3.js');
}
if (filePath === expectedPath2) { // Simulate file not found
const error = new Error(`ENOENT: no such file or directory, open '${filePath}'`);
error.code = 'ENOENT';
return Promise.reject(error);
}
// Default rejection for unexpected paths
return Promise.reject(new Error(`Mock fs.readFile called with unexpected path: ${filePath}`));
});
});
test('reads a file with a valid relative path', async () => {
const content = await manager.readFileRelative('src/file3.js');
expect(content).toBe('Content of file3.js');
expect(fs.readFile).toHaveBeenCalledWith(resolvePath('src/file3.js'), 'utf8');
});
test('throws error if underlying fs.readFile fails', async () => {
// This path is valid relative to base, but fs.readFile mock will reject it
await expect(manager.readFileRelative('nonexistent.txt'))
.rejects.toThrow(/ENOENT: no such file or directory/);
});
});
describe('readFileFromCollection', () => {
beforeEach(() => {
// Configure mock for fs.readFile
fs.readFile.mockImplementation(async (filePath, encoding) => {
const expectedPath = resolvePath('file1.js');
const unreadablePath = resolvePath('file2.ts'); // Assume this exists but read fails
if (filePath === expectedPath && encoding === 'utf8') {
return Promise.resolve('Content of file1.js');
}
if (filePath === unreadablePath) { // Simulate read error
const error = new Error(`EACCES: permission denied, open '${filePath}'`);
error.code = 'EACCES';
return Promise.reject(error);
}
// Default rejection
return Promise.reject(new Error(`Mock fs.readFile called with unexpected path: ${filePath}`));
});
});
test('reads a file that exists in the specified collection', async () => {
// 'file1.js' is added to the 'test' collection in the outer beforeEach
const content = await manager.readFileFromCollection('test', 'file1.js');
expect(content).toBe('Content of file1.js');
expect(fs.readFile).toHaveBeenCalledWith(resolvePath('file1.js'), 'utf8');
});
test('throws error if the file is not in the specified collection', async () => {
// 'dir/file2.js' exists but is not in the 'test' collection
await expect(manager.readFileFromCollection('test', 'dir/file2.js'))
.rejects.toThrow(/File 'dir\/file2.js' not found in collection 'test'/);
});
test('throws error if the collection name does not exist', async () => {
await expect(manager.readFileFromCollection('nonexistent_collection', 'file1.js'))
.rejects.toThrow('File collection not found: nonexistent_collection');
});
test('throws error if underlying fs.readFile fails for a file in the collection', async () => {
// 'file2.ts' is in the 'test' collection, but the mock fs.readFile will fail for it
await expect(manager.readFileFromCollection('test', 'file2.ts'))
.rejects.toThrow(/EACCES: permission denied/);
});
});
});