Files
ai-terminal-assistant/packages/core/tests/unit/context/compaction.test.ts
T
kurihada 5e32375f0e feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更:
- 采用 pnpm workspaces 实现 Monorepo 结构
- 将现有代码迁移到 packages/core
- 新增 packages/server HTTP 服务层

Server 功能:
- REST API: 会话管理、工具管理、配置管理
- WebSocket: 实时双向通信支持
- SSE: 服务端事件推送
- Hono + Bun 作为运行时

API 端点:
- GET/POST /api/sessions - 会话 CRUD
- GET/POST /api/sessions/:id/messages - 消息管理
- GET /api/sessions/:id/events - SSE 事件流
- WS /api/ws/:sessionId - WebSocket 连接
- GET/POST /api/tools - 工具管理
- GET/PUT /api/config - 配置管理
2025-12-12 10:42:20 +08:00

280 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi } from 'vitest';
import { isSummaryMessage, simpleCompact } from '../../../src/context/compaction.js';
import {
SUMMARY_MARKER,
type CompressionConfig,
} from '../../../src/context/types.js';
import type { ModelMessage } 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 createMessageWithTextParts(texts: string[]): ModelMessage {
return {
role: 'assistant',
content: texts.map((text) => ({ type: 'text', text })),
} as ModelMessage;
}
describe('isSummaryMessage - 检测摘要消息', () => {
it('字符串内容包含摘要标记返回 true', () => {
const message = createSummaryMessage('这是摘要内容');
expect(isSummaryMessage(message)).toBe(true);
});
it('字符串内容不包含摘要标记返回 false', () => {
const message = createAssistantMessage('普通助手消息');
expect(isSummaryMessage(message)).toBe(false);
});
it('数组内容包含摘要标记返回 true', () => {
const message = createMessageWithTextParts([
'一些文本',
`${SUMMARY_MARKER}\n摘要内容\n${SUMMARY_MARKER}`,
]);
expect(isSummaryMessage(message)).toBe(true);
});
it('数组内容不包含摘要标记返回 false', () => {
const message = createMessageWithTextParts(['文本1', '文本2']);
expect(isSummaryMessage(message)).toBe(false);
});
it('用户消息不是摘要消息', () => {
const message = createUserMessage(SUMMARY_MARKER);
// 虽然包含标记,但这种情况下 isSummaryMessage 还是会返回 true
// 因为它只检查内容是否包含标记
expect(isSummaryMessage(message)).toBe(true);
});
it('空数组内容返回 false', () => {
const message: ModelMessage = {
role: 'assistant',
content: [],
};
expect(isSummaryMessage(message)).toBe(false);
});
it('非文本部分不匹配', () => {
const message: ModelMessage = {
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'call_1',
toolName: 'test',
args: { key: SUMMARY_MARKER },
},
],
} as ModelMessage;
expect(isSummaryMessage(message)).toBe(false);
});
});
describe('simpleCompact - 简单压缩', () => {
const testConfig: CompressionConfig = {
contextLimit: 1000,
outputReserve: 100,
pruneProtect: 200, // 保护最近 200 tokens
pruneMinimum: 50,
overflowThreshold: 0.85,
};
describe('基本压缩行为', () => {
it('空消息数组不压缩', () => {
const result = simpleCompact([], testConfig);
expect(result.messages).toHaveLength(0);
expect(result.freedTokens).toBe(0);
});
it('消息在保护范围内不压缩', () => {
const messages: ModelMessage[] = [
createUserMessage('Hello'),
createAssistantMessage('Hi'),
];
const result = simpleCompact(messages, testConfig);
// 消息很短,应该在保护范围内
expect(result.freedTokens).toBe(0);
expect(result.messages).toEqual(messages);
});
it('压缩超出保护范围的消息', () => {
// 创建大量消息超出保护范围
const messages: ModelMessage[] = [];
for (let i = 0; i < 50; i++) {
messages.push(createUserMessage(`Message ${i}: ${'a'.repeat(100)}`));
messages.push(createAssistantMessage(`Response ${i}: ${'b'.repeat(100)}`));
}
const result = simpleCompact(messages, testConfig);
// 应该压缩一些旧消息
expect(result.freedTokens).toBeGreaterThan(0);
expect(result.messages.length).toBeLessThan(messages.length);
});
});
describe('保留消息数量', () => {
it('至少保留 2 条消息(正常模式)', () => {
const messages: ModelMessage[] = [];
for (let i = 0; i < 10; i++) {
messages.push(createUserMessage(`Long message ${'a'.repeat(500)}`));
}
const result = simpleCompact(messages, testConfig);
// 即使压缩,也至少保留 2 条
expect(result.messages.length).toBeGreaterThanOrEqual(2 + 1); // 2 保留 + 1 摘要
});
it('强制模式下至少保留 1 条消息', () => {
const forceConfig: CompressionConfig = {
...testConfig,
pruneProtect: 0, // 强制模式
};
const messages: ModelMessage[] = [];
for (let i = 0; i < 10; i++) {
messages.push(createUserMessage(`Message ${i}`));
}
const result = simpleCompact(messages, forceConfig);
// 强制模式下至少保留 1 条消息
expect(result.messages.length).toBeGreaterThanOrEqual(2); // 1 保留 + 1 摘要
});
});
describe('摘要消息生成', () => {
it('压缩后生成摘要消息', () => {
const messages: ModelMessage[] = [];
for (let i = 0; i < 50; i++) {
messages.push(createUserMessage(`Message ${i}: ${'a'.repeat(100)}`));
}
const result = simpleCompact(messages, testConfig);
if (result.freedTokens > 0) {
// 第一条消息应该是摘要
expect(isSummaryMessage(result.messages[0])).toBe(true);
}
});
it('摘要消息包含移除数量信息', () => {
const messages: ModelMessage[] = [];
for (let i = 0; i < 50; i++) {
messages.push(createUserMessage(`Message ${i}: ${'a'.repeat(100)}`));
}
const result = simpleCompact(messages, testConfig);
if (result.freedTokens > 0) {
const summaryContent = result.messages[0].content as string;
expect(summaryContent).toContain('对话历史已压缩');
expect(summaryContent).toContain('条消息');
}
});
});
describe('保护范围计算', () => {
it('短消息全部在保护范围内', () => {
const messages: ModelMessage[] = [
createUserMessage('Hi'),
createAssistantMessage('Hello'),
createUserMessage('How?'),
createAssistantMessage('Good!'),
];
const result = simpleCompact(messages, testConfig);
// 短消息应该全部保留
expect(result.messages).toEqual(messages);
});
});
describe('不修改原数组', () => {
it('原消息数组不被修改', () => {
const messages: ModelMessage[] = [];
for (let i = 0; i < 20; i++) {
messages.push(createUserMessage(`Message ${i}: ${'a'.repeat(100)}`));
}
const originalLength = messages.length;
simpleCompact(messages, testConfig);
expect(messages.length).toBe(originalLength);
});
});
});
// 注意: compact 函数需要真实的 LanguageModel
// 这里只测试 simpleCompact 和辅助函数
// compact 的测试应该在集成测试中进行
describe('Compaction 边界情况', () => {
it('单条消息不压缩', () => {
const messages: ModelMessage[] = [
createUserMessage('Single message'),
];
const testConfig: CompressionConfig = {
contextLimit: 100,
outputReserve: 10,
pruneProtect: 50,
pruneMinimum: 10,
overflowThreshold: 0.85,
};
const result = simpleCompact(messages, testConfig);
expect(result.messages).toEqual(messages);
expect(result.freedTokens).toBe(0);
});
it('两条消息不压缩(最小保留)', () => {
const messages: ModelMessage[] = [
createUserMessage('First'),
createAssistantMessage('Second'),
];
const testConfig: CompressionConfig = {
contextLimit: 100,
outputReserve: 10,
pruneProtect: 10, // 很小的保护范围
pruneMinimum: 1,
overflowThreshold: 0.85,
};
const result = simpleCompact(messages, testConfig);
// 即使配置很激进,也至少保留 2 条
expect(result.messages.length).toBeGreaterThanOrEqual(2);
});
});