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
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { CompressionManager } from '../../../src/context/manager.js';
|
||||
import { DEFAULT_COMPRESSION_CONFIG, SUMMARY_MARKER } from '../../../src/context/types.js';
|
||||
import type { ModelMessage, LanguageModel } from 'ai';
|
||||
|
||||
// 创建测试用消息
|
||||
function createUserMessage(content: string): ModelMessage {
|
||||
return { role: 'user', content };
|
||||
}
|
||||
|
||||
function createAssistantMessage(content: string): ModelMessage {
|
||||
return { role: 'assistant', content };
|
||||
}
|
||||
|
||||
function createSummaryMessage(summary: string): ModelMessage {
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: `${SUMMARY_MARKER}\n## 对话摘要\n\n${summary}\n${SUMMARY_MARKER}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 创建大量消息以超过阈值
|
||||
function createLargeConversation(count: number): ModelMessage[] {
|
||||
const messages: ModelMessage[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
messages.push(createUserMessage(`Message ${i}: ${'a'.repeat(100)}`));
|
||||
messages.push(createAssistantMessage(`Response ${i}: ${'b'.repeat(100)}`));
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
describe('CompressionManager - 压缩管理器', () => {
|
||||
let manager: CompressionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new CompressionManager();
|
||||
});
|
||||
|
||||
describe('配置管理', () => {
|
||||
it('使用默认配置', () => {
|
||||
const config = manager.getConfig();
|
||||
|
||||
expect(config.contextLimit).toBe(DEFAULT_COMPRESSION_CONFIG.contextLimit);
|
||||
expect(config.outputReserve).toBe(DEFAULT_COMPRESSION_CONFIG.outputReserve);
|
||||
expect(config.overflowThreshold).toBe(DEFAULT_COMPRESSION_CONFIG.overflowThreshold);
|
||||
});
|
||||
|
||||
it('自定义配置覆盖默认值', () => {
|
||||
const customManager = new CompressionManager({
|
||||
contextLimit: 50000,
|
||||
outputReserve: 5000,
|
||||
});
|
||||
|
||||
const config = customManager.getConfig();
|
||||
|
||||
expect(config.contextLimit).toBe(50000);
|
||||
expect(config.outputReserve).toBe(5000);
|
||||
// 未指定的使用默认值
|
||||
expect(config.overflowThreshold).toBe(DEFAULT_COMPRESSION_CONFIG.overflowThreshold);
|
||||
});
|
||||
|
||||
it('updateConfig 更新配置', () => {
|
||||
manager.updateConfig({ pruneProtect: 10000 });
|
||||
|
||||
const config = manager.getConfig();
|
||||
expect(config.pruneProtect).toBe(10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateUsage - 计算 token 使用情况', () => {
|
||||
it('空消息返回零使用', () => {
|
||||
const usage = manager.calculateUsage([]);
|
||||
|
||||
expect(usage.input).toBe(0);
|
||||
expect(usage.usagePercent).toBe(0);
|
||||
});
|
||||
|
||||
it('计算简单消息的使用量', () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi there'),
|
||||
];
|
||||
|
||||
const usage = manager.calculateUsage(messages);
|
||||
|
||||
expect(usage.input).toBeGreaterThan(0);
|
||||
expect(usage.usagePercent).toBeGreaterThan(0);
|
||||
expect(usage.usagePercent).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it('大量消息使用量更高', () => {
|
||||
const smallMessages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
];
|
||||
const largeMessages = createLargeConversation(50);
|
||||
|
||||
const smallUsage = manager.calculateUsage(smallMessages);
|
||||
const largeUsage = manager.calculateUsage(largeMessages);
|
||||
|
||||
expect(largeUsage.input).toBeGreaterThan(smallUsage.input);
|
||||
});
|
||||
|
||||
it('使用量百分比不超过 100', () => {
|
||||
// 创建超大对话
|
||||
const messages = createLargeConversation(500);
|
||||
const usage = manager.calculateUsage(messages);
|
||||
|
||||
expect(usage.usagePercent).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldCompress - 判断是否需要压缩', () => {
|
||||
it('小对话不需要压缩', () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi'),
|
||||
];
|
||||
|
||||
expect(manager.shouldCompress(messages)).toBe(false);
|
||||
});
|
||||
|
||||
it('大对话可能需要压缩', () => {
|
||||
// 创建足够大的对话以超过阈值
|
||||
const messages = createLargeConversation(200);
|
||||
|
||||
// 取决于配置的阈值
|
||||
const usage = manager.calculateUsage(messages);
|
||||
const threshold = manager.getConfig().overflowThreshold * 100;
|
||||
const shouldCompress = usage.usagePercent >= threshold;
|
||||
|
||||
expect(manager.shouldCompress(messages)).toBe(shouldCompress);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOverflow - 判断是否溢出', () => {
|
||||
it('小对话不溢出', () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi'),
|
||||
];
|
||||
|
||||
expect(manager.isOverflow(messages)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prune - 执行裁剪', () => {
|
||||
it('空消息不裁剪', () => {
|
||||
const result = manager.prune([]);
|
||||
|
||||
expect(result.messages).toHaveLength(0);
|
||||
expect(result.freedTokens).toBe(0);
|
||||
});
|
||||
|
||||
it('小对话不裁剪', () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi'),
|
||||
];
|
||||
|
||||
const result = manager.prune(messages);
|
||||
|
||||
expect(result.messages).toEqual(messages);
|
||||
expect(result.freedTokens).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compact - 执行压缩', () => {
|
||||
it('无模型时使用简单压缩', async () => {
|
||||
const messages = createLargeConversation(50);
|
||||
|
||||
const result = await manager.compact(messages);
|
||||
|
||||
// 简单压缩会根据配置决定是否压缩
|
||||
expect(result.messages).toBeDefined();
|
||||
expect(result.freedTokens).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('设置模型后使用 AI 压缩', async () => {
|
||||
// 创建 mock 模型
|
||||
const mockModel = {
|
||||
doGenerate: vi.fn().mockResolvedValue({
|
||||
text: '这是一个摘要',
|
||||
}),
|
||||
} as unknown as LanguageModel;
|
||||
|
||||
manager.setModel(mockModel);
|
||||
|
||||
const messages = createLargeConversation(50);
|
||||
const result = await manager.compact(messages);
|
||||
|
||||
expect(result.messages).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('compress - 自动压缩', () => {
|
||||
it('小对话不压缩', async () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi'),
|
||||
];
|
||||
|
||||
const result = await manager.compress(messages);
|
||||
|
||||
expect(result.messages).toEqual(messages);
|
||||
expect(result.freedTokens).toBe(0);
|
||||
});
|
||||
|
||||
it('返回正确的压缩类型', async () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi'),
|
||||
];
|
||||
|
||||
const result = await manager.compress(messages);
|
||||
|
||||
expect(['prune', 'compaction', 'both']).toContain(result.type);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forceCompress - 强制压缩', () => {
|
||||
it('消息数量少于 4 条不压缩', async () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi'),
|
||||
];
|
||||
|
||||
const result = await manager.forceCompress(messages);
|
||||
|
||||
expect(result.messages).toEqual(messages);
|
||||
expect(result.freedTokens).toBe(0);
|
||||
});
|
||||
|
||||
it('强制压缩大对话', async () => {
|
||||
const messages = createLargeConversation(20);
|
||||
|
||||
const result = await manager.forceCompress(messages);
|
||||
|
||||
// 强制压缩应该减少消息数量
|
||||
expect(result.messages.length).toBeLessThanOrEqual(messages.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterCompacted - 过滤已压缩内容', () => {
|
||||
it('不修改普通消息', () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi'),
|
||||
];
|
||||
|
||||
const result = manager.filterCompacted(messages);
|
||||
|
||||
expect(result).toEqual(messages);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSummaryMessage - 检测摘要消息', () => {
|
||||
it('检测摘要消息', () => {
|
||||
const summary = createSummaryMessage('这是摘要');
|
||||
|
||||
expect(manager.isSummaryMessage(summary)).toBe(true);
|
||||
});
|
||||
|
||||
it('普通消息不是摘要', () => {
|
||||
const normal = createAssistantMessage('普通回复');
|
||||
|
||||
expect(manager.isSummaryMessage(normal)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatUsage - 格式化使用情况', () => {
|
||||
it('格式化空消息', () => {
|
||||
const formatted = manager.formatUsage([]);
|
||||
|
||||
expect(formatted).toContain('/');
|
||||
expect(formatted).toContain('(');
|
||||
expect(formatted).toContain('%)');
|
||||
});
|
||||
|
||||
it('格式化包含消息的使用情况', () => {
|
||||
const messages: ModelMessage[] = [
|
||||
createUserMessage('Hello'),
|
||||
createAssistantMessage('Hi there'),
|
||||
];
|
||||
|
||||
const formatted = manager.formatUsage(messages);
|
||||
|
||||
expect(typeof formatted).toBe('string');
|
||||
expect(formatted).toContain('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('compressionManager 单例', () => {
|
||||
it('导出单例实例', async () => {
|
||||
const { compressionManager } = await import('../../../src/context/manager.js');
|
||||
|
||||
expect(compressionManager).toBeDefined();
|
||||
expect(compressionManager).toBeInstanceOf(CompressionManager);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user