Files
ai-terminal-assistant/packages/core/tests/checkpoint/checkpoint.test.ts
T
kurihada 5e32375f0e feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更:
- 采用 pnpm workspaces 实现 Monorepo 结构
- 将现有代码迁移到 packages/core
- 新增 packages/server HTTP 服务层

Server 功能:
- REST API: 会话管理、工具管理、配置管理
- WebSocket: 实时双向通信支持
- SSE: 服务端事件推送
- Hono + Bun 作为运行时

API 端点:
- GET/POST /api/sessions - 会话 CRUD
- GET/POST /api/sessions/:id/messages - 消息管理
- GET /api/sessions/:id/events - SSE 事件流
- WS /api/ws/:sessionId - WebSocket 连接
- GET/POST /api/tools - 工具管理
- GET/PUT /api/config - 配置管理
2025-12-12 10:42:20 +08:00

299 lines
9.5 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('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();
}
});
});