From 28bd094965a7a35d1b348dcfdc7cdd0a13435ac5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 9 Apr 2026 03:32:47 +0000 Subject: [PATCH] coverage --- .../rollingSummaryProcessor.test.ts | 63 ++++++++++++++++ .../context/sidecar/environmentImpl.test.ts | 67 +++++++++++++++++ .../core/src/context/sidecar/inbox.test.ts | 43 +++++++++++ packages/core/src/context/sidecar/inbox.ts | 2 +- .../core/src/context/sidecar/registry.test.ts | 71 +++++++++++++++++++ .../src/context/testing/contextTestUtils.ts | 51 ++++++------- 6 files changed, 265 insertions(+), 32 deletions(-) create mode 100644 packages/core/src/context/processors/rollingSummaryProcessor.test.ts create mode 100644 packages/core/src/context/sidecar/environmentImpl.test.ts create mode 100644 packages/core/src/context/sidecar/inbox.test.ts create mode 100644 packages/core/src/context/sidecar/registry.test.ts diff --git a/packages/core/src/context/processors/rollingSummaryProcessor.test.ts b/packages/core/src/context/processors/rollingSummaryProcessor.test.ts new file mode 100644 index 0000000000..224a33368c --- /dev/null +++ b/packages/core/src/context/processors/rollingSummaryProcessor.test.ts @@ -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'); + }); +}); diff --git a/packages/core/src/context/sidecar/environmentImpl.test.ts b/packages/core/src/context/sidecar/environmentImpl.test.ts new file mode 100644 index 0000000000..1998b91e61 --- /dev/null +++ b/packages/core/src/context/sidecar/environmentImpl.test.ts @@ -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); + }); +}); diff --git a/packages/core/src/context/sidecar/inbox.test.ts b/packages/core/src/context/sidecar/inbox.test.ts new file mode 100644 index 0000000000..e8690856b9 --- /dev/null +++ b/packages/core/src/context/sidecar/inbox.test.ts @@ -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'); + }); +}); diff --git a/packages/core/src/context/sidecar/inbox.ts b/packages/core/src/context/sidecar/inbox.ts index 828cf6b31c..df42a45bae 100644 --- a/packages/core/src/context/sidecar/inbox.ts +++ b/packages/core/src/context/sidecar/inbox.ts @@ -35,7 +35,7 @@ export class InboxSnapshotImpl implements InboxSnapshot { } getMessages(topic: string): ReadonlyArray> { - return this.messages.filter((m) => m.topic === topic) as unknown as ReadonlyArray>; + return this.messages.filter((m) => m.topic === topic) as ReadonlyArray>; } consume(messageId: string): void { diff --git a/packages/core/src/context/sidecar/registry.test.ts b/packages/core/src/context/sidecar/registry.test.ts new file mode 100644 index 0000000000..134ef1ee89 --- /dev/null +++ b/packages/core/src/context/sidecar/registry.test.ts @@ -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(); + }); +}); diff --git a/packages/core/src/context/testing/contextTestUtils.ts b/packages/core/src/context/testing/contextTestUtils.ts index 4b33e53c3a..265101c128 100644 --- a/packages/core/src/context/testing/contextTestUtils.ts +++ b/packages/core/src/context/testing/contextTestUtils.ts @@ -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 { - 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 }; } /**