import { describe, it, expect, vi, beforeEach } from 'vitest'; import { computeDiff, formatDiff, countChanges, formatEditDiff, confirmFileChange, } from '../../../src/utils/diff.js'; // Mock readline vi.mock('readline', () => ({ createInterface: vi.fn(() => ({ question: vi.fn(), close: vi.fn(), })), })); // Mock fs/promises vi.mock('fs/promises', () => ({ readFile: vi.fn(), })); // Mock chalk (保留原始功能以便测试输出格式) vi.mock('chalk', () => ({ default: { yellow: (s: string) => `[yellow]${s}[/yellow]`, cyan: (s: string) => `[cyan]${s}[/cyan]`, white: (s: string) => `[white]${s}[/white]`, gray: (s: string) => `[gray]${s}[/gray]`, red: (s: string) => `[red]${s}[/red]`, green: (s: string) => `[green]${s}[/green]`, }, })); import * as readline from 'readline'; import * as fs from 'fs/promises'; describe('Diff - 差异比较扩展测试', () => { describe('computeDiff - 计算 diff', () => { it('新文件所有行都是添加', () => { const diff = computeDiff(null, 'line1\nline2\nline3'); expect(diff.isNew).toBe(true); expect(diff.oldContent).toBeNull(); expect(diff.hunks.length).toBe(1); expect(diff.hunks[0].lines.every(l => l.type === 'add')).toBe(true); expect(diff.hunks[0].newCount).toBe(3); }); it('相同内容返回空 hunks', () => { const content = 'line1\nline2\nline3'; const diff = computeDiff(content, content); expect(diff.isNew).toBe(false); // 相同内容可能没有实际变化的 hunks const hasChanges = diff.hunks.some(h => h.lines.some(l => l.type === 'add' || l.type === 'remove') ); expect(hasChanges).toBe(false); }); it('检测添加的行', () => { const oldContent = 'line1\nline2'; const newContent = 'line1\nline2\nline3'; const diff = computeDiff(oldContent, newContent); expect(diff.isNew).toBe(false); const addedLines = diff.hunks.flatMap(h => h.lines.filter(l => l.type === 'add')); expect(addedLines.some(l => l.content === 'line3')).toBe(true); }); it('检测删除的行', () => { const oldContent = 'line1\nline2\nline3'; const newContent = 'line1\nline2'; const diff = computeDiff(oldContent, newContent); const removedLines = diff.hunks.flatMap(h => h.lines.filter(l => l.type === 'remove')); expect(removedLines.some(l => l.content === 'line3')).toBe(true); }); it('检测修改的行(删除+添加)', () => { const oldContent = 'line1\nold line\nline3'; const newContent = 'line1\nnew line\nline3'; const diff = computeDiff(oldContent, newContent); const removedLines = diff.hunks.flatMap(h => h.lines.filter(l => l.type === 'remove')); const addedLines = diff.hunks.flatMap(h => h.lines.filter(l => l.type === 'add')); expect(removedLines.some(l => l.content === 'old line')).toBe(true); expect(addedLines.some(l => l.content === 'new line')).toBe(true); }); it('处理空文件', () => { const diff = computeDiff('', 'new content'); expect(diff.isNew).toBe(false); // 空到有内容是添加 const addedLines = diff.hunks.flatMap(h => h.lines.filter(l => l.type === 'add')); expect(addedLines.length).toBeGreaterThan(0); }); it('处理多个不连续的变更', () => { const oldContent = 'line1\nkeep1\nold2\nkeep2\nold3\nline6'; const newContent = 'line1\nkeep1\nnew2\nkeep2\nnew3\nline6'; const diff = computeDiff(oldContent, newContent); // 应该有变更 const changes = diff.hunks.flatMap(h => h.lines.filter(l => l.type === 'add' || l.type === 'remove') ); expect(changes.length).toBeGreaterThan(0); }); it('处理完全不同的内容', () => { const oldContent = 'completely\ndifferent\ncontent'; const newContent = 'totally\nnew\nstuff'; const diff = computeDiff(oldContent, newContent); expect(diff.hunks.length).toBeGreaterThan(0); }); }); describe('formatDiff - 格式化 diff', () => { it('新文件显示 +++ 新文件标记', () => { const diff = computeDiff(null, 'new content'); const formatted = formatDiff(diff, '/test/file.ts'); expect(formatted).toContain('新文件'); expect(formatted).toContain('/test/file.ts'); }); it('修改文件显示 --- 和 +++ 标记', () => { const diff = computeDiff('old', 'new'); const formatted = formatDiff(diff, '/test/file.ts'); expect(formatted).toContain('原文件'); expect(formatted).toContain('修改后'); }); it('显示 hunk 头部 @@ 信息', () => { const diff = computeDiff('old', 'new'); const formatted = formatDiff(diff, '/test/file.ts'); expect(formatted).toContain('@@'); }); it('添加行使用 + 前缀', () => { const diff = computeDiff('', 'added line'); const formatted = formatDiff(diff, '/test/file.ts'); expect(formatted).toContain('+ added line'); }); it('删除行使用 - 前缀', () => { const diff = computeDiff('removed line', ''); const formatted = formatDiff(diff, '/test/file.ts'); expect(formatted).toContain('- removed line'); }); }); describe('countChanges - 统计变更', () => { it('统计添加行数', () => { const diff = computeDiff(null, 'line1\nline2\nline3'); const changes = countChanges(diff); expect(changes.additions).toBe(3); expect(changes.deletions).toBe(0); }); it('统计删除行数', () => { const diff = computeDiff('line1\nline2\nline3', ''); const changes = countChanges(diff); expect(changes.deletions).toBe(3); }); it('统计混合变更', () => { const diff = computeDiff('old1\nold2', 'new1\nold2\nnew2'); const changes = countChanges(diff); // 具体数字取决于 diff 算法实现 expect(changes.additions).toBeGreaterThanOrEqual(0); expect(changes.deletions).toBeGreaterThanOrEqual(0); }); it('无变更返回零', () => { const diff = computeDiff('same', 'same'); const changes = countChanges(diff); expect(changes.additions + changes.deletions).toBe(0); }); }); describe('formatEditDiff - 编辑 diff 格式化', () => { it('显示删除和添加内容', () => { const formatted = formatEditDiff('old text', 'new text'); expect(formatted).toContain('old text'); expect(formatted).toContain('new text'); expect(formatted).toContain('-'); expect(formatted).toContain('+'); }); it('处理多行内容', () => { const formatted = formatEditDiff('old1\nold2', 'new1\nnew2'); expect(formatted).toContain('old1'); expect(formatted).toContain('old2'); expect(formatted).toContain('new1'); expect(formatted).toContain('new2'); }); it('包含变更内容标题', () => { const formatted = formatEditDiff('old', 'new'); expect(formatted).toContain('变更内容'); }); }); describe('confirmFileChange - 文件变更确认', () => { let consoleLogSpy: ReturnType; beforeEach(() => { vi.clearAllMocks(); consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); }); it('内容相同直接返回确认', async () => { vi.mocked(fs.readFile).mockResolvedValue('same content'); const result = await confirmFileChange('/test/file.ts', 'same content', 'write'); expect(result.confirmed).toBe(true); expect(result.remember).toBe(false); }); it('新文件(读取失败)显示 diff', async () => { vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT')); const mockRl = { question: vi.fn((_, callback) => callback('y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await confirmFileChange('/test/file.ts', 'new content', 'write'); expect(result.confirmed).toBe(true); }); it('用户输入 y 确认写入', async () => { vi.mocked(fs.readFile).mockResolvedValue('old content'); const mockRl = { question: vi.fn((_, callback) => callback('y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await confirmFileChange('/test/file.ts', 'new content', 'write'); expect(result.confirmed).toBe(true); expect(result.remember).toBe(false); }); it('用户输入 Y 确认并记住', async () => { vi.mocked(fs.readFile).mockResolvedValue('old content'); const mockRl = { question: vi.fn((_, callback) => callback('Y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await confirmFileChange('/test/file.ts', 'new content', 'write'); expect(result.confirmed).toBe(true); expect(result.remember).toBe(true); }); it('用户输入 n 取消操作', async () => { vi.mocked(fs.readFile).mockResolvedValue('old content'); const mockRl = { question: vi.fn((_, callback) => callback('n')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await confirmFileChange('/test/file.ts', 'new content', 'write'); expect(result.confirmed).toBe(false); expect(result.remember).toBe(false); }); it('用户输入 N 取消并记住', async () => { vi.mocked(fs.readFile).mockResolvedValue('old content'); const mockRl = { question: vi.fn((_, callback) => callback('N')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await confirmFileChange('/test/file.ts', 'new content', 'write'); expect(result.confirmed).toBe(false); expect(result.remember).toBe(true); }); it('无效输入默认取消', async () => { vi.mocked(fs.readFile).mockResolvedValue('old content'); const mockRl = { question: vi.fn((_, callback) => callback('invalid')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await confirmFileChange('/test/file.ts', 'new content', 'write'); expect(result.confirmed).toBe(false); expect(result.remember).toBe(false); }); it('显示变更预览信息', async () => { vi.mocked(fs.readFile).mockResolvedValue('old content'); const mockRl = { question: vi.fn((_, callback) => callback('y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); await confirmFileChange('/test/file.ts', 'new content', 'write'); const output = consoleLogSpy.mock.calls.flat().join('\n'); expect(output).toContain('文件变更预览'); expect(output).toContain('写入文件'); expect(output).toContain('/test/file.ts'); }); it('显示编辑操作类型', async () => { vi.mocked(fs.readFile).mockResolvedValue('old content'); const mockRl = { question: vi.fn((_, callback) => callback('y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); await confirmFileChange('/test/file.ts', 'new content', 'edit'); const output = consoleLogSpy.mock.calls.flat().join('\n'); expect(output).toContain('编辑文件'); }); afterEach(() => { consoleLogSpy.mockRestore(); }); }); });