mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
fcdd783df9
Strategy pattern integration for BaseAgent decomposition: - Add AgentLifecycleStrategy, AgentMemoryStrategy interfaces - Add AgentLearningStrategy, AgentCoordinationStrategy interfaces - Implement DefaultLifecycleStrategy, DefaultMemoryStrategy - Implement DefaultLearningStrategy, DefaultCoordinationStrategy - Create LifecycleManagerAdapter bridging existing manager - Create MemoryServiceAdapter bridging existing service - Add platform operations module (A2.2) with COW/reflink support - Add actual benchmark results (not estimates) Test coverage: 117 tests passing - 75 strategy tests - 21 platform operation tests - 21 lifecycle adapter tests BaseAgent now has strategies property with adapters as defaults. Full decomposition (LOC reduction) in next commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
/**
|
|
* FileOperations Unit Tests
|
|
*
|
|
* Tests for platform-optimized file operations.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import {
|
|
copyFile,
|
|
copyDirectory,
|
|
cloneForIsolation,
|
|
linkFixture,
|
|
getCopyCapabilities,
|
|
benchmarkCopy,
|
|
CopyStrategy,
|
|
} from '../../../src/core/platform/FileOperations';
|
|
import {
|
|
detectCapabilities,
|
|
clearCapabilitiesCache,
|
|
} from '../../../src/core/platform/PlatformDetector';
|
|
|
|
describe('FileOperations', () => {
|
|
let testDir: string;
|
|
let srcFile: string;
|
|
let destFile: string;
|
|
|
|
beforeAll(async () => {
|
|
// Create test directory in temp
|
|
testDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'file-ops-test-'));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Cleanup test directory
|
|
try {
|
|
await fs.promises.rm(testDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Create fresh source file for each test
|
|
srcFile = path.join(testDir, `src-${Date.now()}.txt`);
|
|
destFile = path.join(testDir, `dest-${Date.now()}.txt`);
|
|
await fs.promises.writeFile(srcFile, 'test content for file operations');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Cleanup test files
|
|
try {
|
|
await fs.promises.unlink(srcFile).catch(() => {});
|
|
await fs.promises.unlink(destFile).catch(() => {});
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
});
|
|
|
|
describe('copyFile', () => {
|
|
it('should copy a file successfully', async () => {
|
|
const result = await copyFile(srcFile, destFile);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.source).toBe(srcFile);
|
|
expect(result.destination).toBe(destFile);
|
|
expect(result.size).toBeGreaterThan(0);
|
|
expect(result.duration).toBeGreaterThanOrEqual(0);
|
|
|
|
// Verify file was copied
|
|
const destContent = await fs.promises.readFile(destFile, 'utf8');
|
|
expect(destContent).toBe('test content for file operations');
|
|
});
|
|
|
|
it('should report strategy used', async () => {
|
|
const result = await copyFile(srcFile, destFile);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(Object.values(CopyStrategy)).toContain(result.strategy);
|
|
});
|
|
|
|
it('should fail if source does not exist', async () => {
|
|
const result = await copyFile('/nonexistent/file.txt', destFile);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('should fail if destination exists and overwrite is false', async () => {
|
|
// Create destination first
|
|
await fs.promises.writeFile(destFile, 'existing');
|
|
|
|
const result = await copyFile(srcFile, destFile, { overwrite: false });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('already exists');
|
|
});
|
|
|
|
it('should overwrite destination when option is set', async () => {
|
|
// Create destination first
|
|
await fs.promises.writeFile(destFile, 'existing content');
|
|
|
|
const result = await copyFile(srcFile, destFile, { overwrite: true });
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Verify content was overwritten
|
|
const destContent = await fs.promises.readFile(destFile, 'utf8');
|
|
expect(destContent).toBe('test content for file operations');
|
|
});
|
|
|
|
it('should create parent directories when recursive is set', async () => {
|
|
const nestedDest = path.join(testDir, 'nested', 'dir', 'file.txt');
|
|
|
|
const result = await copyFile(srcFile, nestedDest, { recursive: true });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(fs.existsSync(nestedDest)).toBe(true);
|
|
|
|
// Cleanup
|
|
await fs.promises.rm(path.join(testDir, 'nested'), { recursive: true });
|
|
});
|
|
|
|
it('should preserve timestamps when option is set', async () => {
|
|
// Set source timestamps to a known value
|
|
const pastTime = new Date('2020-01-01');
|
|
await fs.promises.utimes(srcFile, pastTime, pastTime);
|
|
|
|
const result = await copyFile(srcFile, destFile, { preserveTimestamps: true });
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const destStats = await fs.promises.stat(destFile);
|
|
expect(destStats.mtime.getTime()).toBe(pastTime.getTime());
|
|
});
|
|
|
|
it('should use specified strategy', async () => {
|
|
const result = await copyFile(srcFile, destFile, {
|
|
strategy: CopyStrategy.USERSPACE,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.strategy).toBe(CopyStrategy.USERSPACE);
|
|
});
|
|
});
|
|
|
|
describe('copyDirectory', () => {
|
|
let srcDir: string;
|
|
let destDir: string;
|
|
|
|
beforeEach(async () => {
|
|
srcDir = path.join(testDir, `srcdir-${Date.now()}`);
|
|
destDir = path.join(testDir, `destdir-${Date.now()}`);
|
|
|
|
// Create source directory with files
|
|
await fs.promises.mkdir(srcDir, { recursive: true });
|
|
await fs.promises.writeFile(path.join(srcDir, 'file1.txt'), 'content 1');
|
|
await fs.promises.writeFile(path.join(srcDir, 'file2.txt'), 'content 2');
|
|
await fs.promises.mkdir(path.join(srcDir, 'subdir'));
|
|
await fs.promises.writeFile(path.join(srcDir, 'subdir', 'file3.txt'), 'content 3');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.promises.rm(srcDir, { recursive: true, force: true }).catch(() => {});
|
|
await fs.promises.rm(destDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
it('should copy directory recursively', async () => {
|
|
const results = await copyDirectory(srcDir, destDir);
|
|
|
|
expect(results).toHaveLength(3);
|
|
expect(results.every(r => r.success)).toBe(true);
|
|
|
|
// Verify files were copied
|
|
expect(fs.existsSync(path.join(destDir, 'file1.txt'))).toBe(true);
|
|
expect(fs.existsSync(path.join(destDir, 'file2.txt'))).toBe(true);
|
|
expect(fs.existsSync(path.join(destDir, 'subdir', 'file3.txt'))).toBe(true);
|
|
});
|
|
|
|
it('should preserve directory structure', async () => {
|
|
await copyDirectory(srcDir, destDir);
|
|
|
|
// Verify content
|
|
const content1 = await fs.promises.readFile(path.join(destDir, 'file1.txt'), 'utf8');
|
|
const content3 = await fs.promises.readFile(path.join(destDir, 'subdir', 'file3.txt'), 'utf8');
|
|
|
|
expect(content1).toBe('content 1');
|
|
expect(content3).toBe('content 3');
|
|
});
|
|
|
|
it('should throw for non-directory source', async () => {
|
|
await expect(copyDirectory(srcFile, destDir)).rejects.toThrow('not a directory');
|
|
});
|
|
});
|
|
|
|
describe('cloneForIsolation', () => {
|
|
let srcDir: string;
|
|
let destDir: string;
|
|
|
|
beforeEach(async () => {
|
|
srcDir = path.join(testDir, `clone-src-${Date.now()}`);
|
|
destDir = path.join(testDir, `clone-dest-${Date.now()}`);
|
|
|
|
await fs.promises.mkdir(srcDir, { recursive: true });
|
|
await fs.promises.writeFile(path.join(srcDir, 'config.json'), '{"key": "value"}');
|
|
await fs.promises.writeFile(path.join(srcDir, 'data.txt'), 'test data');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.promises.rm(srcDir, { recursive: true, force: true }).catch(() => {});
|
|
await fs.promises.rm(destDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
it('should clone directory for isolation', async () => {
|
|
const result = await cloneForIsolation(srcDir, destDir);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.filesCloned).toBe(2);
|
|
expect(result.totalSize).toBeGreaterThan(0);
|
|
expect(result.duration).toBeGreaterThan(0);
|
|
expect(Object.values(CopyStrategy)).toContain(result.strategy);
|
|
});
|
|
|
|
it('should report errors', async () => {
|
|
// Clone to a path with permission issues
|
|
const badPath = '/root/no-permission';
|
|
|
|
const result = await cloneForIsolation(srcDir, badPath).catch(() => ({
|
|
success: false,
|
|
filesCloned: 0,
|
|
totalSize: 0,
|
|
duration: 0,
|
|
strategy: CopyStrategy.USERSPACE,
|
|
errors: ['Permission denied'],
|
|
}));
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('linkFixture', () => {
|
|
it('should create hardlink for fixture', async () => {
|
|
const result = await linkFixture(srcFile, destFile);
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Verify content
|
|
const destContent = await fs.promises.readFile(destFile, 'utf8');
|
|
expect(destContent).toBe('test content for file operations');
|
|
|
|
// Verify they share the same inode (hardlink)
|
|
const srcStats = await fs.promises.stat(srcFile);
|
|
const destStats = await fs.promises.stat(destFile);
|
|
expect(srcStats.ino).toBe(destStats.ino);
|
|
});
|
|
|
|
it('should fall back to copy if hardlink fails', async () => {
|
|
// Create dest in different filesystem (if possible)
|
|
// This test may not fail hardlink on all systems
|
|
const result = await linkFixture(srcFile, destFile);
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getCopyCapabilities', () => {
|
|
it('should return platform capabilities', async () => {
|
|
const caps = await getCopyCapabilities();
|
|
|
|
expect(caps.platform).toBeDefined();
|
|
expect(caps.filesystem).toBeDefined();
|
|
expect(Object.values(CopyStrategy)).toContain(caps.optimalStrategy);
|
|
expect(caps.expectedSpeedup).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('benchmarkCopy', () => {
|
|
it('should run copy benchmarks', async () => {
|
|
const results = await benchmarkCopy(testDir);
|
|
|
|
expect(results.userspace).toBeGreaterThan(0);
|
|
expect(results.improvement).toBeDefined();
|
|
|
|
// Kernel copy should be available or null
|
|
if (results.kernel !== null) {
|
|
expect(results.kernel).toBeGreaterThanOrEqual(0);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('PlatformDetector', () => {
|
|
afterEach(() => {
|
|
clearCapabilitiesCache();
|
|
});
|
|
|
|
describe('detectCapabilities', () => {
|
|
it('should detect platform capabilities', async () => {
|
|
const caps = await detectCapabilities();
|
|
|
|
expect(caps.platform).toBe(process.platform);
|
|
expect(caps.filesystem).toBeDefined();
|
|
expect(typeof caps.supportsReflink).toBe('boolean');
|
|
expect(typeof caps.supportsHardlinks).toBe('boolean');
|
|
expect(caps.summary).toBeDefined();
|
|
});
|
|
|
|
it('should cache capabilities', async () => {
|
|
const caps1 = await detectCapabilities('/tmp');
|
|
const caps2 = await detectCapabilities('/tmp');
|
|
|
|
expect(caps1).toEqual(caps2);
|
|
});
|
|
|
|
it('should detect capabilities for specific path', async () => {
|
|
const caps = await detectCapabilities(os.tmpdir());
|
|
|
|
expect(caps.platform).toBeDefined();
|
|
expect(caps.filesystem).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('clearCapabilitiesCache', () => {
|
|
it('should clear the cache', async () => {
|
|
await detectCapabilities('/tmp');
|
|
clearCapabilitiesCache();
|
|
|
|
// Should not throw after clearing
|
|
const caps = await detectCapabilities('/tmp');
|
|
expect(caps).toBeDefined();
|
|
});
|
|
});
|
|
});
|