mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-16 23:02:51 -07:00
fixing
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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', [])];
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ export class ProcessorRegistry {
|
||||
private processors = new Map<string, ContextProcessorDef<unknown>>();
|
||||
|
||||
register<TOptions>(def: ContextProcessorDef<TOptions>) {
|
||||
this.processors.set(def.id, def as unknown as ContextProcessorDef<unknown>);
|
||||
this.processors.set(def.id, def);
|
||||
}
|
||||
|
||||
get(id: string): ContextProcessorDef<unknown> {
|
||||
get(id: string): ContextProcessorDef {
|
||||
const def = this.processors.get(id);
|
||||
if (!def) {
|
||||
throw new Error(`Context Processor [${id}] is not registered.`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user