Files
gemini-cli/packages/core/src/context/testing/contextTestUtils.ts
T

266 lines
8.0 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
2026-04-06 17:59:01 +00:00
import { AgentChatHistory } from '../../core/agentChatHistory.js';
import { ContextManager } from '../contextManager.js';
2026-04-06 22:27:32 +00:00
import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js';
import { DeterministicIdGenerator } from '../system/DeterministicIdGenerator.js';
2026-04-06 22:47:43 +00:00
import { randomUUID } from 'node:crypto';
2026-04-08 17:12:12 +00:00
import { ContextTracer } from '../tracer.js';
import { ContextEnvironmentImpl } from '../sidecar/environmentImpl.js';
import { SidecarLoader } from '../sidecar/SidecarLoader.js';
import { ContextEventBus } from '../eventBus.js';
2026-04-09 01:52:44 +00:00
import { SidecarRegistry } from '../sidecar/registry.js';
2026-04-08 17:12:12 +00:00
import { registerBuiltInProcessors } from '../sidecar/builtins.js';
2026-04-09 02:44:14 +00:00
import { PipelineOrchestrator } from '../sidecar/orchestrator.js';
2026-04-08 17:12:12 +00:00
import type { ConcreteNode, ToolExecution } from '../ir/types.js';
import type { ContextEnvironment } from '../sidecar/environment.js';
import type { Config } from '../../config/config.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
2026-04-09 03:35:12 +00:00
import type { Content , GenerateContentResponse } from '@google/genai';
2026-04-06 22:47:43 +00:00
2026-04-08 23:37:46 +00:00
/**
* Creates a valid mock GenerateContentResponse with the provided text.
* Used to avoid having to manually construct the deeply nested candidate/content/part structure.
*/
export const createMockGenerateContentResponse = (text: string): GenerateContentResponse =>
2026-04-09 04:48:07 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-04-08 23:37:46 +00:00
({
candidates: [{ content: { role: 'model', parts: [{ text }] }, index: 0 }],
}) as GenerateContentResponse;
2026-04-06 22:47:43 +00:00
2026-04-08 02:34:06 +00:00
export function createDummyNode(
logicalParentId: string,
type: 'USER_PROMPT' | 'SYSTEM_EVENT' | 'AGENT_THOUGHT' | 'AGENT_YIELD',
tokens = 100,
overrides?: Partial<ConcreteNode>,
2026-04-08 20:27:00 +00:00
id?: string,
2026-04-08 02:34:06 +00:00
): ConcreteNode {
2026-04-08 17:12:12 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-04-08 02:34:06 +00:00
return {
id: id || randomUUID(),
episodeId: logicalParentId,
logicalParentId,
2026-04-09 04:48:07 +00:00
type,
2026-04-08 02:34:06 +00:00
timestamp: Date.now(),
text: `Dummy ${type}`,
name: type === 'SYSTEM_EVENT' ? 'dummy_event' : undefined,
payload: type === 'SYSTEM_EVENT' ? {} : undefined,
semanticParts: [],
metadata: {
originalTokens: tokens,
currentTokens: tokens,
transformations: [],
},
...overrides,
} as unknown as ConcreteNode;
}
export function createDummyToolNode(
logicalParentId: string,
intentTokens = 100,
obsTokens = 200,
overrides?: Partial<ToolExecution>,
2026-04-08 20:27:00 +00:00
id?: string,
2026-04-08 02:34:06 +00:00
): ToolExecution {
2026-04-08 17:12:12 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-04-08 02:34:06 +00:00
return {
id: id || randomUUID(),
episodeId: logicalParentId,
logicalParentId,
type: 'TOOL_EXECUTION',
timestamp: Date.now(),
toolName: 'dummy_tool',
intent: { action: 'test' },
observation: { result: 'ok' },
tokens: {
intent: intentTokens,
observation: obsTokens,
},
metadata: {
originalTokens: intentTokens + obsTokens,
currentTokens: intentTokens + obsTokens,
transformations: [],
},
...overrides,
} as unknown as ToolExecution;
}
2026-04-08 20:27:00 +00:00
export function createMockEnvironment(
overrides?: Partial<ContextEnvironment>,
): ContextEnvironment {
2026-04-09 04:48:07 +00:00
const mockClient: Partial<BaseLlmClient> = {
2026-04-09 03:32:47 +00:00
generateContent: vi.fn().mockResolvedValue(createMockGenerateContentResponse('Mock LLM summary response')),
2026-04-09 04:48:07 +00:00
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const llmClient = mockClient as BaseLlmClient;
2026-04-09 03:32:47 +00:00
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-')
);
2026-04-08 22:35:34 +00:00
2026-04-09 04:48:07 +00:00
if (overrides) {
Object.assign(env, overrides);
}
return env;
}
/**
* Creates a block of synthetic conversation history designed to consume a specific number of tokens.
* Assumes roughly 4 characters per token for standard English text.
*/
2026-04-09 15:41:15 +00:00
import { InboxSnapshotImpl } from '../sidecar/inbox.js';
import type { ContextWorkingBuffer, InboxMessage, ProcessArgs } from '../pipeline.js';
export class FakeContextWorkingBuffer implements ContextWorkingBuffer {
readonly nodes: readonly ConcreteNode[];
private readonly nodesById = new Map<string, ConcreteNode>();
private readonly nodesByEpisode = new Map<string, ConcreteNode[]>();
constructor(nodes: readonly ConcreteNode[]) {
this.nodes = nodes;
for (const node of nodes) {
this.nodesById.set(node.id, node);
const parentId = node.logicalParentId || 'orphan';
const epNodes = this.nodesByEpisode.get(parentId) || [];
epNodes.push(node);
this.nodesByEpisode.set(parentId, epNodes);
}
}
getPristineNode(id: string): ConcreteNode | undefined {
return this.nodesById.get(id);
}
getLineage(id: string): readonly ConcreteNode[] {
const node = this.nodesById.get(id);
if (!node) return [];
return this.nodesByEpisode.get(node.logicalParentId || 'orphan') || [];
}
}
export function createMockProcessArgs(targets: ConcreteNode[], bufferNodes: ConcreteNode[] = [], inboxMessages: InboxMessage[] = []): ProcessArgs {
return {
targets,
buffer: new FakeContextWorkingBuffer(bufferNodes.length ? bufferNodes : targets),
inbox: new InboxSnapshotImpl(inboxMessages),
};
}
export function createSyntheticHistory(
numTurns: number,
tokensPerTurn: number,
): Content[] {
const history: Content[] = [];
const charsPerTurn = tokensPerTurn * 1;
for (let i = 0; i < numTurns; i++) {
history.push({
role: 'user',
parts: [{ text: `User turn ${i}. ` + 'A'.repeat(charsPerTurn) }],
});
history.push({
role: 'model',
parts: [{ text: `Model response ${i}. ` + 'B'.repeat(charsPerTurn) }],
});
}
return history;
}
/**
* Creates a fully mocked Config object tailored for Context Component testing.
*/
export function createMockContextConfig(
overrides?: Record<string, unknown>,
llmClientOverride?: unknown,
): Config {
const defaultConfig = {
isContextManagementEnabled: vi.fn().mockReturnValue(true),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
},
getBaseLlmClient: vi.fn().mockReturnValue(
llmClientOverride || {
generateContent: vi.fn().mockResolvedValue({
text: '<mocked_snapshot>Synthesized state</mocked_snapshot>',
}),
},
),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
getTargetDir: vi.fn().mockReturnValue('/tmp'),
getSessionId: vi.fn().mockReturnValue('test-session'),
2026-04-06 19:54:09 +00:00
getExperimentalContextSidecarConfig: vi.fn().mockReturnValue(undefined),
};
2026-04-06 17:59:01 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return { ...defaultConfig, ...overrides } as unknown as Config;
}
/**
* Wires up a full ContextManager component with an AgentChatHistory and active background workers.
*/
2026-04-08 20:27:00 +00:00
export function setupContextComponentTest(
config: Config,
sidecarOverride?: import('../sidecar/types.js').SidecarConfig,
) {
const chatHistory = new AgentChatHistory();
2026-04-09 01:52:44 +00:00
const registry = new SidecarRegistry();
2026-04-07 03:58:50 +00:00
registerBuiltInProcessors(registry);
2026-04-08 20:27:00 +00:00
const sidecar = sidecarOverride || SidecarLoader.fromConfig(config, registry);
2026-04-07 04:46:04 +00:00
const tracer = new ContextTracer({
targetDir: '/tmp',
sessionId: 'test-session',
});
2026-04-06 19:18:17 +00:00
const eventBus = new ContextEventBus();
const env = new ContextEnvironmentImpl(
2026-04-06 17:59:01 +00:00
config.getBaseLlmClient(),
'test prompt-id',
'test-session',
'/tmp',
'/tmp/gemini-test',
tracer,
1,
2026-04-07 04:46:04 +00:00
eventBus,
);
2026-04-09 02:44:14 +00:00
const orchestrator = new PipelineOrchestrator(
2026-04-07 04:46:04 +00:00
sidecar,
env,
2026-04-09 02:44:14 +00:00
eventBus,
2026-04-07 04:46:04 +00:00
tracer,
2026-04-09 02:44:14 +00:00
registry
);
2026-04-09 02:44:14 +00:00
const contextManager = new ContextManager(
sidecar,
env,
tracer,
orchestrator,
chatHistory
);
2026-04-09 02:44:14 +00:00
// The async worker is now internally managed by ContextManager
return { chatHistory, contextManager };
}