bca19b7741
新增测试文件: - agent/executor-extended.test.ts, presets/ - context/manager-extended.test.ts - core/agent.test.ts, providers.test.ts - lsp/cli.test.ts, client-extended.test.ts, index.test.ts - permission/file-prompt.test.ts, prompt.test.ts - skills/builtin/ - tools/filesystem/write_file-extended.test.ts - tools/git/git_commit-extended.test.ts - tools/load_description.test.ts - tools/todo/todo-manager.test.ts - tools/tool-search.test.ts - types/ - utils/config-extended.test.ts, diff-extended.test.ts 修改现有测试: - agent/manager.test.ts - tools/skill/skill.test.ts - utils/config.test.ts, diff.test.ts, image.test.ts
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
promptFileWrite,
|
|
promptFileEdit,
|
|
promptFilePermission,
|
|
} from '../../../src/permission/file-prompt.js';
|
|
import type { FilePermissionContext } from '../../../src/permission/types.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) => s,
|
|
cyan: (s: string) => s,
|
|
white: (s: string) => s,
|
|
gray: (s: string) => s,
|
|
red: (s: string) => s,
|
|
green: (s: string) => s,
|
|
},
|
|
}));
|
|
|
|
// Mock diff utils
|
|
vi.mock('../../../src/utils/diff.js', () => ({
|
|
computeDiff: vi.fn(() => ({
|
|
isNew: false,
|
|
oldContent: 'old',
|
|
newContent: 'new',
|
|
hunks: [],
|
|
})),
|
|
formatDiff: vi.fn(() => 'diff output'),
|
|
countChanges: vi.fn(() => ({ additions: 5, deletions: 3 })),
|
|
formatEditDiff: vi.fn(() => 'edit diff output'),
|
|
}));
|
|
|
|
import * as readline from 'readline';
|
|
import * as fs from 'fs/promises';
|
|
import { computeDiff, countChanges } from '../../../src/utils/diff.js';
|
|
|
|
describe('File Prompt - 文件操作提示', () => {
|
|
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
consoleLogSpy.mockRestore();
|
|
});
|
|
|
|
describe('promptFileWrite - 文件写入提示', () => {
|
|
const baseContext: FilePermissionContext = {
|
|
operation: 'write',
|
|
path: '/test/file.ts',
|
|
workdir: '/test',
|
|
toolName: 'write_file',
|
|
newContent: 'new content',
|
|
};
|
|
|
|
it('无内容时使用简单确认', async () => {
|
|
const ctx: FilePermissionContext = {
|
|
...baseContext,
|
|
newContent: undefined,
|
|
};
|
|
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
const result = await promptFileWrite(ctx);
|
|
|
|
expect(result.allow).toBe(true);
|
|
});
|
|
|
|
it('内容相同时直接允许', async () => {
|
|
vi.mocked(fs.readFile).mockResolvedValue('new content');
|
|
|
|
const result = await promptFileWrite(baseContext);
|
|
|
|
expect(result).toEqual({ allow: true, remember: false });
|
|
});
|
|
|
|
it('新文件显示新增行数', async () => {
|
|
vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
vi.mocked(computeDiff).mockReturnValue({
|
|
isNew: true,
|
|
oldContent: null,
|
|
newContent: 'new content',
|
|
hunks: [],
|
|
} as any);
|
|
vi.mocked(countChanges).mockReturnValue({ additions: 10, deletions: 0 });
|
|
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
await promptFileWrite(baseContext);
|
|
|
|
const calls = consoleLogSpy.mock.calls.flat().join('\n');
|
|
expect(calls).toContain('新文件');
|
|
expect(calls).toContain('+10 行');
|
|
});
|
|
|
|
it('修改文件显示增删行数', async () => {
|
|
vi.mocked(fs.readFile).mockResolvedValue('old content');
|
|
vi.mocked(computeDiff).mockReturnValue({
|
|
isNew: false,
|
|
oldContent: 'old content',
|
|
newContent: 'new content',
|
|
hunks: [],
|
|
} as any);
|
|
vi.mocked(countChanges).mockReturnValue({ additions: 5, deletions: 3 });
|
|
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
await promptFileWrite(baseContext);
|
|
|
|
const calls = consoleLogSpy.mock.calls.flat().join('\n');
|
|
expect(calls).toContain('+5 行');
|
|
expect(calls).toContain('-3 行');
|
|
});
|
|
|
|
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 promptFileWrite(baseContext);
|
|
|
|
expect(result).toEqual({ allow: true, remember: 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 promptFileWrite(baseContext);
|
|
|
|
expect(result).toEqual({ allow: true, remember: 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 promptFileWrite(baseContext);
|
|
|
|
expect(result).toEqual({ allow: false, remember: 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 promptFileWrite(baseContext);
|
|
|
|
expect(result).toEqual({ allow: false, remember: 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 promptFileWrite(baseContext);
|
|
|
|
expect(result).toEqual({ allow: false, remember: false });
|
|
});
|
|
|
|
it('超长 diff 被截断', async () => {
|
|
vi.mocked(fs.readFile).mockResolvedValue('old content');
|
|
// 模拟超过 50 行的 diff
|
|
const longDiff = Array(100).fill('line').join('\n');
|
|
const { formatDiff } = await import('../../../src/utils/diff.js');
|
|
vi.mocked(formatDiff).mockReturnValue(longDiff);
|
|
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
await promptFileWrite(baseContext);
|
|
|
|
const calls = consoleLogSpy.mock.calls.flat().join('\n');
|
|
expect(calls).toContain('省略');
|
|
});
|
|
});
|
|
|
|
describe('promptFileEdit - 文件编辑提示', () => {
|
|
const baseContext: FilePermissionContext = {
|
|
operation: 'edit',
|
|
path: '/test/file.ts',
|
|
workdir: '/test',
|
|
toolName: 'edit_file',
|
|
oldContent: 'old text',
|
|
newContent: 'new text',
|
|
};
|
|
|
|
it('无内容时使用简单确认', async () => {
|
|
const ctx: FilePermissionContext = {
|
|
...baseContext,
|
|
oldContent: undefined,
|
|
newContent: undefined,
|
|
};
|
|
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
const result = await promptFileEdit(ctx);
|
|
|
|
expect(result.allow).toBe(true);
|
|
});
|
|
|
|
it('显示编辑 diff', async () => {
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
await promptFileEdit(baseContext);
|
|
|
|
const calls = consoleLogSpy.mock.calls.flat().join('\n');
|
|
expect(calls).toContain('文件编辑预览');
|
|
expect(calls).toContain('/test/file.ts');
|
|
});
|
|
|
|
it('用户确认后返回决定', async () => {
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('Y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
const result = await promptFileEdit(baseContext);
|
|
|
|
expect(result).toEqual({ allow: true, remember: true });
|
|
});
|
|
});
|
|
|
|
describe('promptFilePermission - 统一入口', () => {
|
|
it('write 操作调用 promptFileWrite', async () => {
|
|
vi.mocked(fs.readFile).mockResolvedValue('same content');
|
|
|
|
const ctx: FilePermissionContext = {
|
|
operation: 'write',
|
|
path: '/test/file.ts',
|
|
workdir: '/test',
|
|
toolName: 'write_file',
|
|
newContent: 'same content',
|
|
};
|
|
|
|
const result = await promptFilePermission(ctx);
|
|
|
|
// 内容相同直接允许
|
|
expect(result).toEqual({ allow: true, remember: false });
|
|
});
|
|
|
|
it('edit 操作调用 promptFileEdit', async () => {
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
const ctx: FilePermissionContext = {
|
|
operation: 'edit',
|
|
path: '/test/file.ts',
|
|
workdir: '/test',
|
|
toolName: 'edit_file',
|
|
oldContent: 'old',
|
|
newContent: 'new',
|
|
};
|
|
|
|
await promptFilePermission(ctx);
|
|
|
|
const calls = consoleLogSpy.mock.calls.flat().join('\n');
|
|
expect(calls).toContain('文件编辑预览');
|
|
});
|
|
|
|
it('其他操作使用简单确认', async () => {
|
|
const mockRl = {
|
|
question: vi.fn((_, callback) => callback('y')),
|
|
close: vi.fn(),
|
|
};
|
|
vi.mocked(readline.createInterface).mockReturnValue(mockRl as any);
|
|
|
|
const ctx: FilePermissionContext = {
|
|
operation: 'delete',
|
|
path: '/test/file.ts',
|
|
workdir: '/test',
|
|
toolName: 'delete_file',
|
|
};
|
|
|
|
await promptFilePermission(ctx);
|
|
|
|
const calls = consoleLogSpy.mock.calls.flat().join('\n');
|
|
expect(calls).toContain('文件操作确认');
|
|
});
|
|
});
|
|
|
|
describe('确认选项显示', () => {
|
|
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 promptFileWrite({
|
|
operation: 'write',
|
|
path: '/test/file.ts',
|
|
workdir: '/test',
|
|
toolName: 'write_file',
|
|
newContent: 'new content',
|
|
});
|
|
|
|
const calls = consoleLogSpy.mock.calls.flat().join('\n');
|
|
expect(calls).toContain('[y]');
|
|
expect(calls).toContain('[Y]');
|
|
expect(calls).toContain('[n]');
|
|
expect(calls).toContain('[N]');
|
|
expect(calls).toContain('确认执行');
|
|
expect(calls).toContain('拒绝执行');
|
|
expect(calls).toContain('记住');
|
|
});
|
|
});
|
|
|
|
describe('输入处理', () => {
|
|
it('输入被 trim', 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 promptFileWrite({
|
|
operation: 'write',
|
|
path: '/test/file.ts',
|
|
workdir: '/test',
|
|
toolName: 'write_file',
|
|
newContent: 'new content',
|
|
});
|
|
|
|
expect(result).toEqual({ allow: true, remember: false });
|
|
});
|
|
});
|
|
});
|