| |
| |
| |
| |
| |
|
|
| import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| import { ContextManager } from './contextManager.js'; |
| import { |
| createMockEnvironment, |
| createDummyNode, |
| } from './testing/contextTestUtils.js'; |
| import type { ContextProfile } from './config/profiles.js'; |
| import { NodeType, type ConcreteNode } from './graph/types.js'; |
| import type { PipelineOrchestrator } from './pipeline/orchestrator.js'; |
| import type { AgentChatHistory } from '../core/agentChatHistory.js'; |
| import type { AdvancedTokenCalculator } from './utils/contextTokenCalculator.js'; |
| import type { ContextManagementConfig } from './config/types.js'; |
| import type { ContextEnvironment } from './pipeline/environment.js'; |
| import type { ContextWorkingBufferImpl } from './pipeline/contextWorkingBuffer.js'; |
|
|
| describe('ContextManager - Multi-stage and Incremental GC', () => { |
| let mockEnv: ReturnType<typeof createMockEnvironment>; |
| let mockOrchestrator: PipelineOrchestrator; |
| let mockChatHistory: AgentChatHistory; |
| let mockAdvancedTokenCalculator: AdvancedTokenCalculator; |
|
|
| beforeEach(() => { |
| mockEnv = createMockEnvironment(); |
|
|
| mockOrchestrator = { |
| setNodeProvider: vi.fn(), |
| waitForPipelines: vi.fn().mockResolvedValue(undefined), |
| executeTriggerSync: vi |
| .fn() |
| .mockImplementation(async (trigger, buffer) => buffer), |
| executeIngestionPipeline: vi |
| .fn() |
| .mockImplementation(async (nodes) => nodes), |
| shutdown: vi.fn(), |
| } as unknown as PipelineOrchestrator; |
|
|
| mockChatHistory = { |
| all: vi.fn().mockReturnValue([]), |
| getHistory: vi.fn().mockReturnValue([]), |
| get: vi.fn().mockReturnValue([]), |
| subscribe: vi.fn(), |
| } as unknown as AgentChatHistory; |
|
|
| mockAdvancedTokenCalculator = { |
| getRawBaseUnits: vi.fn().mockReturnValue(0), |
| getRawBaseUnitsForContent: vi.fn().mockReturnValue(0), |
| calculateTokensAndBaseUnits: vi.fn(), |
| } as unknown as AdvancedTokenCalculator; |
| }); |
|
|
| const setupManager = (config: ContextManagementConfig) => { |
| const sidecar: ContextProfile = { |
| name: 'test', |
| config, |
| buildPipelines: () => [], |
| buildAsyncPipelines: () => [], |
| }; |
| return new ContextManager( |
| sidecar, |
| mockEnv as unknown as ContextEnvironment, |
| mockEnv.tracer, |
| mockOrchestrator, |
| mockChatHistory, |
| mockAdvancedTokenCalculator, |
| ); |
| }; |
|
|
| it('should emit NormalizeNeeded when normalizedTokens budget is exceeded', async () => { |
| const manager = setupManager({ |
| budget: { |
| retainedTokens: 100, |
| normalizedTokens: 150, |
| maxTokens: 300, |
| }, |
| } as unknown as ContextManagementConfig); |
|
|
| const normalizeSpy = vi.fn(); |
| mockEnv.eventBus.onNormalizeNeeded(normalizeSpy); |
| const consolidationSpy = vi.fn(); |
| mockEnv.eventBus.onConsolidationNeeded(consolidationSpy); |
|
|
| |
| mockEnv.tokenCalculator.calculateConcreteListTokens = vi |
| .fn() |
| .mockImplementation((nodes: ConcreteNode[]) => |
| nodes.reduce( |
| (sum: number, n: ConcreteNode) => |
| |
| sum + ((n as unknown as { _mockTokens: number })._mockTokens || 0), |
| 0, |
| ), |
| ); |
|
|
| const createNodeWithTokens = ( |
| id: string, |
| type: NodeType, |
| tokens: number, |
| ) => { |
| const node = createDummyNode(id, type); |
| |
| node._mockTokens = tokens; |
| return node; |
| }; |
|
|
| |
| |
| |
| |
| |
| const nodes = [ |
| createNodeWithTokens('ep1', NodeType.USER_PROMPT, 80), |
| createNodeWithTokens('ep2', NodeType.AGENT_THOUGHT, 80), |
| createNodeWithTokens('ep3', NodeType.TOOL_EXECUTION, 80), |
| createNodeWithTokens('ep4', NodeType.TOOL_EXECUTION, 80), |
| ]; |
|
|
| |
| manager.buffer = { nodes } as unknown as ContextWorkingBufferImpl; |
|
|
| |
| |
| await manager.evaluateTriggers(nodes, new Set([nodes[3].id]), new Set()); |
|
|
| |
| |
| |
| |
| expect(consolidationSpy).not.toHaveBeenCalled(); |
|
|
| expect(normalizeSpy).toHaveBeenCalledOnce(); |
| const normalizeEvent = normalizeSpy.mock.calls[0][0]; |
| expect(normalizeEvent.targetNodeIds.has(nodes[0].id)).toBe(true); |
| expect(normalizeEvent.targetNodeIds.has(nodes[1].id)).toBe(true); |
| expect(normalizeEvent.targetNodeIds.has(nodes[2].id)).toBe(false); |
| }); |
| }); |
|
|