This commit is contained in:
Your Name
2026-04-09 03:32:47 +00:00
parent 0179a140f0
commit 28bd094965
6 changed files with 265 additions and 32 deletions
@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest';
import { RollingSummaryProcessor } from './rollingSummaryProcessor.js';
import { createMockEnvironment, createDummyNode } from '../testing/contextTestUtils.js';
describe('RollingSummaryProcessor', () => {
it('should initialize with correct default options', () => {
const env = createMockEnvironment();
const processor = RollingSummaryProcessor.create(env, { target: 'incremental' });
expect(processor.id).toBe('RollingSummaryProcessor');
});
it('should summarize older nodes when the deficit exceeds the threshold', async () => {
// env.tokenCalculator uses charsPerToken=1 based on createMockEnvironment
const env = createMockEnvironment();
// We want to free exactly 100 tokens.
// We will supply nodes that cost 50 tokens each.
const processor = RollingSummaryProcessor.create(env, {
target: 'freeNTokens',
freeTokensTarget: 100
});
const text50 = 'A'.repeat(50);
const targets = [
createDummyNode('ep1', 'USER_PROMPT', 50, { semanticParts: [{ type: 'text', text: text50 }] }, 'id1'),
createDummyNode('ep1', 'AGENT_THOUGHT', 50, { text: text50 }, 'id2'),
createDummyNode('ep1', 'AGENT_YIELD', 50, { text: text50 }, 'id3'),
];
const result = await processor.process({ targets, buffer: {} as any, inbox: {} as any });
// 3 nodes at 50 cost each.
// The first node (id1) is the initial USER_PROMPT and is always skipped by RollingSummaryProcessor.
// Node id2 adds 50 deficit. Node id3 adds 50 deficit. Total = 100 deficit, which hits the target break point.
// Thus, id2 and id3 are summarized into a new ROLLING_SUMMARY node.
expect(result.length).toBe(2);
expect(result[0].type).toBe('USER_PROMPT');
expect(result[1].type).toBe('ROLLING_SUMMARY');
});
it('should preserve targets if deficit does not trigger summary', async () => {
const env = createMockEnvironment();
// We want to free 100 tokens, but our nodes will only cost 10 tokens each.
const processor = RollingSummaryProcessor.create(env, {
target: 'freeNTokens',
freeTokensTarget: 100
});
const text10 = 'A'.repeat(10);
const targets = [
createDummyNode('ep1', 'USER_PROMPT', 10, { semanticParts: [{ type: 'text', text: text10 }] }, 'id1'),
createDummyNode('ep1', 'AGENT_THOUGHT', 10, { text: text10 }, 'id2'),
];
const result = await processor.process({ targets, buffer: {} as any, inbox: {} as any });
// Deficit accumulator reaches 10. This is < 100 limit, and total summarizable nodes < 2 anyway.
expect(result.length).toBe(2);
expect(result[0].type).toBe('USER_PROMPT');
expect(result[1].type).toBe('AGENT_THOUGHT');
});
});
@@ -0,0 +1,67 @@
import { describe, it, expect } from 'vitest';
import { ContextEnvironmentImpl } from './environmentImpl.js';
import { ContextTracer } from '../tracer.js';
import { ContextEventBus } from '../eventBus.js';
import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js';
import { DeterministicIdGenerator } from '../system/DeterministicIdGenerator.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
describe('ContextEnvironmentImpl', () => {
it('should initialize with defaults correctly', () => {
const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'mock' });
const eventBus = new ContextEventBus();
const mockLlmClient = {} as BaseLlmClient;
const env = new ContextEnvironmentImpl(
mockLlmClient,
'mock-session',
'mock-prompt',
'/tmp/trace',
'/tmp/temp',
tracer,
4,
eventBus,
);
expect(env.llmClient).toBe(mockLlmClient);
expect(env.sessionId).toBe('mock-session');
expect(env.promptId).toBe('mock-prompt');
expect(env.traceDir).toBe('/tmp/trace');
expect(env.projectTempDir).toBe('/tmp/temp');
expect(env.tracer).toBe(tracer);
expect(env.charsPerToken).toBe(4);
expect(env.eventBus).toBe(eventBus);
// Default internals
expect(env.behaviorRegistry).toBeDefined();
expect(env.tokenCalculator).toBeDefined();
expect(env.fileSystem).toBeDefined();
expect(env.idGenerator).toBeDefined();
expect(env.inbox).toBeDefined();
expect(env.irMapper).toBeDefined();
});
it('should initialize with provided overrides', () => {
const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'mock' });
const eventBus = new ContextEventBus();
const mockLlmClient = {} as BaseLlmClient;
const fileSystem = new InMemoryFileSystem();
const idGenerator = new DeterministicIdGenerator('test-');
const env = new ContextEnvironmentImpl(
mockLlmClient,
'mock-session',
'mock-prompt',
'/tmp/trace',
'/tmp/temp',
tracer,
4,
eventBus,
fileSystem,
idGenerator,
);
expect(env.fileSystem).toBe(fileSystem);
expect(env.idGenerator).toBe(idGenerator);
});
});
@@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest';
import { LiveInbox, InboxSnapshotImpl } from './inbox.js';
import { DeterministicIdGenerator } from '../system/DeterministicIdGenerator.js';
describe('Inbox', () => {
it('should publish messages and provide snapshots', () => {
const inbox = new LiveInbox();
const idGenerator = new DeterministicIdGenerator('mock-uuid-');
inbox.publish('test-topic', { data: 'hello' }, idGenerator);
inbox.publish('other-topic', { data: 'world' }, idGenerator);
const messages = inbox.getMessages();
expect(messages.length).toBe(2);
expect(messages[0].topic).toBe('test-topic');
expect(messages[0].payload).toEqual({ data: 'hello' });
});
it('should drain consumed messages from the snapshot', () => {
const inbox = new LiveInbox();
const idGenerator = new DeterministicIdGenerator('mock-uuid-');
inbox.publish('test-topic', { data: 'hello' }, idGenerator);
inbox.publish('other-topic', { data: 'world' }, idGenerator);
const messages = inbox.getMessages();
const snapshot = new InboxSnapshotImpl(messages);
const filtered = snapshot.getMessages<{ data: string }>('test-topic');
expect(filtered.length).toBe(1);
expect(filtered[0].payload.data).toBe('hello');
// Consume the message
snapshot.consume(filtered[0].id);
// Provide the consumed IDs to the real inbox to drain them
inbox.drainConsumed(snapshot.getConsumedIds());
const finalMessages = inbox.getMessages();
expect(finalMessages.length).toBe(1);
expect(finalMessages[0].topic).toBe('other-topic');
});
});
+1 -1
View File
@@ -35,7 +35,7 @@ export class InboxSnapshotImpl implements InboxSnapshot {
}
getMessages<T = unknown>(topic: string): ReadonlyArray<InboxMessage<T>> {
return this.messages.filter((m) => m.topic === topic) as unknown as ReadonlyArray<InboxMessage<T>>;
return this.messages.filter((m) => m.topic === topic) as ReadonlyArray<InboxMessage<T>>;
}
consume(messageId: string): void {
@@ -0,0 +1,71 @@
import { describe, it, expect } from 'vitest';
import { SidecarRegistry } from './registry.js';
import type { ContextProcessorDef, ContextWorkerDef } from './registry.js';
describe('SidecarRegistry', () => {
it('should register and retrieve processors correctly', () => {
const registry = new SidecarRegistry();
const processorDef: ContextProcessorDef = {
id: 'TestProcessor',
schema: { type: 'object' },
create: () => ({} as any),
};
registry.registerProcessor(processorDef);
const retrieved = registry.getProcessor('TestProcessor');
expect(retrieved).toBe(processorDef);
});
it('should register and retrieve workers correctly', () => {
const registry = new SidecarRegistry();
const workerDef: ContextWorkerDef = {
id: 'TestWorker',
schema: { type: 'object' },
create: () => ({} as any),
};
registry.registerWorker(workerDef);
const retrieved = registry.getWorker('TestWorker');
expect(retrieved).toBe(workerDef);
});
it('should throw an error when retrieving unregistered processors', () => {
const registry = new SidecarRegistry();
expect(() => registry.getProcessor('Unknown')).toThrow('Context Processor [Unknown] is not registered.');
});
it('should throw an error when retrieving unregistered workers', () => {
const registry = new SidecarRegistry();
expect(() => registry.getWorker('Unknown')).toThrow('Context Worker [Unknown] is not registered.');
});
it('should return combined schemas', () => {
const registry = new SidecarRegistry();
registry.registerProcessor({
id: 'TestProcessor',
schema: { title: 'processorSchema' },
create: () => ({} as any),
});
registry.registerWorker({
id: 'TestWorker',
schema: { title: 'workerSchema' },
create: () => ({} as any),
});
const schemas = registry.getSchemas() as any[];
expect(schemas.length).toBe(2);
expect(schemas.find(s => s.title === 'processorSchema')).toBeDefined();
expect(schemas.find(s => s.title === 'workerSchema')).toBeDefined();
});
it('should safely clear the registry', () => {
const registry = new SidecarRegistry();
registry.registerProcessor({ id: 'TestProcessor', schema: {}, create: () => ({} as any) });
registry.registerWorker({ id: 'TestWorker', schema: {}, create: () => ({} as any) });
registry.clear();
expect(() => registry.getProcessor('TestProcessor')).toThrow();
expect(() => registry.getWorker('TestWorker')).toThrow();
});
});
@@ -14,10 +14,6 @@ import { ContextTracer } from '../tracer.js';
import { ContextEnvironmentImpl } from '../sidecar/environmentImpl.js';
import { SidecarLoader } from '../sidecar/SidecarLoader.js';
import { ContextEventBus } from '../eventBus.js';
import { ContextTokenCalculator } from '../utils/contextTokenCalculator.js';
import { IrNodeBehaviorRegistry } from '../ir/behaviorRegistry.js';
import { registerBuiltInBehaviors } from '../ir/builtinBehaviors.js';
import { IrMapper } from '../ir/mapper.js';
import { SidecarRegistry } from '../sidecar/registry.js';
import { registerBuiltInProcessors } from '../sidecar/builtins.js';
import { PipelineOrchestrator } from '../sidecar/orchestrator.js';
@@ -99,34 +95,27 @@ export function createDummyToolNode(
export function createMockEnvironment(
overrides?: Partial<ContextEnvironment>,
): ContextEnvironment {
const registry = new IrNodeBehaviorRegistry();
registerBuiltInBehaviors(registry);
const irMapper = new IrMapper(registry);
const llmClient = vi.fn().mockReturnValue({
generateContent: vi.fn().mockResolvedValue(createMockGenerateContentResponse('Mock LLM summary response')),
})() as unknown as BaseLlmClient;
const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'mock-session' });
const eventBus = new ContextEventBus();
const env = new ContextEnvironmentImpl(
llmClient,
'mock-session',
'mock-prompt-id',
'/tmp/.gemini/trace',
'/tmp/.gemini/tool-outputs',
tracer,
1,
eventBus,
new InMemoryFileSystem(),
new DeterministicIdGenerator('mock-uuid-')
);
return {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
llmClient: vi.fn().mockReturnValue({
generateContent: vi.fn().mockResolvedValue(createMockGenerateContentResponse('Mock LLM summary response')),
})() as unknown as BaseLlmClient,
promptId: 'mock-prompt-id',
sessionId: 'mock-session',
traceDir: '/tmp/.gemini/trace',
projectTempDir: '/tmp/.gemini/tool-outputs',
eventBus: new ContextEventBus(),
tracer: new ContextTracer({ targetDir: '/tmp', sessionId: 'mock-session' }),
charsPerToken: 1,
tokenCalculator: new ContextTokenCalculator(1, registry),
fileSystem: new InMemoryFileSystem(),
idGenerator: new DeterministicIdGenerator('mock-uuid-'),
behaviorRegistry: registry,
inbox: {
publish: vi.fn(),
getMessages: vi.fn().mockReturnValue([]),
drainConsumed: vi.fn(),
} as any,
irMapper,
...overrides,
} as ContextEnvironment;
return { ...env, ...overrides };
}
/**