Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

518 lines
17 KiB
TypeScript

/**
* Agentic QE v3 - File Reader Unit Tests
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import {
FileReader,
FileReadError,
JsonParseError,
getFileReader,
readFile,
readJSON,
fileExists,
listFiles,
} from '../../../../src/shared/io/file-reader';
describe('FileReader', () => {
let testDir: string;
let fileReader: FileReader;
beforeEach(async () => {
// Create a temporary test directory
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'file-reader-test-'));
fileReader = new FileReader({ basePath: testDir });
});
afterEach(async () => {
// Clean up test directory
await fs.rm(testDir, { recursive: true, force: true });
});
// ============================================================================
// readFile Tests
// ============================================================================
describe('readFile', () => {
it('should read file contents successfully', async () => {
const content = 'Hello, World!';
const filePath = path.join(testDir, 'test.txt');
await fs.writeFile(filePath, content, 'utf-8');
const result = await fileReader.readFile(filePath);
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe(content);
}
});
it('should handle relative paths', async () => {
const content = 'Relative path content';
await fs.writeFile(path.join(testDir, 'relative.txt'), content, 'utf-8');
const result = await fileReader.readFile('relative.txt');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe(content);
}
});
it('should return error for non-existent file', async () => {
const result = await fileReader.readFile('non-existent.txt');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(FileReadError);
expect(result.error.code).toBe('ENOENT');
expect(result.error.message).toContain('File not found');
}
});
it('should return error when reading a directory', async () => {
await fs.mkdir(path.join(testDir, 'subdir'));
const result = await fileReader.readFile('subdir');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(FileReadError);
expect(result.error.code).toBe('EISDIR');
}
});
it('should cache file contents on second read', async () => {
const content = 'Cached content';
const filePath = path.join(testDir, 'cached.txt');
await fs.writeFile(filePath, content, 'utf-8');
// First read
await fileReader.readFile(filePath);
const statsAfterFirst = fileReader.getStats();
// Second read (should hit cache)
await fileReader.readFile(filePath);
const statsAfterSecond = fileReader.getStats();
expect(statsAfterFirst.cacheMisses).toBe(1);
expect(statsAfterFirst.cacheHits).toBe(0);
expect(statsAfterSecond.cacheHits).toBe(1);
expect(statsAfterSecond.cacheMisses).toBe(1);
});
it('should respect cache disabled option', async () => {
const noCacheReader = new FileReader({
basePath: testDir,
enableCache: false,
});
const content = 'No cache content';
const filePath = path.join(testDir, 'no-cache.txt');
await fs.writeFile(filePath, content, 'utf-8');
await noCacheReader.readFile(filePath);
await noCacheReader.readFile(filePath);
const stats = noCacheReader.getStats();
expect(stats.cacheHits).toBe(0);
expect(stats.cacheEntries).toBe(0);
});
it('should read UTF-8 files with special characters', async () => {
const content = 'Unicode: \u00e9\u00e0\u00fc\u4e2d\u6587\ud83d\ude80';
const filePath = path.join(testDir, 'unicode.txt');
await fs.writeFile(filePath, content, 'utf-8');
const result = await fileReader.readFile(filePath);
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe(content);
}
});
});
// ============================================================================
// readJSON Tests
// ============================================================================
describe('readJSON', () => {
it('should parse valid JSON file', async () => {
const data = { name: 'test', count: 42, nested: { value: true } };
const filePath = path.join(testDir, 'data.json');
await fs.writeFile(filePath, JSON.stringify(data), 'utf-8');
const result = await fileReader.readJSON<typeof data>(filePath);
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toEqual(data);
}
});
it('should return error for invalid JSON', async () => {
const filePath = path.join(testDir, 'invalid.json');
await fs.writeFile(filePath, '{ invalid json }', 'utf-8');
const result = await fileReader.readJSON(filePath);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(JsonParseError);
expect(result.error.message).toContain('Invalid JSON');
}
});
it('should return error for non-existent JSON file', async () => {
const result = await fileReader.readJSON('non-existent.json');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(FileReadError);
}
});
it('should handle empty JSON object', async () => {
const filePath = path.join(testDir, 'empty.json');
await fs.writeFile(filePath, '{}', 'utf-8');
const result = await fileReader.readJSON<object>(filePath);
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toEqual({});
}
});
it('should handle JSON arrays', async () => {
const data = [1, 2, 3, 'four', { five: 5 }];
const filePath = path.join(testDir, 'array.json');
await fs.writeFile(filePath, JSON.stringify(data), 'utf-8');
const result = await fileReader.readJSON<typeof data>(filePath);
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toEqual(data);
}
});
});
// ============================================================================
// fileExists Tests
// ============================================================================
describe('fileExists', () => {
it('should return true for existing file', async () => {
const filePath = path.join(testDir, 'exists.txt');
await fs.writeFile(filePath, 'content', 'utf-8');
const result = await fileReader.fileExists(filePath);
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe(true);
}
});
it('should return false for non-existent file', async () => {
const result = await fileReader.fileExists('non-existent.txt');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe(false);
}
});
it('should handle directories', async () => {
await fs.mkdir(path.join(testDir, 'subdir'));
const result = await fileReader.fileExists('subdir');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe(true);
}
});
it('should handle relative paths', async () => {
await fs.writeFile(path.join(testDir, 'relative-exists.txt'), 'content', 'utf-8');
const result = await fileReader.fileExists('relative-exists.txt');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe(true);
}
});
});
// ============================================================================
// listFiles Tests
// ============================================================================
describe('listFiles', () => {
beforeEach(async () => {
// Create test file structure
await fs.writeFile(path.join(testDir, 'file1.ts'), 'content', 'utf-8');
await fs.writeFile(path.join(testDir, 'file2.ts'), 'content', 'utf-8');
await fs.writeFile(path.join(testDir, 'file3.js'), 'content', 'utf-8');
await fs.mkdir(path.join(testDir, 'subdir'));
await fs.writeFile(path.join(testDir, 'subdir', 'nested.ts'), 'content', 'utf-8');
await fs.writeFile(path.join(testDir, 'subdir', 'nested.js'), 'content', 'utf-8');
});
it('should list files matching simple extension pattern', async () => {
const result = await fileReader.listFiles('*.ts');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.length).toBe(2);
expect(result.value.every((f) => f.endsWith('.ts'))).toBe(true);
}
});
it('should list files matching globstar pattern', async () => {
const result = await fileReader.listFiles('**/*.ts');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.length).toBe(3);
expect(result.value.some((f) => f.includes('subdir'))).toBe(true);
}
});
it('should list files in specific subdirectory', async () => {
const result = await fileReader.listFiles('*.ts', path.join(testDir, 'subdir'));
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.length).toBe(1);
expect(result.value[0]).toContain('nested.ts');
}
});
it('should return empty array for no matches', async () => {
const result = await fileReader.listFiles('*.xyz');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toEqual([]);
}
});
it('should return error for non-existent base path', async () => {
const result = await fileReader.listFiles('*.ts', '/non/existent/path');
expect(result.success).toBe(false);
// SEC-004: PathTraversalError is returned for paths escaping basePath
// FileReadError is returned for I/O errors on valid paths
// Both are valid error types for this scenario
if (!result.success) {
expect(
result.error instanceof FileReadError ||
result.error.name === 'PathTraversalError'
).toBe(true);
}
});
it('should handle complex glob patterns', async () => {
await fs.writeFile(path.join(testDir, 'test.spec.ts'), 'content', 'utf-8');
const result = await fileReader.listFiles('*.spec.ts');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.length).toBe(1);
expect(result.value[0]).toContain('test.spec.ts');
}
});
it('should return sorted results', async () => {
const result = await fileReader.listFiles('*.ts');
expect(result.success).toBe(true);
if (result.success) {
const sorted = [...result.value].sort();
expect(result.value).toEqual(sorted);
}
});
});
// ============================================================================
// Cache Management Tests
// ============================================================================
describe('cache management', () => {
it('should invalidate specific cache entry', async () => {
const filePath = path.join(testDir, 'invalidate.txt');
await fs.writeFile(filePath, 'original', 'utf-8');
await fileReader.readFile(filePath);
expect(fileReader.getStats().cacheEntries).toBe(1);
fileReader.invalidateCache(filePath);
expect(fileReader.getStats().cacheEntries).toBe(0);
});
it('should clear all cache entries', async () => {
await fs.writeFile(path.join(testDir, 'a.txt'), 'a', 'utf-8');
await fs.writeFile(path.join(testDir, 'b.txt'), 'b', 'utf-8');
await fileReader.readFile(path.join(testDir, 'a.txt'));
await fileReader.readFile(path.join(testDir, 'b.txt'));
expect(fileReader.getStats().cacheEntries).toBe(2);
fileReader.clearCache();
expect(fileReader.getStats().cacheEntries).toBe(0);
});
it('should prune expired cache entries', async () => {
const shortTtlReader = new FileReader({
basePath: testDir,
cacheTtlMs: 10, // 10ms TTL
});
const filePath = path.join(testDir, 'expire.txt');
await fs.writeFile(filePath, 'content', 'utf-8');
await shortTtlReader.readFile(filePath);
expect(shortTtlReader.getStats().cacheEntries).toBe(1);
// Wait for TTL to expire
await new Promise((resolve) => setTimeout(resolve, 20));
const pruned = shortTtlReader.pruneCache();
expect(pruned).toBe(1);
expect(shortTtlReader.getStats().cacheEntries).toBe(0);
});
it('should evict oldest entries when cache is full', async () => {
const smallCacheReader = new FileReader({
basePath: testDir,
maxCacheSize: 2,
});
await fs.writeFile(path.join(testDir, '1.txt'), '1', 'utf-8');
await fs.writeFile(path.join(testDir, '2.txt'), '2', 'utf-8');
await fs.writeFile(path.join(testDir, '3.txt'), '3', 'utf-8');
await smallCacheReader.readFile(path.join(testDir, '1.txt'));
await smallCacheReader.readFile(path.join(testDir, '2.txt'));
expect(smallCacheReader.getStats().cacheEntries).toBe(2);
// This should evict the oldest (1.txt)
await smallCacheReader.readFile(path.join(testDir, '3.txt'));
expect(smallCacheReader.getStats().cacheEntries).toBe(2);
// Reading 1.txt again should be a cache miss
const statsBeforeReread = smallCacheReader.getStats();
await smallCacheReader.readFile(path.join(testDir, '1.txt'));
const statsAfterReread = smallCacheReader.getStats();
expect(statsAfterReread.cacheMisses).toBe(statsBeforeReread.cacheMisses + 1);
});
});
// ============================================================================
// Statistics Tests
// ============================================================================
describe('statistics', () => {
it('should track total reads', async () => {
await fs.writeFile(path.join(testDir, 'stats.txt'), 'content', 'utf-8');
expect(fileReader.getStats().totalReads).toBe(0);
await fileReader.readFile(path.join(testDir, 'stats.txt'));
expect(fileReader.getStats().totalReads).toBe(1);
await fileReader.readFile(path.join(testDir, 'stats.txt'));
expect(fileReader.getStats().totalReads).toBe(2);
});
it('should reset statistics', async () => {
await fs.writeFile(path.join(testDir, 'reset.txt'), 'content', 'utf-8');
await fileReader.readFile(path.join(testDir, 'reset.txt'));
await fileReader.readFile(path.join(testDir, 'reset.txt'));
expect(fileReader.getStats().totalReads).toBe(2);
expect(fileReader.getStats().cacheHits).toBe(1);
fileReader.resetStats();
expect(fileReader.getStats().totalReads).toBe(0);
expect(fileReader.getStats().cacheHits).toBe(0);
expect(fileReader.getStats().cacheMisses).toBe(0);
});
it('should track cache size in bytes', async () => {
const content = 'Hello, World!';
await fs.writeFile(path.join(testDir, 'size.txt'), content, 'utf-8');
await fileReader.readFile(path.join(testDir, 'size.txt'));
const stats = fileReader.getStats();
expect(stats.cacheSize).toBe(Buffer.byteLength(content, 'utf-8'));
});
});
// ============================================================================
// Convenience Functions Tests
// ============================================================================
describe('convenience functions', () => {
it('should read file using convenience function', async () => {
const content = 'Convenience test';
const filePath = path.join(testDir, 'convenience.txt');
await fs.writeFile(filePath, content, 'utf-8');
// Need to reset default instance to use our test directory
const reader = getFileReader({ basePath: testDir });
const result = await reader.readFile('convenience.txt');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe(content);
}
});
});
});
// ============================================================================
// Error Class Tests
// ============================================================================
describe('FileReadError', () => {
it('should have correct name and properties', () => {
const error = new FileReadError('Test error', '/path/to/file', 'ENOENT');
expect(error.name).toBe('FileReadError');
expect(error.message).toBe('Test error');
expect(error.filePath).toBe('/path/to/file');
expect(error.code).toBe('ENOENT');
});
it('should preserve cause', () => {
const cause = new Error('Original error');
const error = new FileReadError('Wrapped error', '/path', 'ERR', cause);
expect(error.cause).toBe(cause);
});
});
describe('JsonParseError', () => {
it('should have correct name and properties', () => {
const error = new JsonParseError('Parse failed', '/path/to/file.json');
expect(error.name).toBe('JsonParseError');
expect(error.message).toBe('Parse failed');
expect(error.filePath).toBe('/path/to/file.json');
});
});