/** * Git 深度集成测试 */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as path from 'path'; import * as fs from 'fs/promises'; import * as os from 'os'; import { execSync } from 'child_process'; import { GitRepo, GitManager, MessageGenerator, UndoManager, initGitManager, getGitManager, resetGitManager, DEFAULT_GIT_CONFIG, type DiffResult, } from '../../src/git/index.js'; describe('MessageGenerator', () => { const generator = new MessageGenerator({ style: 'conventional', includeFileList: true, maxLength: 72, useAI: false, }); it('should generate conventional commit message', () => { const diff: DiffResult = { files: [ { path: 'src/test.ts', status: 'modified', hunks: [], binary: false }, ], stats: { filesChanged: 1, insertions: 10, deletions: 5 }, }; const message = generator.generate(diff, ['src/test.ts']); expect(message).toMatch(/^(feat|fix|chore|test|docs|style|refactor)/); expect(message).toContain('test'); }); it('should detect test type for test files', () => { const diff: DiffResult = { files: [ { path: 'tests/foo.test.ts', status: 'added', hunks: [], binary: false }, ], stats: { filesChanged: 1, insertions: 50, deletions: 0 }, }; const message = generator.generate(diff, ['tests/foo.test.ts']); expect(message).toMatch(/^test/); }); it('should detect docs type for markdown files', () => { const diff: DiffResult = { files: [ { path: 'README.md', status: 'modified', hunks: [], binary: false }, ], stats: { filesChanged: 1, insertions: 20, deletions: 10 }, }; const message = generator.generate(diff, ['README.md']); expect(message).toMatch(/^docs/); }); it('should include scope when files are in same directory', () => { const diff: DiffResult = { files: [ { path: 'src/git/repo.ts', status: 'modified', hunks: [], binary: false }, { path: 'src/git/types.ts', status: 'modified', hunks: [], binary: false }, ], stats: { filesChanged: 2, insertions: 30, deletions: 20 }, }; const message = generator.generate(diff, ['src/git/repo.ts', 'src/git/types.ts']); expect(message).toContain('(git)'); }); it('should generate simple format message', () => { const simpleGenerator = new MessageGenerator({ style: 'simple', includeFileList: false, maxLength: 72, useAI: false, }); const diff: DiffResult = { files: [ { path: 'src/index.ts', status: 'modified', hunks: [], binary: false }, ], stats: { filesChanged: 1, insertions: 5, deletions: 2 }, }; const message = simpleGenerator.generate(diff, ['src/index.ts']); expect(message).toContain('index'); expect(message.length).toBeLessThanOrEqual(72); }); }); describe('GitRepo', () => { let tempDir: string; let repo: GitRepo; beforeEach(async () => { // 创建临时目录 tempDir = path.join(os.tmpdir(), `git-test-${Date.now()}`); await fs.mkdir(tempDir, { recursive: true }); // 初始化 Git 仓库 execSync('git init', { cwd: tempDir, stdio: 'ignore' }); execSync('git config user.email "test@test.com"', { cwd: tempDir, stdio: 'ignore' }); execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' }); // 创建初始提交 await fs.writeFile(path.join(tempDir, 'README.md'), '# Test'); execSync('git add .', { cwd: tempDir, stdio: 'ignore' }); execSync('git commit -m "Initial commit"', { cwd: tempDir, stdio: 'ignore' }); repo = new GitRepo(tempDir, DEFAULT_GIT_CONFIG); await repo.initialize(); }); afterEach(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // ignore cleanup errors } }); it('should initialize and detect git repo', async () => { const isRepo = await repo.initialize(); expect(isRepo).toBe(true); }); it('should get status', async () => { const status = await repo.getStatus(); expect(status.branch).toMatch(/^(main|master)$/); expect(status.isDirty).toBe(false); }); it('should detect dirty files', async () => { await fs.writeFile(path.join(tempDir, 'new.txt'), 'content'); const isDirty = await repo.isDirty(); expect(isDirty).toBe(true); }); it('should get diff', async () => { await fs.writeFile(path.join(tempDir, 'README.md'), '# Test\n\nUpdated'); const diff = await repo.getDiff(); expect(diff.files.length).toBe(1); expect(diff.files[0].path).toBe('README.md'); }); it('should commit changes', async () => { await fs.writeFile(path.join(tempDir, 'new.txt'), 'new content'); const result = await repo.commit({ files: ['new.txt'], message: 'Add new file', aiEdits: true, }); expect(result.success).toBe(true); expect(result.shortHash).toBeDefined(); expect(repo.isAICommit(result.shortHash!)).toBe(true); }); it('should get recent commits', async () => { const commits = await repo.getRecentCommits(5); expect(commits.length).toBeGreaterThanOrEqual(1); expect(commits[0].message).toBe('Initial commit'); }); it('should get HEAD commit', async () => { const head = await repo.getHeadCommit(); expect(head).toBeDefined(); expect(head!.length).toBe(40); }); it('should get current branch', async () => { const branch = await repo.getCurrentBranch(); expect(['main', 'master']).toContain(branch); }); }); describe('UndoManager', () => { let tempDir: string; let repo: GitRepo; let undoManager: UndoManager; beforeEach(async () => { tempDir = path.join(os.tmpdir(), `undo-test-${Date.now()}`); await fs.mkdir(tempDir, { recursive: true }); execSync('git init', { cwd: tempDir, stdio: 'ignore' }); execSync('git config user.email "test@test.com"', { cwd: tempDir, stdio: 'ignore' }); execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' }); await fs.writeFile(path.join(tempDir, 'README.md'), '# Test'); execSync('git add .', { cwd: tempDir, stdio: 'ignore' }); execSync('git commit -m "Initial commit"', { cwd: tempDir, stdio: 'ignore' }); repo = new GitRepo(tempDir, DEFAULT_GIT_CONFIG); await repo.initialize(); undoManager = new UndoManager(repo, { maxHistory: 50 }); }); afterEach(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // ignore } }); it('should return error when nothing to undo', async () => { const result = await undoManager.undo(); expect(result.success).toBe(false); expect(result.message).toContain('Nothing to undo'); }); it('should record and undo AI commit', async () => { // 创建 AI 提交 await fs.writeFile(path.join(tempDir, 'new.txt'), 'content'); const commitResult = await repo.commit({ files: ['new.txt'], message: 'Add new file', aiEdits: true, }); expect(commitResult.success).toBe(true); // 记录到 undo 历史 undoManager.recordCommit( commitResult.hash!, commitResult.shortHash!, commitResult.message!, ['new.txt'] ); // 执行 undo const undoResult = await undoManager.undo(); expect(undoResult.success).toBe(true); expect(undoResult.commitHash).toBe(commitResult.shortHash); }); it('should reject undo when repository changed', async () => { // 创建 AI 提交 await fs.writeFile(path.join(tempDir, 'new.txt'), 'content'); const commitResult = await repo.commit({ files: ['new.txt'], message: 'Add new file', aiEdits: true, }); undoManager.recordCommit( commitResult.hash!, commitResult.shortHash!, commitResult.message!, ['new.txt'] ); // 在 AI 提交后又创建了一个手动提交 await fs.writeFile(path.join(tempDir, 'manual.txt'), 'manual'); execSync('git add .', { cwd: tempDir, stdio: 'ignore' }); execSync('git commit -m "Manual commit"', { cwd: tempDir, stdio: 'ignore' }); // 尝试 undo const undoResult = await undoManager.undo(); expect(undoResult.success).toBe(false); expect(undoResult.message).toContain('does not match'); }); it('should get undo preview', () => { // 没有记录时返回 null expect(undoManager.getUndoPreview()).toBeNull(); // 记录后可以获取预览 undoManager.recordCommit('abc1234', 'abc1234', 'Test commit', ['test.txt']); const preview = undoManager.getUndoPreview(); expect(preview).not.toBeNull(); expect(preview!.message).toBe('Test commit'); }); it('should check if can undo', async () => { let canUndo = await undoManager.canUndo(); expect(canUndo.canUndo).toBe(false); // 创建并记录 AI 提交 await fs.writeFile(path.join(tempDir, 'new.txt'), 'content'); const commitResult = await repo.commit({ files: ['new.txt'], message: 'Add new file', aiEdits: true, }); undoManager.recordCommit( commitResult.hash!, commitResult.shortHash!, commitResult.message!, ['new.txt'] ); canUndo = await undoManager.canUndo(); expect(canUndo.canUndo).toBe(true); }); }); describe('GitManager', () => { let tempDir: string; beforeEach(async () => { resetGitManager(); tempDir = path.join(os.tmpdir(), `manager-test-${Date.now()}`); await fs.mkdir(tempDir, { recursive: true }); execSync('git init', { cwd: tempDir, stdio: 'ignore' }); execSync('git config user.email "test@test.com"', { cwd: tempDir, stdio: 'ignore' }); execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' }); await fs.writeFile(path.join(tempDir, 'README.md'), '# Test'); execSync('git add .', { cwd: tempDir, stdio: 'ignore' }); execSync('git commit -m "Initial commit"', { cwd: tempDir, stdio: 'ignore' }); }); afterEach(async () => { resetGitManager(); try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // ignore } }); it('should initialize git manager', async () => { const manager = await initGitManager(tempDir); expect(manager).not.toBeNull(); expect(manager!.isInitialized()).toBe(true); }); it('should return null for non-git directory', async () => { const nonGitDir = path.join(os.tmpdir(), `non-git-${Date.now()}`); await fs.mkdir(nonGitDir, { recursive: true }); const manager = await initGitManager(nonGitDir); expect(manager).toBeNull(); await fs.rm(nonGitDir, { recursive: true, force: true }); }); it('should get global manager', async () => { expect(getGitManager()).toBeNull(); await initGitManager(tempDir); expect(getGitManager()).not.toBeNull(); }); it('should get status', async () => { const manager = await initGitManager(tempDir); const status = await manager!.getStatus(); expect(status.branch).toMatch(/^(main|master)$/); expect(status.isDirty).toBe(false); }); it('should commit manually', async () => { const manager = await initGitManager(tempDir); await fs.writeFile(path.join(tempDir, 'new.txt'), 'content'); const result = await manager!.commit({ files: ['new.txt'], message: 'Add new file', }); expect(result.success).toBe(true); expect(result.shortHash).toBeDefined(); }); it('should undo AI commit', async () => { const manager = await initGitManager(tempDir); // 创建并提交文件 await fs.writeFile(path.join(tempDir, 'new.txt'), 'content'); const commitResult = await manager!.commit({ files: ['new.txt'], message: 'Add new file', }); expect(commitResult.success).toBe(true); // 执行 undo const undoResult = await manager!.undo(); expect(undoResult.success).toBe(true); }); it('should get undo history', async () => { const manager = await initGitManager(tempDir); expect(manager!.getUndoHistory()).toHaveLength(0); await fs.writeFile(path.join(tempDir, 'new.txt'), 'content'); await manager!.commit({ files: ['new.txt'], message: 'Add new file', }); expect(manager!.getUndoHistory()).toHaveLength(1); }); it('should emit events', async () => { const manager = await initGitManager(tempDir); const events: any[] = []; manager!.addEventListener((event) => events.push(event)); await fs.writeFile(path.join(tempDir, 'new.txt'), 'content'); await manager!.commit({ files: ['new.txt'], message: 'Add new file', }); expect(events.length).toBe(1); expect(events[0].type).toBe('commit'); }); });