Files
kurihada 1d69fd876d feat(permission): 实现 WebSocket 权限确认机制
重构权限系统,将终端 UI 代码从 core 模块移除,实现基于 WebSocket 的权限确认流程:

Core 模块清理:
- 删除 permission/prompt.ts 和 file-prompt.ts(终端交互)
- 删除 diff.ts 中的 chalk 渲染函数
- 删除 config.ts 中的 inquirer 交互
- 移除 chalk 依赖

Server 权限处理:
- 新增 permission/handler.ts,实现 WebSocket 权限请求/响应
- 更新 agent/adapter.ts 设置权限回调
- 更新 ws.ts 处理 permission_response 消息

Web 权限组件:
- 新增 PermissionDialog 组件,显示权限请求详情和 Diff
- 更新 useChat hook 管理权限状态
- 更新 Chat 页面集成权限弹窗
2025-12-13 01:09:35 +08:00

280 lines
7.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { computeDiff, countChanges } from '../../../src/utils/diff.js';
describe('computeDiff - 计算文件差异', () => {
describe('新文件', () => {
it('新文件所有行标记为新增', () => {
const diff = computeDiff(null, 'line1\nline2\nline3');
expect(diff.isNew).toBe(true);
expect(diff.oldContent).toBeNull();
expect(diff.hunks).toHaveLength(1);
const hunk = diff.hunks[0];
expect(hunk.oldStart).toBe(0);
expect(hunk.oldCount).toBe(0);
expect(hunk.newStart).toBe(1);
expect(hunk.newCount).toBe(3);
expect(hunk.lines).toHaveLength(3);
expect(hunk.lines.every((l) => l.type === 'add')).toBe(true);
});
it('空新文件', () => {
const diff = computeDiff(null, '');
expect(diff.isNew).toBe(true);
expect(diff.hunks).toHaveLength(1);
expect(diff.hunks[0].lines).toHaveLength(1); // 空行也是一行
});
});
describe('修改文件', () => {
it('相同内容无变化', () => {
const content = 'line1\nline2\nline3';
const diff = computeDiff(content, content);
expect(diff.isNew).toBe(false);
expect(diff.hunks).toHaveLength(0);
});
it('单行修改', () => {
const oldContent = 'line1\nline2\nline3';
const newContent = 'line1\nmodified\nline3';
const diff = computeDiff(oldContent, newContent);
expect(diff.isNew).toBe(false);
expect(diff.hunks.length).toBeGreaterThan(0);
// 应该有删除和新增
const allLines = diff.hunks.flatMap((h) => h.lines);
expect(allLines.some((l) => l.type === 'remove' && l.content === 'line2')).toBe(true);
expect(allLines.some((l) => l.type === 'add' && l.content === 'modified')).toBe(true);
});
it('添加行', () => {
const oldContent = 'line1\nline3';
const newContent = 'line1\nline2\nline3';
const diff = computeDiff(oldContent, newContent);
expect(diff.isNew).toBe(false);
const allLines = diff.hunks.flatMap((h) => h.lines);
expect(allLines.some((l) => l.type === 'add' && l.content === 'line2')).toBe(true);
});
it('删除行', () => {
const oldContent = 'line1\nline2\nline3';
const newContent = 'line1\nline3';
const diff = computeDiff(oldContent, newContent);
expect(diff.isNew).toBe(false);
const allLines = diff.hunks.flatMap((h) => h.lines);
expect(allLines.some((l) => l.type === 'remove' && l.content === 'line2')).toBe(true);
});
it('全部替换', () => {
const oldContent = 'old1\nold2\nold3';
const newContent = 'new1\nnew2';
const diff = computeDiff(oldContent, newContent);
expect(diff.isNew).toBe(false);
expect(diff.hunks.length).toBeGreaterThan(0);
const changes = countChanges(diff);
expect(changes.deletions).toBeGreaterThan(0);
expect(changes.additions).toBeGreaterThan(0);
});
});
describe('特殊情况', () => {
it('空文件变为非空', () => {
const diff = computeDiff('', 'new content');
expect(diff.isNew).toBe(false);
expect(diff.hunks.length).toBeGreaterThan(0);
});
it('非空文件变为空', () => {
const diff = computeDiff('old content', '');
expect(diff.isNew).toBe(false);
expect(diff.hunks.length).toBeGreaterThan(0);
});
it('包含空行的内容', () => {
const oldContent = 'line1\n\nline3';
const newContent = 'line1\nline2\n\nline3';
const diff = computeDiff(oldContent, newContent);
expect(diff.isNew).toBe(false);
// 应该正确处理空行
});
it('单行文件', () => {
const diff = computeDiff('old', 'new');
expect(diff.isNew).toBe(false);
expect(diff.hunks.length).toBeGreaterThan(0);
});
});
describe('行号正确性', () => {
it('新增行有正确的行号', () => {
const diff = computeDiff(null, 'line1\nline2\nline3');
const lines = diff.hunks[0].lines;
expect(lines[0].lineNumber).toBe(1);
expect(lines[1].lineNumber).toBe(2);
expect(lines[2].lineNumber).toBe(3);
});
});
});
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('old1\nold2', 'new1\nold2\nnew2');
const changes = countChanges(diff);
expect(changes.additions).toBeGreaterThan(0);
expect(changes.deletions).toBeGreaterThan(0);
});
it('空 diff 返回零', () => {
const diff = computeDiff('same', 'same');
const changes = countChanges(diff);
expect(changes.additions).toBe(0);
expect(changes.deletions).toBe(0);
});
});
describe('DiffResult 结构', () => {
it('包含所有必要字段', () => {
const diff = computeDiff('old', 'new');
expect(diff).toHaveProperty('oldContent');
expect(diff).toHaveProperty('newContent');
expect(diff).toHaveProperty('isNew');
expect(diff).toHaveProperty('hunks');
});
it('hunk 包含所有必要字段', () => {
const diff = computeDiff('old', 'new');
if (diff.hunks.length > 0) {
const hunk = diff.hunks[0];
expect(hunk).toHaveProperty('oldStart');
expect(hunk).toHaveProperty('oldCount');
expect(hunk).toHaveProperty('newStart');
expect(hunk).toHaveProperty('newCount');
expect(hunk).toHaveProperty('lines');
}
});
it('line 包含所有必要字段', () => {
const diff = computeDiff(null, 'content');
const line = diff.hunks[0].lines[0];
expect(line).toHaveProperty('type');
expect(line).toHaveProperty('lineNumber');
expect(line).toHaveProperty('content');
});
});
describe('LCS 算法测试', () => {
it('相同前缀保留', () => {
const oldContent = 'prefix\ncommon\nold';
const newContent = 'prefix\ncommon\nnew';
const diff = computeDiff(oldContent, newContent);
// common 行应该保持为上下文
const contextLines = diff.hunks.flatMap((h) =>
h.lines.filter((l) => l.type === 'context')
);
// prefix 和 common 可能作为上下文保留
expect(diff.hunks.length).toBeGreaterThan(0);
});
it('相同后缀保留', () => {
const oldContent = 'old\ncommon\nsuffix';
const newContent = 'new\ncommon\nsuffix';
const diff = computeDiff(oldContent, newContent);
expect(diff.hunks.length).toBeGreaterThan(0);
});
it('完全不同的内容', () => {
const oldContent = 'a\nb\nc';
const newContent = 'x\ny\nz';
const diff = computeDiff(oldContent, newContent);
const changes = countChanges(diff);
expect(changes.additions).toBe(3);
expect(changes.deletions).toBe(3);
});
});
describe('实际代码场景', () => {
it('函数修改', () => {
const oldContent = `function hello() {
console.log("Hello");
}`;
const newContent = `function hello() {
console.log("Hello World");
return true;
}`;
const diff = computeDiff(oldContent, newContent);
expect(diff.isNew).toBe(false);
expect(diff.hunks.length).toBeGreaterThan(0);
const changes = countChanges(diff);
expect(changes.additions).toBeGreaterThan(0);
});
it('导入语句添加', () => {
const oldContent = `import { a } from 'module';
export function test() {}`;
const newContent = `import { a } from 'module';
import { b } from 'another';
export function test() {}`;
const diff = computeDiff(oldContent, newContent);
const changes = countChanges(diff);
expect(changes.additions).toBeGreaterThan(0);
});
it('配置文件修改', () => {
const oldContent = `{
"name": "test",
"version": "1.0.0"
}`;
const newContent = `{
"name": "test",
"version": "1.1.0",
"description": "Added description"
}`;
const diff = computeDiff(oldContent, newContent);
expect(diff.hunks.length).toBeGreaterThan(0);
});
});