/** * 检查点系统测试 */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import * as path from 'path'; import * as fs from 'fs/promises'; import * as os from 'os'; import { CheckpointManager, ShadowGit, hashWorkingDir, } from '../../src/checkpoint/index.js'; describe('hashWorkingDir', () => { it('should generate consistent hash for same path', () => { const path1 = '/Users/test/project'; const path2 = '/Users/test/project'; expect(hashWorkingDir(path1)).toBe(hashWorkingDir(path2)); }); it('should generate different hash for different paths', () => { const path1 = '/Users/test/project1'; const path2 = '/Users/test/project2'; expect(hashWorkingDir(path1)).not.toBe(hashWorkingDir(path2)); }); it('should generate 13-character hash', () => { const hash = hashWorkingDir('/some/test/path'); expect(hash.length).toBe(13); }); it('should only contain digits', () => { const hash = hashWorkingDir('/some/test/path'); expect(/^\d+$/.test(hash)).toBe(true); }); }); describe('ShadowGit', () => { let tempDir: string; let storageDir: string; let shadowGit: ShadowGit; beforeAll(async () => { tempDir = path.join(os.tmpdir(), `checkpoint-test-${Date.now()}`); storageDir = path.join(os.tmpdir(), `checkpoint-storage-${Date.now()}`); await fs.mkdir(tempDir, { recursive: true }); await fs.mkdir(storageDir, { recursive: true }); shadowGit = new ShadowGit(tempDir, storageDir); }); afterAll(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); await fs.rm(storageDir, { recursive: true, force: true }); } catch { // ignore cleanup errors } }); it('should initialize shadow git repository', async () => { await shadowGit.initialize(); const gitDir = path.join(shadowGit.getShadowGitDir(), '.git'); const stat = await fs.stat(gitDir); expect(stat.isDirectory()).toBe(true); }); it('should create commit and return hash', async () => { // 创建测试文件 await fs.writeFile(path.join(tempDir, 'test.txt'), 'Hello World'); const commitHash = await shadowGit.createCommit('Test commit'); expect(commitHash).toBeDefined(); expect(commitHash.length).toBe(40); // Git SHA-1 hash length }); it('should list commits', async () => { const commits = await shadowGit.getCommits(10); expect(commits.length).toBeGreaterThan(0); expect(commits[0].hash).toBeDefined(); expect(commits[0].message).toBeDefined(); expect(commits[0].timestamp).toBeGreaterThan(0); }); it('should detect changes', async () => { // 修改文件 await fs.writeFile(path.join(tempDir, 'test.txt'), 'Modified content'); const hasChanges = await shadowGit.hasChanges(); expect(hasChanges).toBe(true); }); it('should get diff summary', async () => { // 先提交当前状态 await shadowGit.createCommit('Before diff test'); // 修改文件 await fs.writeFile(path.join(tempDir, 'test.txt'), 'New content for diff'); await shadowGit.createCommit('After diff test'); const commits = await shadowGit.getCommits(2); const diff = await shadowGit.getDiffSummary(commits[1].hash, commits[0].hash); expect(diff.from).toBe(commits[1].hash); expect(diff.to).toBe(commits[0].hash); expect(diff.files.length).toBeGreaterThanOrEqual(0); }); it('should reset to previous commit', async () => { const commits = await shadowGit.getCommits(2); if (commits.length < 2) return; // 跳过如果没有足够的 commits const olderCommit = commits[1].hash; await shadowGit.resetHard(olderCommit); const currentHead = await shadowGit.getHead(); expect(currentHead).toBe(olderCommit); }); }); describe('CheckpointManager', () => { let tempDir: string; let storageDir: string; let manager: CheckpointManager; beforeEach(async () => { tempDir = path.join(os.tmpdir(), `checkpoint-mgr-test-${Date.now()}`); storageDir = path.join(os.tmpdir(), `checkpoint-mgr-storage-${Date.now()}`); await fs.mkdir(tempDir, { recursive: true }); manager = new CheckpointManager(tempDir, { storageDir, maxCheckpoints: 10, maxAge: 24 * 60 * 60 * 1000, }); }); afterAll(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); await fs.rm(storageDir, { recursive: true, force: true }); } catch { // ignore cleanup errors } }); it('should initialize manager', async () => { await manager.initialize(); expect(manager.isEnabled()).toBe(true); }); it('should create checkpoint', async () => { await fs.writeFile(path.join(tempDir, 'file1.txt'), 'Content 1'); const checkpoint = await manager.createCheckpoint({ name: 'Test checkpoint', description: 'A test checkpoint', }); expect(checkpoint.id).toBeDefined(); expect(checkpoint.name).toBe('Test checkpoint'); expect(checkpoint.description).toBe('A test checkpoint'); expect(checkpoint.commitHash).toBeDefined(); expect(checkpoint.timestamp).toBeGreaterThan(0); }); it('should list checkpoints', async () => { // 创建几个检查点 await fs.writeFile(path.join(tempDir, 'file2.txt'), 'Content 2'); await manager.createCheckpoint({ name: 'Checkpoint 1' }); await fs.writeFile(path.join(tempDir, 'file3.txt'), 'Content 3'); await manager.createCheckpoint({ name: 'Checkpoint 2' }); const checkpoints = await manager.listCheckpoints(); expect(checkpoints.length).toBeGreaterThanOrEqual(2); // 应该按时间倒序排列 expect(checkpoints[0].timestamp).toBeGreaterThanOrEqual(checkpoints[1].timestamp); }); it('should get checkpoint by id', async () => { await fs.writeFile(path.join(tempDir, 'file4.txt'), 'Content 4'); const created = await manager.createCheckpoint({ name: 'Find me' }); const found = await manager.getCheckpoint(created.id); expect(found).not.toBeNull(); expect(found!.id).toBe(created.id); expect(found!.name).toBe('Find me'); }); it('should get latest checkpoint', async () => { await fs.writeFile(path.join(tempDir, 'file5.txt'), 'Content 5'); const cp1 = await manager.createCheckpoint({ name: 'Older' }); // 等待一小段时间确保时间戳不同 await new Promise((resolve) => setTimeout(resolve, 100)); await fs.writeFile(path.join(tempDir, 'file6.txt'), 'Content 6'); const cp2 = await manager.createCheckpoint({ name: 'Newer' }); const latest = await manager.getLatestCheckpoint(); expect(latest).not.toBeNull(); expect(latest!.id).toBe(cp2.id); }); it('should determine if checkpoint should be created for tool', () => { expect(manager.shouldCreateCheckpoint('write_file')).toBe(true); expect(manager.shouldCreateCheckpoint('edit_file')).toBe(true); expect(manager.shouldCreateCheckpoint('delete_file')).toBe(true); expect(manager.shouldCreateCheckpoint('bash')).toBe(false); // 默认禁用 expect(manager.shouldCreateCheckpoint('read_file')).toBe(false); }); it('should create checkpoint before tool execution', async () => { await fs.writeFile(path.join(tempDir, 'before-tool.txt'), 'Before'); const checkpointId = await manager.beforeToolExecution('write_file', { path: path.join(tempDir, 'new-file.txt'), }); expect(checkpointId).not.toBeNull(); const checkpoint = await manager.getCheckpoint(checkpointId!); expect(checkpoint).not.toBeNull(); expect(checkpoint!.trigger).toBe('tool:write_file'); }); it('should get diff between checkpoint and current state', async () => { await fs.writeFile(path.join(tempDir, 'diff-test.txt'), 'Original'); const checkpoint = await manager.createCheckpoint({ name: 'Before change' }); // 修改文件 await fs.writeFile(path.join(tempDir, 'diff-test.txt'), 'Modified'); const diff = await manager.getDiff(checkpoint.id); expect(diff).toBeDefined(); expect(diff.from).toBe(checkpoint.commitHash); }); it('should rollback to checkpoint (dry run)', async () => { await fs.writeFile(path.join(tempDir, 'rollback-test.txt'), 'Original'); const checkpoint = await manager.createCheckpoint({ name: 'Before rollback' }); // 修改文件 await fs.writeFile(path.join(tempDir, 'rollback-test.txt'), 'Modified'); await manager.createCheckpoint({ name: 'After change' }); // 预览回滚 const result = await manager.rollback({ target: checkpoint.id, dryRun: true, }); expect(result.success).toBe(true); expect(result.restoredFiles.length).toBeGreaterThanOrEqual(0); // 文件应该保持修改状态(因为是 dry run) const content = await fs.readFile(path.join(tempDir, 'rollback-test.txt'), 'utf-8'); expect(content).toBe('Modified'); }); it('should emit events on checkpoint creation', async () => { const events: any[] = []; manager.addEventListener((event) => { events.push(event); }); await fs.writeFile(path.join(tempDir, 'event-test.txt'), 'Event test'); await manager.createCheckpoint({ name: 'Event test' }); expect(events.length).toBeGreaterThan(0); expect(events.some((e) => e.type === 'created')).toBe(true); }); it('should get stats', async () => { const stats = await manager.getStats(); expect(stats.count).toBeGreaterThanOrEqual(0); // oldest 和 newest 可能为 null 如果没有检查点 if (stats.count > 0) { expect(stats.oldestTimestamp).not.toBeNull(); expect(stats.newestTimestamp).not.toBeNull(); } }); });