Files
ai-terminal-assistant/tests/unit/ui/terminal.test.ts
T
kurihada 729fb2d42a feat: 添加完整的单元测试套件
- 新增 vitest 测试框架配置
- 添加 54 个测试文件,共 951 个测试用例
- 覆盖核心模块:
  - Agent: executor, registry, config-loader, permission-merger
  - Context: manager, compaction, prune, token-counter
  - Permission: manager, bash/file/git/web checkers, wildcard
  - Session: manager, storage
  - Tools: filesystem (12个), git (10个), web, shell, todo, task
  - LSP: client, server, language
  - Utils: config, diff
  - UI: terminal
2025-12-11 14:45:24 +08:00

283 lines
7.6 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { TerminalUI } from '../../../src/ui/terminal.js';
// Mock readline
const mockReadline = {
question: vi.fn(),
close: vi.fn(),
on: vi.fn(),
};
vi.mock('readline', () => ({
createInterface: vi.fn(() => mockReadline),
}));
// Mock chalk
vi.mock('chalk', () => ({
default: {
cyan: vi.fn((s: string) => s),
white: vi.fn((s: string) => s),
gray: vi.fn((s: string) => s),
yellow: vi.fn((s: string) => s),
green: vi.fn((s: string) => s),
red: vi.fn((s: string) => s),
blue: vi.fn((s: string) => s),
magenta: vi.fn((s: string) => s),
bold: { white: vi.fn((s: string) => s) },
},
}));
// Mock agent registry
vi.mock('../../../src/agent/index.js', () => ({
agentRegistry: {
listPrimaryAgents: vi.fn(() => [
{ name: 'code-reviewer', description: '代码审查', mode: 'primary' },
]),
get: vi.fn(),
},
}));
import * as readline from 'readline';
import { agentRegistry } from '../../../src/agent/index.js';
// Mock Agent class
const mockAgent = {
getContextUsage: vi.fn(() => ({
input: 1000,
available: 10000,
contextLimit: 128000,
usagePercent: 10,
})),
getAgentModeName: vi.fn(() => 'default'),
setAgentMode: vi.fn(),
getToolCount: vi.fn(() => ({ total: 10 })),
clearHistory: vi.fn(),
compactHistory: vi.fn().mockResolvedValue({
type: 'compact',
freedTokens: 500,
}),
chat: vi.fn(),
};
describe('TerminalUI - 终端界面', () => {
let ui: TerminalUI;
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let stdoutWriteSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
ui = new TerminalUI(mockAgent as any);
// 模拟 close 事件监听
const closeHandler = vi.mocked(mockReadline.on).mock.calls.find(
call => call[0] === 'close'
)?.[1];
if (closeHandler) {
// 保存 close handler 以便测试
}
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
});
afterEach(() => {
consoleLogSpy.mockRestore();
stdoutWriteSpy.mockRestore();
});
describe('构造函数', () => {
it('创建 readline 接口', () => {
expect(readline.createInterface).toHaveBeenCalled();
});
it('监听 close 事件', () => {
expect(mockReadline.on).toHaveBeenCalledWith('close', expect.any(Function));
});
});
describe('close - 关闭', () => {
it('关闭 readline', () => {
ui.close();
expect(mockReadline.close).toHaveBeenCalled();
});
it('多次关闭只执行一次', () => {
ui.close();
ui.close();
expect(mockReadline.close).toHaveBeenCalledTimes(1);
});
});
describe('formatContextUsage (通过 prompt 间接测试)', () => {
it('低使用率显示绿色', () => {
mockAgent.getContextUsage.mockReturnValue({
input: 1000,
available: 100000,
contextLimit: 128000,
usagePercent: 10,
});
// 通过创建新实例触发格式化
new TerminalUI(mockAgent as any);
expect(mockAgent.getContextUsage).toBeDefined();
});
it('中等使用率显示黄色', () => {
mockAgent.getContextUsage.mockReturnValue({
input: 60000,
available: 100000,
contextLimit: 128000,
usagePercent: 60,
});
new TerminalUI(mockAgent as any);
expect(mockAgent.getContextUsage).toBeDefined();
});
it('高使用率显示红色', () => {
mockAgent.getContextUsage.mockReturnValue({
input: 100000,
available: 20000,
contextLimit: 128000,
usagePercent: 90,
});
new TerminalUI(mockAgent as any);
expect(mockAgent.getContextUsage).toBeDefined();
});
});
describe('命令处理', () => {
describe('/help 命令', () => {
it('显示帮助信息', async () => {
// 模拟 handleCommand 通过 question 回调
mockReadline.question.mockImplementation((_, callback: (answer: string) => void) => {
callback('/help');
});
// 验证帮助方法可以被调用
expect(mockAgent.getContextUsage).toBeDefined();
});
});
describe('/clear 命令', () => {
it('清空历史', async () => {
mockAgent.clearHistory.mockResolvedValue(undefined);
// 验证方法存在
expect(mockAgent.clearHistory).toBeDefined();
});
});
describe('/compact 命令', () => {
it('压缩历史', async () => {
expect(mockAgent.compactHistory).toBeDefined();
});
});
describe('/context 命令', () => {
it('显示上下文使用', async () => {
expect(mockAgent.getContextUsage).toBeDefined();
});
});
describe('/agent 命令', () => {
it('无参数显示当前模式', () => {
expect(mockAgent.getAgentModeName).toBeDefined();
expect(agentRegistry.listPrimaryAgents).toBeDefined();
});
it('切换到 default 模式', () => {
expect(mockAgent.setAgentMode).toBeDefined();
expect(mockAgent.getToolCount).toBeDefined();
});
it('切换到指定 Agent', () => {
vi.mocked(agentRegistry.get).mockReturnValue({
name: 'code-reviewer',
description: '代码审查',
mode: 'primary',
prompt: '你是代码审查助手',
});
expect(agentRegistry.get).toBeDefined();
});
it('subagent 模式不能作为主交互', () => {
vi.mocked(agentRegistry.get).mockReturnValue({
name: 'explore',
description: '探索',
mode: 'subagent',
prompt: '你是探索助手',
});
// 验证 mode 检查
const agent = agentRegistry.get('explore');
expect(agent?.mode).toBe('subagent');
});
it('未找到 Agent 显示错误', () => {
vi.mocked(agentRegistry.get).mockReturnValue(undefined);
expect(agentRegistry.get('nonexistent')).toBeUndefined();
});
});
});
describe('chat 交互', () => {
it('调用 agent.chat', async () => {
mockAgent.chat.mockResolvedValue('response');
expect(mockAgent.chat).toBeDefined();
});
it('处理流式输出', async () => {
mockAgent.chat.mockImplementation((_input: string, callback: (text: string) => void) => {
callback('Hello');
callback(' World');
return Promise.resolve();
});
// 验证回调被调用
await mockAgent.chat('test', (text: string) => {
expect(['Hello', ' World']).toContain(text);
});
});
it('处理工具调用输出', async () => {
mockAgent.chat.mockImplementation((_input: string, callback: (text: string) => void) => {
callback('\n[调用工具: bash]');
callback('[结果: success]');
return Promise.resolve();
});
await mockAgent.chat('test', () => {});
expect(mockAgent.chat).toHaveBeenCalled();
});
it('处理错误', async () => {
mockAgent.chat.mockRejectedValue(new Error('API Error'));
await expect(mockAgent.chat('test', () => {})).rejects.toThrow('API Error');
});
});
describe('Agent 模式显示', () => {
it('default 模式不显示指示器', () => {
mockAgent.getAgentModeName.mockReturnValue('default');
const mode = mockAgent.getAgentModeName();
expect(mode).toBe('default');
});
it('其他模式显示 @ 指示器', () => {
mockAgent.getAgentModeName.mockReturnValue('code-reviewer');
const mode = mockAgent.getAgentModeName();
expect(mode).toBe('code-reviewer');
});
});
});