refactor(core): 移除 UI 交互层,保持 core 为纯能力模块

- 删除 terminal.ts 及其测试(UI 交互应属于 cli 包)
- 清理 index.ts 中的 CLI 入口代码
- 合并 lib.ts 导出到 index.ts
- 移除 commander、inquirer 依赖
- 更新 package.json 导出配置
This commit is contained in:
2025-12-13 00:42:07 +08:00
parent 5b7d62e793
commit 5d4afecd48
5 changed files with 95 additions and 1212 deletions
@@ -1,282 +0,0 @@
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');
});
});
});