diff --git a/packages/core/src/context/contextManager.golden.test.ts b/packages/core/src/context/contextManager.golden.test.ts index 90621144cf..3694b6ab69 100644 --- a/packages/core/src/context/contextManager.golden.test.ts +++ b/packages/core/src/context/contextManager.golden.test.ts @@ -1,5 +1,3 @@ -import { ProcessorRegistry } from "./sidecar/registry.js"; -import { registerBuiltInProcessors } from "./sidecar/builtins.js"; /** * @license * Copyright 2026 Google LLC @@ -21,11 +19,13 @@ import { SidecarLoader } from './sidecar/SidecarLoader.js'; import { ContextTracer } from './tracer.js'; 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'; +import { ProcessorRegistry } from "./sidecar/registry.js"; +import { registerBuiltInProcessors } from "./sidecar/builtins.js"; + expect.addSnapshotSerializer({ test: (val) => diff --git a/packages/core/src/context/contextManager.ts b/packages/core/src/context/contextManager.ts index c9e9d417af..6d1e28ff2f 100644 --- a/packages/core/src/context/contextManager.ts +++ b/packages/core/src/context/contextManager.ts @@ -4,40 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ import type { Content } from '@google/genai'; - - import type { AgentChatHistory } from '../core/agentChatHistory.js'; import { debugLogger } from '../utils/debugLogger.js'; import type { Episode } from './ir/types.js'; - import type { ContextEventBus } from './eventBus.js'; import type { ContextTracer } from './tracer.js'; - - - import type { ContextEnvironment } from './sidecar/environment.js'; - import type { SidecarConfig } from './sidecar/types.js'; - import { PipelineOrchestrator } from './sidecar/orchestrator.js'; import { HistoryObserver } from './historyObserver.js'; - import { generateWorkingBufferView } from './ir/graphUtils.js'; - - - - - - - - - import { IrProjector } from './ir/projector.js'; - -import './sidecar/builtins.js'; - -import { ProcessorRegistry } from './sidecar/registry.js'; import { registerBuiltInProcessors } from './sidecar/builtins.js'; +import { ProcessorRegistry } from './sidecar/registry.js'; export class ContextManager { diff --git a/packages/core/src/context/ir/graphUtils.ts b/packages/core/src/context/ir/graphUtils.ts index 45234c9345..f68aca1063 100644 --- a/packages/core/src/context/ir/graphUtils.ts +++ b/packages/core/src/context/ir/graphUtils.ts @@ -41,19 +41,30 @@ export function generateWorkingBufferView( continue; } - let projectedEp = { - ...ep, - trigger: { + let projectedTrigger: typeof ep.trigger; + + if (ep.trigger.type === 'USER_PROMPT') { + projectedTrigger = { ...ep.trigger, metadata: { - ...ep.trigger?.metadata, - transformations: [...(ep.trigger?.metadata?.transformations || [])], + ...ep.trigger.metadata, + transformations: [...(ep.trigger.metadata?.transformations || [])], }, - semanticParts: - ep.trigger?.type === 'USER_PROMPT' - ? [...(ep.trigger.semanticParts || []).map((sp) => ({ ...sp }))] - : undefined, - } as unknown as typeof ep.trigger, + semanticParts: ep.trigger.semanticParts.map(sp => ({...sp})) + }; + } else { + projectedTrigger = { + ...ep.trigger, + metadata: { + ...ep.trigger.metadata, + transformations: [...(ep.trigger.metadata?.transformations || [])], + } + }; + } + + let projectedEp: Episode = { + ...ep, + trigger: projectedTrigger, steps: ep.steps.map( (step) => ({ @@ -62,7 +73,7 @@ export function generateWorkingBufferView( ...step.metadata, transformations: [...(step.metadata?.transformations || [])], }, - }) as unknown as typeof step, + }) ), yield: ep.yield ? { @@ -87,7 +98,7 @@ export function generateWorkingBufferView( snapshot.status === 'ready' && snapshot.type === 'snapshot' ) { - projectedEp = snapshot.episode as any; + projectedEp = snapshot.episode; // Mark all the episodes this snapshot covers to be skipped by the backwards sweep. for (const id of snapshot.replacedEpisodeIds) { skippedIds.add(id); @@ -121,7 +132,7 @@ export function generateWorkingBufferView( ], }, }, - ] as any; + ] as typeof projectedEp.steps; projectedEp.yield = undefined; tracer.logEvent( 'ViewGenerator', diff --git a/packages/core/src/context/sidecar/SidecarLoader.test.ts b/packages/core/src/context/sidecar/SidecarLoader.test.ts index c7e1badac5..6da19fb952 100644 --- a/packages/core/src/context/sidecar/SidecarLoader.test.ts +++ b/packages/core/src/context/sidecar/SidecarLoader.test.ts @@ -1,10 +1,10 @@ -import { ProcessorRegistry } from "./registry.js"; -import { registerBuiltInProcessors } from "./builtins.js"; /** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ +import { ProcessorRegistry } from "./registry.js"; +import { registerBuiltInProcessors } from "./builtins.js"; import { describe, it, expect, beforeEach } from 'vitest'; import { SidecarLoader } from './SidecarLoader.js'; import { defaultSidecarProfile } from './profiles.js'; diff --git a/packages/core/src/context/sidecar/builtins.ts b/packages/core/src/context/sidecar/builtins.ts index ef811bd075..8c34face24 100644 --- a/packages/core/src/context/sidecar/builtins.ts +++ b/packages/core/src/context/sidecar/builtins.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ProcessorRegistry } from './registry.js'; +import type { ProcessorRegistry } from './registry.js'; import { ToolMaskingProcessor, type ToolMaskingProcessorOptions } from '../processors/toolMaskingProcessor.js'; import { BlobDegradationProcessor } from '../processors/blobDegradationProcessor.js'; import { SemanticCompressionProcessor, type SemanticCompressionProcessorOptions } from '../processors/semanticCompressionProcessor.js'; diff --git a/packages/core/src/context/sidecar/orchestrator.test.ts b/packages/core/src/context/sidecar/orchestrator.test.ts index 38d3a1ffb6..b6d65ede9f 100644 --- a/packages/core/src/context/sidecar/orchestrator.test.ts +++ b/packages/core/src/context/sidecar/orchestrator.test.ts @@ -10,7 +10,7 @@ import { ProcessorRegistry } from './registry.js'; import { createMockEnvironment, createDummyState, createDummyEpisode } from '../testing/contextTestUtils.js'; import type { ContextEnvironment } from './environment.js'; import type { ContextAccountingState, ContextProcessor } from '../pipeline.js'; -import type { SidecarConfig } from './types.js'; +import type { PipelineDef, ProcessorConfig, SidecarConfig } from './types.js'; import type { ContextEventBus } from '../eventBus.js'; import type { EpisodeEditor } from '../ir/episodeEditor.js'; @@ -56,26 +56,26 @@ class ThrowingProcessor implements ContextProcessor { describe('PipelineOrchestrator (Component)', () => { let env: ContextEnvironment; let eventBus: ContextEventBus; + let registry: ProcessorRegistry; beforeEach(() => { vi.resetAllMocks(); env = createMockEnvironment(); eventBus = env.eventBus; + registry = new ProcessorRegistry(); // Register our test processors - ProcessorRegistry.register({ id: 'DummySyncProcessor', create: () => new DummySyncProcessor() }); - ProcessorRegistry.register({ id: 'DummyAsyncProcessor', create: () => new DummyAsyncProcessor() }); - ProcessorRegistry.register({ id: 'ThrowingProcessor', create: () => new ThrowingProcessor() }); + registry.register({ id: 'DummySyncProcessor', create: () => new DummySyncProcessor() }); + registry.register({ id: 'DummyAsyncProcessor', create: () => new DummyAsyncProcessor() }); + registry.register({ id: 'ThrowingProcessor', create: () => new ThrowingProcessor() }); }); afterEach(() => { // Cleanup registry to not pollute other tests - (ProcessorRegistry as any).processors.delete('DummySyncProcessor'); - (ProcessorRegistry as any).processors.delete('DummyAsyncProcessor'); - (ProcessorRegistry as any).processors.delete('ThrowingProcessor'); + registry.clear(); }); - const createConfig = (pipelines: any[]): SidecarConfig => ({ + const createConfig = (pipelines: PipelineDef[]): SidecarConfig => ({ budget: { maxTokens: 100, retainedTokens: 50 }, gcBackstop: { strategy: 'truncate', target: 'max' }, pipelines @@ -87,11 +87,12 @@ describe('PipelineOrchestrator (Component)', () => { name: 'Sync', execution: 'blocking', triggers: [], - processors: [{ processorId: 'DummySyncProcessor' }] + processors: [{ processorId: 'DummySyncProcessor' } as unknown as ProcessorConfig] } ]); - const orchestrator = new PipelineOrchestrator(config, env, eventBus, env.tracer); + const orchestrator = new PipelineOrchestrator(config, env, eventBus, env.tracer, registry); + // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((orchestrator as any).instantiatedProcessors.has('DummySyncProcessor')).toBe(true); }); @@ -101,11 +102,11 @@ describe('PipelineOrchestrator (Component)', () => { name: 'Bad', execution: 'blocking', triggers: [], - processors: [{ processorId: 'DoesNotExist' }] + processors: [{ processorId: 'DoesNotExist' } as unknown as ProcessorConfig] } ]); - expect(() => new PipelineOrchestrator(config, env, eventBus, env.tracer)) + expect(() => new PipelineOrchestrator(config, env, eventBus, env.tracer, registry)) .toThrow('Context Processor [DoesNotExist] is not registered.'); }); @@ -115,10 +116,10 @@ describe('PipelineOrchestrator (Component)', () => { name: 'SyncPipe', execution: 'blocking', triggers: [], - processors: [{ processorId: 'DummySyncProcessor' }] + processors: [{ processorId: 'DummySyncProcessor' } as unknown as ProcessorConfig] } ]); - const orchestrator = new PipelineOrchestrator(config, env, eventBus, env.tracer); + const orchestrator = new PipelineOrchestrator(config, env, eventBus, env.tracer, registry); const episodes = [createDummyEpisode('1', 'USER_PROMPT', [])]; const state = createDummyState(false); @@ -126,7 +127,7 @@ describe('PipelineOrchestrator (Component)', () => { const result = await orchestrator.executePipeline('SyncPipe', episodes, state); expect(result).toHaveLength(1); - expect((result[0] as any).dummyModified).toBe(true); + expect((result[0] as unknown as {dummyModified: boolean}).dummyModified).toBe(true); }); it('executes background pipelines asynchronously without blocking the return', async () => { @@ -135,10 +136,10 @@ describe('PipelineOrchestrator (Component)', () => { name: 'AsyncPipe', execution: 'background', triggers: [], - processors: [{ processorId: 'DummyAsyncProcessor' }] + processors: [{ processorId: 'DummyAsyncProcessor' } as unknown as ProcessorConfig] } ]); - const orchestrator = new PipelineOrchestrator(config, env, eventBus, env.tracer); + const orchestrator = new PipelineOrchestrator(config, env, eventBus, env.tracer, registry); const episodes = [createDummyEpisode('1', 'USER_PROMPT', [])]; const state = createDummyState(false); @@ -147,7 +148,7 @@ describe('PipelineOrchestrator (Component)', () => { const result = await orchestrator.executePipeline('AsyncPipe', episodes, state); expect(result).toHaveLength(1); - expect((result[0] as any).asyncModified).toBeUndefined(); // Not modified yet! + expect((result[0] as unknown as {asyncModified: unknown}).asyncModified).toBeUndefined(); // Not modified yet! // Wait for the background task to complete (50ms delay in DummyAsyncProcessor) await new Promise(resolve => setTimeout(resolve, 60)); @@ -159,10 +160,10 @@ describe('PipelineOrchestrator (Component)', () => { name: 'ThrowingPipe', execution: 'blocking', triggers: [], - processors: [{ processorId: 'ThrowingProcessor' }] + processors: [{ processorId: 'ThrowingProcessor' } as unknown as ProcessorConfig] } ]); - const orchestrator = new PipelineOrchestrator(config, env, eventBus, env.tracer); + const orchestrator = new PipelineOrchestrator(config, env, eventBus, env.tracer, registry); const episodes = [createDummyEpisode('1', 'USER_PROMPT', [])]; const state = createDummyState(false); @@ -180,14 +181,15 @@ describe('PipelineOrchestrator (Component)', () => { name: 'PressureRelief', execution: 'background', triggers: ['budget_exceeded'], - processors: [{ processorId: 'DummyAsyncProcessor' }] + processors: [{ processorId: 'DummyAsyncProcessor' } as unknown as ProcessorConfig] } ]); // Spy on the private method to see if the trigger fires it + // eslint-disable-next-line @typescript-eslint/no-explicit-any const executeSpy = vi.spyOn(PipelineOrchestrator.prototype as any, 'executePipelineAsync'); - new PipelineOrchestrator(config, env, eventBus, env.tracer); + new PipelineOrchestrator(config, env, eventBus, env.tracer, registry); const episodes = [createDummyEpisode('1', 'USER_PROMPT', [])]; diff --git a/packages/core/src/context/sidecar/registry.ts b/packages/core/src/context/sidecar/registry.ts index 0dcfaa21bc..46232baa23 100644 --- a/packages/core/src/context/sidecar/registry.ts +++ b/packages/core/src/context/sidecar/registry.ts @@ -23,10 +23,10 @@ export class ProcessorRegistry { private processors = new Map>(); register(def: ContextProcessorDef) { - this.processors.set(def.id, def as unknown as ContextProcessorDef); + this.processors.set(def.id, def); } - get(id: string): ContextProcessorDef { + get(id: string): ContextProcessorDef { const def = this.processors.get(id); if (!def) { throw new Error(`Context Processor [${id}] is not registered.`); diff --git a/packages/core/src/context/sidecar/schema.ts b/packages/core/src/context/sidecar/schema.ts index 0319d3522c..066d07a2b8 100644 --- a/packages/core/src/context/sidecar/schema.ts +++ b/packages/core/src/context/sidecar/schema.ts @@ -3,7 +3,7 @@ * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import { ProcessorRegistry } from './registry.js'; +import type { ProcessorRegistry } from './registry.js'; import './builtins.js'; export function getSidecarConfigSchema(registry: ProcessorRegistry) { diff --git a/packages/core/src/context/testing.md b/packages/core/src/context/testing.md deleted file mode 100644 index f0cd794858..0000000000 --- a/packages/core/src/context/testing.md +++ /dev/null @@ -1,42 +0,0 @@ -# Context Pipeline Testing Strategy & Audit - -## Philosophy: Defense in Depth -Our testing strategy avoids the "endless tax" of brittle tests by strictly separating concerns: -1. **Unit Tests (Processors, System Fakes, Mappers):** Exhaustively test logical boundaries, token math, and state transformations. Driven by shared, DRY test factories (no repetitive boilerplate). -2. **Component Tests (ContextManager, Orchestrator):** Test the *wiring* and *triggers*. Verify that barriers block, background pipelines execute, and events fire correctly. -3. **Golden / E2E Tests:** Test emergent behavior. Pass in complex, raw chat histories and assert the exact final projected `Content[]` output against committed JSON snapshots. - ---- - -## Audit Checklist & Coverage Tracker - -### 1. The Tooling Library (`contextTestUtils.ts`) -- [x] Implement `ContextTestBuilder` or shared factory functions (`createDummyEpisode`, `createDummyState`). -- [x] Ensure all existing tests are migrated to use these helpers to establish the pattern. - -### 2. Unit Tests (The Processors & Map/Reduce) -Goal: Ensure every component gracefully handles boundary conditions (budget satisfied vs. deficit), skips protected IDs, and correctly transforms IR. -- [x] `BlobDegradationProcessor` (Completed) -- [x] `ToolMaskingProcessor` (Completed) -- [x] `HistorySquashingProcessor` (Completed) -- [x] `SemanticCompressionProcessor` (Completed) -- [x] `StateSnapshotProcessor` (Completed) -- [x] `EmergencyTruncationProcessor` (Completed) -- [x] `ContextTracer` (Completed) -- [x] `SidecarLoader` (Completed) -- [x] `IrMapper` / `graphUtils` (Completed - Handles Multi-Tool Concurrency and Backwards Graph Traversal) - -### 3. Component Tests (The Orchestration) -Goal: Prove the sidecar configuration accurately drives runtime behavior without testing the processor logic itself. -- [x] `PipelineOrchestrator`: Test sync vs. async routing, error swallowing, and trigger setup. -- [ ] `ContextManager`: Test `subscribeToHistory` (Opportunistic triggers). -- [ ] `ContextManager`: Test `project()` (Synchronous barrier triggers). - -### 4. Golden / E2E Tests -- [ ] `contextManager.golden.test.ts`: Ensure we have a scenario representing a "Day in the Life" of the CLI (some images, some huge tool outputs, deep history) mapping to a snapshot. - ---- - -## Next Actions -1. Audit the ContextManager component tests (opportunistic updates & sync barrier). -2. Finalize the End-to-End "Day in the Life" Golden Snapshot test.