This commit is contained in:
Your Name
2026-04-07 03:39:25 +00:00
parent 63e8b825a7
commit 94c59405aa
5 changed files with 57 additions and 42 deletions
@@ -21,6 +21,9 @@ import { ContextEventBus } from './eventBus.js';
import { ContextTokenCalculator } from './utils/contextTokenCalculator.js';
import type { Content } from '@google/genai';
import type { BaseLlmClient } from '../core/baseLlmClient.js';
import type { Episode } from './ir/types.js';
import type { SidecarConfig } from './sidecar/types.js';
expect.addSnapshotSerializer({
test: (val) =>
@@ -74,7 +77,7 @@ describe('ContextManager Golden Tests', () => {
const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'test-session' });
const eventBus = new ContextEventBus();
const env = new ContextEnvironmentImpl(
{} as any,
{ generateContent: async () => ({}), generateJson: async () => ({}) } as unknown as BaseLlmClient,
'test-prompt-id',
'test',
'/tmp',
@@ -118,7 +121,7 @@ describe('ContextManager Golden Tests', () => {
it('should process history and match golden snapshot', async () => {
const history = createLargeHistory();
(contextManager as any).pristineEpisodes = (
(contextManager as unknown as { pristineEpisodes: Episode[] }).pristineEpisodes = (
await import('./ir/mapper.js')
).IrMapper.toIr(history, new ContextTokenCalculator(4));
const result = await contextManager.projectCompressedHistory();
@@ -127,7 +130,7 @@ describe('ContextManager Golden Tests', () => {
it('should not modify history when under budget', async () => {
const history = createLargeHistory();
(contextManager as any).pristineEpisodes = (
(contextManager as unknown as { pristineEpisodes: Episode[] }).pristineEpisodes = (
await import('./ir/mapper.js')
).IrMapper.toIr(history, new ContextTokenCalculator(4));
// In Golden Tests, we just want to ensure the logic doesn't throw or alter unprotected history in weird ways.
@@ -135,7 +138,7 @@ describe('ContextManager Golden Tests', () => {
const tracer2 = new ContextTracer({ targetDir: '/tmp', sessionId: 'test2' });
const eventBus2 = new ContextEventBus();
const env2 = new ContextEnvironmentImpl(
{} as any,
{ generateContent: async () => ({}), generateJson: async () => ({}) } as unknown as BaseLlmClient,
'test-prompt-id',
'test',
'/tmp',
@@ -148,12 +151,12 @@ describe('ContextManager Golden Tests', () => {
{
budget: { retainedTokens: 100000, maxTokens: 150000 },
pipelines: [],
} as any,
} as unknown as SidecarConfig,
env2,
tracer2,
);
(contextManager as any).pristineEpisodes = (
(contextManager as unknown as { pristineEpisodes: Episode[] }).pristineEpisodes = (
await import('./ir/mapper.js')
).IrMapper.toIr(history, new ContextTokenCalculator(4));
const result = await contextManager.projectCompressedHistory();
+14 -13
View File
@@ -8,6 +8,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
import { generateWorkingBufferView } from './graphUtils.js';
import { createMockEnvironment, createDummyEpisode } from '../testing/contextTestUtils.js';
import type { ContextEnvironment } from '../sidecar/environment.js';
import type { AgentThought, UserPrompt } from './types.js';
describe('graphUtils (View Generator)', () => {
let env: ContextEnvironment;
@@ -21,8 +22,8 @@ describe('graphUtils (View Generator)', () => {
it('returns pristine episodes untouched if under budget', () => {
const episodes = [
createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: '1' }]),
createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: '2' }]),
createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: '1' }]),
createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: '2' }]),
];
// We retain 5000 tokens. Total mock tokens = 200.
@@ -53,12 +54,12 @@ describe('graphUtils (View Generator)', () => {
expect(view[1].id).toBe('ep-2'); // Unchanged (newest)
expect(view[0].id).toBe('ep-1');
expect((view[0].trigger as any).semanticParts[0].presentation.text).toBe('<MASKED>');
expect((view[0].trigger as UserPrompt).semanticParts[0].presentation?.text).toBe('<MASKED>');
});
it('swaps to Summary variant when over budget', () => {
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: '1' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: '2' }]);
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: '1' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: '2' }]);
ep1.variants = {
'summary': { type: 'summary', status: 'ready', text: '<SUMMARY>', recoveredTokens: 50 }
@@ -71,15 +72,15 @@ describe('graphUtils (View Generator)', () => {
// The summary completely replaces the internal steps and clears the yield.
expect(view[0].steps).toHaveLength(1);
expect(view[0].steps[0].type).toBe('AGENT_THOUGHT');
expect((view[0].steps[0] as any).text).toBe('<SUMMARY>');
expect((view[0].steps[0] as AgentThought).text).toBe('<SUMMARY>');
expect(view[0].yield).toBeUndefined();
});
it('handles complex N-to-1 Snapshot skipping gracefully', () => {
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: '1' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: '2' }]);
const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ text: '3' }]);
const ep4 = createDummyEpisode('ep-4', 'USER_PROMPT', [{ text: '4' }]);
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: '1' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: '2' }]);
const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ type: 'text', text: '3' }]);
const ep4 = createDummyEpisode('ep-4', 'USER_PROMPT', [{ type: 'text', text: '4' }]);
// ep-3 has a snapshot that replaces [ep-1, ep-2, ep-3]
const snapshotEp = createDummyEpisode('snap-1', 'SYSTEM_EVENT', []);
@@ -103,8 +104,8 @@ describe('graphUtils (View Generator)', () => {
});
it('ignores variants that are not yet "ready"', () => {
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: '1' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: '2' }]);
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: '1' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: '2' }]);
ep1.variants = {
'masked': { type: 'masked', status: 'computing', text: '<MASKED>', recoveredTokens: 10 }
@@ -114,6 +115,6 @@ describe('graphUtils (View Generator)', () => {
// Because the variant was computing, it must fall back to the raw pristine text.
expect(view).toHaveLength(2);
expect((view[0].trigger as any).semanticParts[0].presentation).toBeUndefined();
expect((view[0].trigger as UserPrompt).semanticParts[0].presentation).toBeUndefined();
});
});
@@ -28,7 +28,7 @@ describe('EmergencyTruncationProcessor', () => {
it('bypasses processing if currentTokens <= maxTokens', async () => {
const episodes = [
createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'short' }])
createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'short' }])
];
// State says we are under budget (5000 < 10000)
const state = createDummyState(true, 0, new Set(), 5000, 10000);
@@ -41,9 +41,9 @@ describe('EmergencyTruncationProcessor', () => {
});
it('truncates episodes from the front (oldest) until targetTokens is met', async () => {
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'oldest' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: 'middle' }]);
const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ text: 'newest' }]);
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'oldest' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: 'middle' }]);
const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ type: 'text', text: 'newest' }]);
// Each is worth 100 tokens according to our mock
const episodes = [ep1, ep2, ep3];
@@ -62,9 +62,9 @@ describe('EmergencyTruncationProcessor', () => {
});
it('never drops protected episodes (e.g. system instructions)', async () => {
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'protected system prompt' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: 'middle' }]);
const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ text: 'newest' }]);
const ep1 = createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'protected system prompt' }]);
const ep2 = createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: 'middle' }]);
const ep3 = createDummyEpisode('ep-3', 'USER_PROMPT', [{ type: 'text', text: 'newest' }]);
const episodes = [ep1, ep2, ep3];
@@ -33,7 +33,7 @@ describe('StateSnapshotProcessor', () => {
it('bypasses processing if deficit is <= 0', async () => {
const episodes = [
createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'hello' }])
createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'hello' }])
];
// current: 100, max: 1000, retained: 200 (deficit 0)
const state = createDummyState(false, 0, new Set(), 100, 1000, 200);
@@ -48,7 +48,7 @@ describe('StateSnapshotProcessor', () => {
it('bypasses processing if not enough episodes to summarize (needs at least 2 inner episodes)', async () => {
const episodes = [
createDummyEpisode('ep-sys', 'SYSTEM_EVENT', []),
createDummyEpisode('ep-active', 'USER_PROMPT', [{ text: 'help' }]),
createDummyEpisode('ep-active', 'USER_PROMPT', [{ type: 'text', text: 'help' }]),
];
// current: 1000, max: 10000, retained: 500. Target deficit = 500
@@ -64,9 +64,9 @@ describe('StateSnapshotProcessor', () => {
it('summarizes intermediate episodes into a single snapshot episode', async () => {
const episodes = [
createDummyEpisode('ep-0', 'SYSTEM_EVENT', []),
createDummyEpisode('ep-1', 'USER_PROMPT', [{ text: 'old 1' }]),
createDummyEpisode('ep-2', 'USER_PROMPT', [{ text: 'old 2' }]),
createDummyEpisode('ep-3', 'USER_PROMPT', [{ text: 'current' }]),
createDummyEpisode('ep-1', 'USER_PROMPT', [{ type: 'text', text: 'old 1' }]),
createDummyEpisode('ep-2', 'USER_PROMPT', [{ type: 'text', text: 'old 2' }]),
createDummyEpisode('ep-3', 'USER_PROMPT', [{ type: 'text', text: 'current' }]),
];
// Target deficit = 200
@@ -13,7 +13,7 @@ import { ContextManager } from '../contextManager.js';
import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js';
import { DeterministicIdGenerator } from '../system/DeterministicIdGenerator.js';
import type { Episode } from '../ir/types.js';
import type { Episode, UserPrompt, SystemEvent, SemanticPart } from '../ir/types.js';
import type { ContextAccountingState } from '../pipeline.js';
import { randomUUID } from 'node:crypto';
@@ -38,21 +38,32 @@ export function createDummyState(
export function createDummyEpisode(
id: string,
type: 'USER_PROMPT' | 'SYSTEM_EVENT',
parts: unknown[] = [],
parts: SemanticPart[] = [],
toolSteps: Array<{ intent: Record<string, unknown>; observation: Record<string, unknown>; toolName?: string; tokens?: { intent: number; observation: number } }> = []
): Episode {
let trigger: UserPrompt | SystemEvent;
if (type === 'USER_PROMPT') {
trigger = {
id: randomUUID(),
type: 'USER_PROMPT',
semanticParts: parts,
metadata: { originalTokens: 100, currentTokens: 100, transformations: [] },
};
} else {
trigger = {
id: randomUUID(),
type: 'SYSTEM_EVENT',
name: 'dummy_event',
payload: {},
metadata: { originalTokens: 100, currentTokens: 100, transformations: [] },
};
}
return {
id,
timestamp: Date.now(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
trigger: {
id: randomUUID(),
type,
name: type === 'SYSTEM_EVENT' ? 'dummy_event' : undefined,
payload: type === 'SYSTEM_EVENT' ? {} : undefined,
semanticParts: type === 'USER_PROMPT' ? parts as any : undefined,
metadata: { originalTokens: 100, currentTokens: 100, transformations: [] },
} as any,
trigger,
steps: toolSteps.map(step => ({
id: randomUUID(),
type: 'TOOL_EXECUTION',