bca19b7741
新增测试文件: - agent/executor-extended.test.ts, presets/ - context/manager-extended.test.ts - core/agent.test.ts, providers.test.ts - lsp/cli.test.ts, client-extended.test.ts, index.test.ts - permission/file-prompt.test.ts, prompt.test.ts - skills/builtin/ - tools/filesystem/write_file-extended.test.ts - tools/git/git_commit-extended.test.ts - tools/load_description.test.ts - tools/todo/todo-manager.test.ts - tools/tool-search.test.ts - types/ - utils/config-extended.test.ts, diff-extended.test.ts 修改现有测试: - agent/manager.test.ts - tools/skill/skill.test.ts - utils/config.test.ts, diff.test.ts, image.test.ts
318 lines
10 KiB
TypeScript
318 lines
10 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import type { ModelMessage, LanguageModel } from 'ai';
|
|
|
|
// Mock prune module
|
|
const mockPrune = vi.fn();
|
|
const mockFilterCompacted = vi.fn();
|
|
vi.mock('../../../src/context/prune.js', () => ({
|
|
prune: (...args: unknown[]) => mockPrune(...args),
|
|
filterCompacted: (...args: unknown[]) => mockFilterCompacted(...args),
|
|
}));
|
|
|
|
// Mock compaction module
|
|
const mockCompact = vi.fn();
|
|
const mockSimpleCompact = vi.fn();
|
|
const mockIsSummaryMessage = vi.fn();
|
|
vi.mock('../../../src/context/compaction.js', () => ({
|
|
compact: (...args: unknown[]) => mockCompact(...args),
|
|
simpleCompact: (...args: unknown[]) => mockSimpleCompact(...args),
|
|
isSummaryMessage: (...args: unknown[]) => mockIsSummaryMessage(...args),
|
|
}));
|
|
|
|
// Mock token counter
|
|
const mockEstimateMessages = vi.fn();
|
|
const mockFormat = vi.fn();
|
|
vi.mock('../../../src/context/token-counter.js', () => ({
|
|
TokenCounter: {
|
|
estimateMessages: (...args: unknown[]) => mockEstimateMessages(...args),
|
|
format: (...args: unknown[]) => mockFormat(...args),
|
|
},
|
|
}));
|
|
|
|
import { CompressionManager } from '../../../src/context/manager.js';
|
|
|
|
describe('CompressionManager - 压缩管理器扩展测试', () => {
|
|
let manager: CompressionManager;
|
|
|
|
const createMessages = (count: number): ModelMessage[] => {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
role: 'user' as const,
|
|
content: `Message ${i}`,
|
|
}));
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
manager = new CompressionManager();
|
|
|
|
// 默认 mock 返回值
|
|
mockEstimateMessages.mockReturnValue(1000);
|
|
mockFormat.mockReturnValue('1K');
|
|
mockPrune.mockReturnValue({ messages: [], freedTokens: 0 });
|
|
mockFilterCompacted.mockImplementation((msgs) => msgs);
|
|
mockCompact.mockResolvedValue({ messages: [], freedTokens: 0 });
|
|
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 0 });
|
|
mockIsSummaryMessage.mockReturnValue(false);
|
|
});
|
|
|
|
describe('constructor - 构造函数', () => {
|
|
it('使用默认配置', () => {
|
|
const config = manager.getConfig();
|
|
|
|
expect(config.contextLimit).toBeDefined();
|
|
expect(config.outputReserve).toBeDefined();
|
|
expect(config.overflowThreshold).toBeDefined();
|
|
});
|
|
|
|
it('合并自定义配置', () => {
|
|
const customManager = new CompressionManager({
|
|
contextLimit: 100000,
|
|
outputReserve: 5000,
|
|
});
|
|
|
|
const config = customManager.getConfig();
|
|
expect(config.contextLimit).toBe(100000);
|
|
expect(config.outputReserve).toBe(5000);
|
|
});
|
|
});
|
|
|
|
describe('setModel - 设置模型', () => {
|
|
it('设置模型用于摘要生成', () => {
|
|
const mockModel = {} as LanguageModel;
|
|
manager.setModel(mockModel);
|
|
|
|
// 模型被设置后 compact 应该使用它
|
|
expect(manager['model']).toBe(mockModel);
|
|
});
|
|
});
|
|
|
|
describe('updateConfig - 更新配置', () => {
|
|
it('更新部分配置', () => {
|
|
const originalConfig = manager.getConfig();
|
|
|
|
manager.updateConfig({ contextLimit: 200000 });
|
|
|
|
const updatedConfig = manager.getConfig();
|
|
expect(updatedConfig.contextLimit).toBe(200000);
|
|
expect(updatedConfig.outputReserve).toBe(originalConfig.outputReserve);
|
|
});
|
|
});
|
|
|
|
describe('calculateUsage - 计算使用量', () => {
|
|
it('计算 token 使用情况', () => {
|
|
mockEstimateMessages.mockReturnValue(50000);
|
|
|
|
const messages = createMessages(10);
|
|
const usage = manager.calculateUsage(messages);
|
|
|
|
expect(usage.input).toBe(50000);
|
|
expect(usage.contextLimit).toBeDefined();
|
|
expect(usage.available).toBeDefined();
|
|
expect(usage.usagePercent).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('使用百分比不超过 100', () => {
|
|
// 模拟超出限制
|
|
mockEstimateMessages.mockReturnValue(300000);
|
|
|
|
const usage = manager.calculateUsage([]);
|
|
|
|
expect(usage.usagePercent).toBeLessThanOrEqual(100);
|
|
});
|
|
});
|
|
|
|
describe('shouldCompress - 是否需要压缩', () => {
|
|
it('未超过阈值返回 false', () => {
|
|
mockEstimateMessages.mockReturnValue(10000);
|
|
|
|
const result = manager.shouldCompress([]);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('超过阈值返回 true', () => {
|
|
// 设置接近阈值的使用量
|
|
mockEstimateMessages.mockReturnValue(150000);
|
|
|
|
const result = manager.shouldCompress([]);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isOverflow - 是否溢出', () => {
|
|
it('未溢出返回 false', () => {
|
|
mockEstimateMessages.mockReturnValue(10000);
|
|
|
|
const result = manager.isOverflow([]);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('溢出返回 true', () => {
|
|
mockEstimateMessages.mockReturnValue(500000);
|
|
|
|
const result = manager.isOverflow([]);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('prune - 执行裁剪', () => {
|
|
it('调用 prune 函数', () => {
|
|
const messages = createMessages(5);
|
|
mockPrune.mockReturnValue({ messages: messages.slice(2), freedTokens: 1000 });
|
|
|
|
const result = manager.prune(messages);
|
|
|
|
expect(mockPrune).toHaveBeenCalledWith(messages, expect.any(Object));
|
|
expect(result.freedTokens).toBe(1000);
|
|
});
|
|
});
|
|
|
|
describe('compact - 执行压缩', () => {
|
|
it('有模型时使用 AI 压缩', async () => {
|
|
const mockModel = {} as LanguageModel;
|
|
manager.setModel(mockModel);
|
|
mockCompact.mockResolvedValue({ messages: [], freedTokens: 2000 });
|
|
|
|
const messages = createMessages(5);
|
|
const result = await manager.compact(messages);
|
|
|
|
expect(mockCompact).toHaveBeenCalled();
|
|
expect(result.freedTokens).toBe(2000);
|
|
});
|
|
|
|
it('无模型时使用简单压缩', async () => {
|
|
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 500 });
|
|
|
|
const messages = createMessages(5);
|
|
const result = await manager.compact(messages);
|
|
|
|
expect(mockSimpleCompact).toHaveBeenCalled();
|
|
expect(result.freedTokens).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe('compress - 自动压缩', () => {
|
|
it('先 prune 后不需要 compact', async () => {
|
|
mockEstimateMessages.mockReturnValue(10000); // 低于阈值
|
|
mockPrune.mockReturnValue({ messages: [], freedTokens: 500 });
|
|
|
|
const messages = createMessages(5);
|
|
const result = await manager.compress(messages);
|
|
|
|
expect(result.type).toBe('prune');
|
|
expect(mockPrune).toHaveBeenCalled();
|
|
expect(mockCompact).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('prune 后仍需 compact', async () => {
|
|
mockEstimateMessages.mockReturnValue(150000); // 高于阈值
|
|
mockPrune.mockReturnValue({ messages: createMessages(3), freedTokens: 500 });
|
|
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 1000 });
|
|
|
|
const messages = createMessages(5);
|
|
const result = await manager.compress(messages);
|
|
|
|
expect(result.type).toBe('both');
|
|
expect(mockPrune).toHaveBeenCalled();
|
|
});
|
|
|
|
it('只 compact 时类型为 compaction', async () => {
|
|
mockEstimateMessages.mockReturnValue(150000);
|
|
mockPrune.mockReturnValue({ messages: createMessages(5), freedTokens: 0 });
|
|
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 1000 });
|
|
|
|
const messages = createMessages(5);
|
|
const result = await manager.compress(messages);
|
|
|
|
expect(result.type).toBe('compaction');
|
|
});
|
|
});
|
|
|
|
describe('forceCompress - 强制压缩', () => {
|
|
it('消息太少不压缩', async () => {
|
|
const messages = createMessages(3);
|
|
const result = await manager.forceCompress(messages);
|
|
|
|
expect(result.messages).toEqual(messages);
|
|
expect(result.freedTokens).toBe(0);
|
|
});
|
|
|
|
it('足够消息时执行压缩', async () => {
|
|
mockEstimateMessages.mockReturnValue(10000);
|
|
mockPrune.mockReturnValue({ messages: createMessages(3), freedTokens: 500 });
|
|
mockSimpleCompact.mockReturnValue({ messages: createMessages(2), freedTokens: 300 });
|
|
|
|
const messages = createMessages(10);
|
|
const result = await manager.forceCompress(messages);
|
|
|
|
expect(mockPrune).toHaveBeenCalled();
|
|
});
|
|
|
|
it('有模型时尝试 AI 压缩', async () => {
|
|
const mockModel = {} as LanguageModel;
|
|
manager.setModel(mockModel);
|
|
mockEstimateMessages.mockReturnValue(10000);
|
|
mockPrune.mockReturnValue({ messages: createMessages(5), freedTokens: 500 });
|
|
mockCompact.mockResolvedValue({ messages: createMessages(2), freedTokens: 1000 });
|
|
|
|
const messages = createMessages(10);
|
|
await manager.forceCompress(messages);
|
|
|
|
expect(mockCompact).toHaveBeenCalled();
|
|
});
|
|
|
|
it('AI 压缩失败时回退到简单压缩', async () => {
|
|
const mockModel = {} as LanguageModel;
|
|
manager.setModel(mockModel);
|
|
mockEstimateMessages.mockReturnValue(10000);
|
|
mockPrune.mockReturnValue({ messages: createMessages(5), freedTokens: 500 });
|
|
mockCompact.mockRejectedValue(new Error('AI error'));
|
|
mockSimpleCompact.mockReturnValue({ messages: createMessages(2), freedTokens: 800 });
|
|
|
|
const messages = createMessages(10);
|
|
const result = await manager.forceCompress(messages);
|
|
|
|
expect(mockSimpleCompact).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('filterCompacted - 过滤压缩内容', () => {
|
|
it('调用 filterCompacted 函数', () => {
|
|
const messages = createMessages(5);
|
|
mockFilterCompacted.mockReturnValue(messages.slice(1));
|
|
|
|
const result = manager.filterCompacted(messages);
|
|
|
|
expect(mockFilterCompacted).toHaveBeenCalledWith(messages);
|
|
});
|
|
});
|
|
|
|
describe('isSummaryMessage - 检查摘要消息', () => {
|
|
it('调用 isSummaryMessage 函数', () => {
|
|
mockIsSummaryMessage.mockReturnValue(true);
|
|
|
|
const message: ModelMessage = { role: 'assistant', content: 'summary' };
|
|
const result = manager.isSummaryMessage(message);
|
|
|
|
expect(mockIsSummaryMessage).toHaveBeenCalledWith(message);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('formatUsage - 格式化使用情况', () => {
|
|
it('返回格式化字符串', () => {
|
|
mockEstimateMessages.mockReturnValue(50000);
|
|
mockFormat.mockImplementation((n) => `${Math.round(n / 1000)}K`);
|
|
|
|
const messages = createMessages(5);
|
|
const result = manager.formatUsage(messages);
|
|
|
|
expect(result).toMatch(/\d+K\/\d+K/);
|
|
expect(result).toContain('%');
|
|
});
|
|
});
|
|
});
|