f54f24b079
- 添加独立摘要模型配置支持(SUMMARY_PROVIDER/MODEL/API_KEY/BASE_URL) - 添加 CompressionStatus 枚举和 DetailedCompressionResult 详细返回类型 - 实现压缩失败检测(空摘要、token膨胀) - 添加首条 user-assistant 对保护,确保上下文连贯性 - CompressionManager 支持独立摘要模型(优先使用小模型降低成本) - Agent 自动压缩时显示详细状态信息 - 更新相关测试用例
302 lines
8.8 KiB
TypeScript
302 lines
8.8 KiB
TypeScript
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);
|
|
|
|
// 小对话不压缩时返回 'none'
|
|
expect(['prune', 'compaction', 'both', 'none']).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);
|
|
});
|
|
});
|