2abea47386
删除以下工具及相关文件: - copy_file: 复制文件 - create_directory: 创建目录 - delete_file: 删除文件 - move_file: 移动文件 - search_files: 搜索文件 清理范围: - 工具实现文件 (5个) - 工具描述文件 (5个) - 单元测试文件 (6个) - Agent presets 中的引用 - Checkpoint 系统中的触发类型 - Hook 系统中的相关处理
298 lines
9.4 KiB
TypeScript
298 lines
9.4 KiB
TypeScript
/**
|
|
* 检查点系统测试
|
|
*/
|
|
|
|
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('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();
|
|
}
|
|
});
|
|
});
|