Files
ai-terminal-assistant/packages/core/tests/unit/context/prune.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

308 lines
9.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { prune, filterCompacted } from '../../../src/context/prune.js';
import {
COMPACTED_PLACEHOLDER,
SUMMARY_MARKER,
COMPACTED_MARKER,
type CompressionConfig,
} from '../../../src/context/types.js';
import type { ModelMessage } from 'ai';
// 创建测试用的工具结果消息
function createToolResultMessage(
toolCallId: string,
result: unknown,
compacted = false
): ModelMessage {
const content: Record<string, unknown> = {
type: 'tool-result',
toolCallId,
toolName: 'test_tool',
result,
};
if (compacted) {
content[COMPACTED_MARKER] = {
compactedAt: Date.now(),
originalSize: 100,
};
}
return {
role: 'tool',
content: [content],
} as ModelMessage;
}
// 创建测试用的工具调用消息
function createToolCallMessage(toolCallId: string): ModelMessage {
return {
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId,
toolName: 'test_tool',
args: { param: 'value' },
},
],
} as ModelMessage;
}
// 创建用户消息
function createUserMessage(content: string): ModelMessage {
return { role: 'user', content };
}
// 创建助手消息
function createAssistantMessage(content: string): ModelMessage {
return { role: 'assistant', content };
}
// 创建摘要消息
function createSummaryMessage(): ModelMessage {
return {
role: 'assistant',
content: `${SUMMARY_MARKER}\n## 对话摘要\n\n这是一个摘要\n${SUMMARY_MARKER}`,
};
}
// 创建大工具结果(指定 token 数)
function createLargeToolResult(toolCallId: string, sizeInChars: number): ModelMessage {
// 约 4 字符/token(英文)
const content = 'a'.repeat(sizeInChars);
return createToolResultMessage(toolCallId, { output: content });
}
describe('prune - 消息裁剪策略', () => {
// 测试配置:小值便于测试
const testConfig: CompressionConfig = {
contextLimit: 1000,
outputReserve: 100,
pruneProtect: 200, // 保护最近 200 tokens
pruneMinimum: 50, // 至少释放 50 tokens 才执行
overflowThreshold: 0.85,
};
describe('基本裁剪行为', () => {
it('空消息数组不裁剪', () => {
const result = prune([], testConfig);
expect(result.messages).toHaveLength(0);
expect(result.freedTokens).toBe(0);
});
it('只有用户消息不裁剪', () => {
const messages: ModelMessage[] = [
createUserMessage('Hello'),
createUserMessage('How are you?'),
];
const result = prune(messages, testConfig);
expect(result.messages).toEqual(messages);
expect(result.freedTokens).toBe(0);
});
it('保护范围内的工具结果不裁剪', () => {
// 创建小的工具结果(在保护范围内)
const messages: ModelMessage[] = [
createUserMessage('Use tool'),
createToolCallMessage('call_1'),
createToolResultMessage('call_1', { output: 'small result' }),
];
const result = prune(messages, testConfig);
// 不应该裁剪
expect(result.freedTokens).toBe(0);
});
it('裁剪超出保护范围的工具结果', () => {
// 创建多个工具结果,超出保护范围
const messages: ModelMessage[] = [
createUserMessage('Task 1'),
createToolCallMessage('call_1'),
createLargeToolResult('call_1', 1000), // ~250 tokens, 超出 pruneProtect
createUserMessage('Task 2'),
createToolCallMessage('call_2'),
createLargeToolResult('call_2', 400), // ~100 tokens
createUserMessage('Task 3'),
createToolCallMessage('call_3'),
createToolResultMessage('call_3', { output: 'recent' }), // 最近的,在保护范围内
];
const result = prune(messages, testConfig);
// 应该裁剪旧的大工具结果
expect(result.freedTokens).toBeGreaterThan(0);
});
});
describe('摘要消息边界', () => {
it('遇到摘要消息停止裁剪', () => {
const messages: ModelMessage[] = [
createSummaryMessage(), // 摘要消息
createUserMessage('After summary'),
createToolCallMessage('call_1'),
createLargeToolResult('call_1', 2000), // 大结果
];
const result = prune(messages, testConfig);
// 因为遇到摘要消息,不会继续向前裁剪
// 但是摘要后面的大工具结果如果超出保护范围仍会被裁剪
expect(result.messages[0]).toEqual(messages[0]); // 摘要保留
});
it('摘要消息前的内容不裁剪', () => {
const messages: ModelMessage[] = [
createUserMessage('Before summary'),
createToolCallMessage('call_old'),
createLargeToolResult('call_old', 2000), // 摘要前的大结果
createSummaryMessage(),
createUserMessage('After summary'),
];
const result = prune(messages, testConfig);
// 摘要前的内容因为遇到摘要边界停止
expect(result.messages.length).toBe(messages.length);
});
});
describe('已压缩内容处理', () => {
it('遇到已压缩的工具结果停止', () => {
const messages: ModelMessage[] = [
createUserMessage('Task 1'),
createToolCallMessage('call_1'),
createToolResultMessage('call_1', COMPACTED_PLACEHOLDER, true), // 已压缩
createUserMessage('Task 2'),
createToolCallMessage('call_2'),
createLargeToolResult('call_2', 1000), // 新的大结果
];
const result = prune(messages, testConfig);
// 遇到已压缩的结果应该停止继续向前
expect(result.messages).toBeDefined();
});
});
describe('最小裁剪量检查', () => {
it('释放量不足最小值时不执行裁剪', () => {
// 使用较大的 pruneMinimum
const strictConfig: CompressionConfig = {
...testConfig,
pruneMinimum: 10000, // 要求至少释放 10000 tokens
};
const messages: ModelMessage[] = [
createUserMessage('Task'),
createToolCallMessage('call_1'),
createLargeToolResult('call_1', 400), // 只有 ~100 tokens
];
const result = prune(messages, strictConfig);
expect(result.freedTokens).toBe(0);
expect(result.messages).toEqual(messages);
});
});
describe('深拷贝验证', () => {
it('不修改原消息数组', () => {
const messages: ModelMessage[] = [
createUserMessage('Task'),
createToolCallMessage('call_1'),
createLargeToolResult('call_1', 2000),
createUserMessage('Recent'),
];
const originalLength = messages.length;
const originalFirstMessage = { ...messages[0] };
prune(messages, testConfig);
expect(messages.length).toBe(originalLength);
expect(messages[0]).toEqual(originalFirstMessage);
});
});
});
describe('filterCompacted - 过滤已压缩内容', () => {
it('不修改普通消息', () => {
const messages: ModelMessage[] = [
createUserMessage('Hello'),
createAssistantMessage('Hi there'),
];
const result = filterCompacted(messages);
expect(result).toEqual(messages);
});
it('不修改未压缩的工具结果', () => {
const messages: ModelMessage[] = [
createToolResultMessage('call_1', { output: 'normal result' }),
];
const result = filterCompacted(messages);
expect(result).toEqual(messages);
});
it('将已压缩工具结果替换为占位符', () => {
const messages: ModelMessage[] = [
createToolResultMessage('call_1', 'original', true), // 已压缩
];
const result = filterCompacted(messages);
const content = result[0].content as { result: unknown }[];
expect(content[0].result).toBe(COMPACTED_PLACEHOLDER);
});
it('混合内容正确处理', () => {
const messages: ModelMessage[] = [
createUserMessage('Task'),
createToolResultMessage('call_1', 'original', true), // 已压缩
createToolResultMessage('call_2', { output: 'normal' }), // 未压缩
createAssistantMessage('Done'),
];
const result = filterCompacted(messages);
expect(result).toHaveLength(4);
// 第一个工具结果应该被替换
const compactedContent = result[1].content as { result: unknown }[];
expect(compactedContent[0].result).toBe(COMPACTED_PLACEHOLDER);
// 第二个工具结果应该保持不变
const normalContent = result[2].content as { result: unknown }[];
expect(normalContent[0].result).toEqual({ output: 'normal' });
});
it('保留消息的其他属性', () => {
const messages: ModelMessage[] = [
{
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: 'call_1',
toolName: 'my_tool',
result: 'data',
[COMPACTED_MARKER]: { compactedAt: 123, originalSize: 100 },
},
],
} as ModelMessage,
];
const result = filterCompacted(messages);
const content = result[0].content as { toolCallId: string; toolName: string }[];
expect(content[0].toolCallId).toBe('call_1');
expect(content[0].toolName).toBe('my_tool');
});
});