mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-15 20:40:35 -07:00
fix disables
This commit is contained in:
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type { BaseLlmClient } from "../../core/baseLlmClient.js";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
@@ -19,8 +13,11 @@ import { ContextTracer } from '../tracer.js';
|
||||
import { ContextEventBus } from '../eventBus.js';
|
||||
import { PipelineOrchestrator } from '../sidecar/orchestrator.js';
|
||||
import { registerBuiltInProcessors } from '../sidecar/builtins.js';
|
||||
import { debugLogger } from "../../utils/debugLogger.js";
|
||||
import { ProcessorRegistry } from "../sidecar/registry.js";
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import { ProcessorRegistry } from '../sidecar/registry.js';
|
||||
import { DeterministicIdGenerator } from '../system/DeterministicIdGenerator.js';
|
||||
import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js';
|
||||
import type { BaseLlmClient } from '../../core/baseLlmClient.js';
|
||||
|
||||
export interface TurnSummary {
|
||||
turnIndex: number;
|
||||
@@ -39,7 +36,11 @@ export class SimulationHarness {
|
||||
private currentTurnIndex = 0;
|
||||
private tokenTrajectory: TurnSummary[] = [];
|
||||
|
||||
static async create(config: SidecarConfig, mockLlmClient: BaseLlmClient, mockTempDir = '/tmp/sim'): Promise<SimulationHarness> {
|
||||
static async create(
|
||||
config: SidecarConfig,
|
||||
mockLlmClient: BaseLlmClient,
|
||||
mockTempDir = '/tmp/sim',
|
||||
): Promise<SimulationHarness> {
|
||||
const harness = new SimulationHarness();
|
||||
await harness.init(config, mockLlmClient, mockTempDir);
|
||||
return harness;
|
||||
@@ -53,19 +54,17 @@ export class SimulationHarness {
|
||||
private async init(
|
||||
config: SidecarConfig,
|
||||
mockLlmClient: BaseLlmClient,
|
||||
mockTempDir: string
|
||||
mockTempDir: string,
|
||||
) {
|
||||
this.config = config;
|
||||
const registry = new ProcessorRegistry();
|
||||
// Register all standard processors
|
||||
registerBuiltInProcessors(registry);
|
||||
|
||||
this.tracer = new ContextTracer({ targetDir: mockTempDir, sessionId: 'sim-session' });
|
||||
|
||||
// Using real token calculator instead of mock, so we test actual string sizes
|
||||
const InMemoryFS = (await import('../system/InMemoryFileSystem.js')).InMemoryFileSystem;
|
||||
const DetIdGen = (await import('../system/DeterministicIdGenerator.js')).DeterministicIdGenerator;
|
||||
|
||||
this.tracer = new ContextTracer({
|
||||
targetDir: mockTempDir,
|
||||
sessionId: 'sim-session',
|
||||
});
|
||||
this.env = new ContextEnvironmentImpl(
|
||||
mockLlmClient,
|
||||
'sim-prompt',
|
||||
@@ -75,12 +74,24 @@ export class SimulationHarness {
|
||||
this.tracer,
|
||||
4, // 4 chars per token average
|
||||
this.eventBus,
|
||||
new InMemoryFS(),
|
||||
new DetIdGen()
|
||||
new InMemoryFileSystem(),
|
||||
new DeterministicIdGenerator(),
|
||||
);
|
||||
|
||||
this.orchestrator = new PipelineOrchestrator(config, this.env, this.eventBus, this.tracer, registry);
|
||||
this.contextManager = ContextManager.create(config, this.env, this.tracer, this.orchestrator, registry);
|
||||
this.orchestrator = new PipelineOrchestrator(
|
||||
config,
|
||||
this.env,
|
||||
this.eventBus,
|
||||
this.tracer,
|
||||
registry,
|
||||
);
|
||||
this.contextManager = ContextManager.create(
|
||||
config,
|
||||
this.env,
|
||||
this.tracer,
|
||||
this.orchestrator,
|
||||
registry,
|
||||
);
|
||||
this.contextManager.subscribeToHistory(this.chatHistory);
|
||||
}
|
||||
|
||||
@@ -92,61 +103,74 @@ export class SimulationHarness {
|
||||
// 1. Append the new messages
|
||||
const currentHistory = this.chatHistory.get();
|
||||
this.chatHistory.set([...currentHistory, ...messages]);
|
||||
|
||||
|
||||
// 2. Measure tokens immediately after append (Before background processing)
|
||||
const tokensBefore = this.env.tokenCalculator.calculateEpisodeListTokens(
|
||||
this.contextManager.getWorkingBufferView()
|
||||
this.contextManager.getWorkingBufferView(),
|
||||
);
|
||||
debugLogger.log(`[Turn ${this.currentTurnIndex}] Tokens BEFORE: ${tokensBefore}`);
|
||||
|
||||
debugLogger.log(
|
||||
`[Turn ${this.currentTurnIndex}] Tokens BEFORE: ${tokensBefore}`,
|
||||
);
|
||||
|
||||
// 3. Yield to event loop to allow internal async subscribers and orchestrator to finish
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// 3.1 Simulate what projectCompressedHistory does with the sync handlers
|
||||
let currentView = this.contextManager.getWorkingBufferView();
|
||||
const currentTokens = this.env.tokenCalculator.calculateEpisodeListTokens(currentView);
|
||||
const currentTokens =
|
||||
this.env.tokenCalculator.calculateEpisodeListTokens(currentView);
|
||||
if (this.config.budget && currentTokens > this.config.budget.maxTokens) {
|
||||
debugLogger.log(`[Turn ${this.currentTurnIndex}] Sync panic triggered! ${currentTokens} > ${this.config.budget.maxTokens}`);
|
||||
const syncPipelines = this.config.pipelines.filter(p => p.execution === 'blocking');
|
||||
debugLogger.log(
|
||||
`[Turn ${this.currentTurnIndex}] Sync panic triggered! ${currentTokens} > ${this.config.budget.maxTokens}`,
|
||||
);
|
||||
const syncPipelines = this.config.pipelines.filter(
|
||||
(p) => p.execution === 'blocking',
|
||||
);
|
||||
const orchestrator = this.orchestrator;
|
||||
for (const pipe of syncPipelines) {
|
||||
await orchestrator.executePipeline(pipe.name, currentView, {
|
||||
currentTokens,
|
||||
maxTokens: this.config.budget.maxTokens,
|
||||
retainedTokens: this.config.budget.retainedTokens,
|
||||
isBudgetSatisfied: false,
|
||||
deficitTokens: currentTokens - this.config.budget.maxTokens,
|
||||
protectedEpisodeIds: new Set()
|
||||
});
|
||||
currentView = this.contextManager.getWorkingBufferView();
|
||||
await orchestrator.executePipeline(pipe.name, currentView, {
|
||||
currentTokens,
|
||||
maxTokens: this.config.budget.maxTokens,
|
||||
retainedTokens: this.config.budget.retainedTokens,
|
||||
isBudgetSatisfied: false,
|
||||
deficitTokens: currentTokens - this.config.budget.maxTokens,
|
||||
protectedEpisodeIds: new Set(),
|
||||
});
|
||||
currentView = this.contextManager.getWorkingBufferView();
|
||||
}
|
||||
|
||||
|
||||
// Inject the truncated view back into the graph
|
||||
for (let i = 0; i < currentView.length; i++) {
|
||||
const ep = currentView[i];
|
||||
if (!this.contextManager.getWorkingBufferView().find(c => c.id === ep.id)) {
|
||||
this.eventBus.emitVariantReady({
|
||||
targetId: ep.id,
|
||||
variantId: 'v-emergency',
|
||||
variant: {
|
||||
status: 'ready',
|
||||
type: 'masked', // Truncation is technically a mask
|
||||
text: ep.yield?.text || '',
|
||||
recoveredTokens: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
const ep = currentView[i];
|
||||
if (
|
||||
!this.contextManager
|
||||
.getWorkingBufferView()
|
||||
.find((c) => c.id === ep.id)
|
||||
) {
|
||||
this.eventBus.emitVariantReady({
|
||||
targetId: ep.id,
|
||||
variantId: 'v-emergency',
|
||||
variant: {
|
||||
status: 'ready',
|
||||
type: 'masked', // Truncation is technically a mask
|
||||
text: ep.yield?.text || '',
|
||||
recoveredTokens: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// Wait for variant propagation
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
|
||||
// 4. Measure tokens after background processors have (hopefully) emitted variants
|
||||
const tokensAfter = this.env.tokenCalculator.calculateEpisodeListTokens(
|
||||
this.contextManager.getWorkingBufferView()
|
||||
this.contextManager.getWorkingBufferView(),
|
||||
);
|
||||
debugLogger.log(`[Turn ${this.currentTurnIndex}] Tokens AFTER: ${tokensAfter}`);
|
||||
|
||||
debugLogger.log(
|
||||
`[Turn ${this.currentTurnIndex}] Tokens AFTER: ${tokensAfter}`,
|
||||
);
|
||||
|
||||
this.tokenTrajectory.push({
|
||||
turnIndex: this.currentTurnIndex++,
|
||||
tokensBeforeBackground: tokensBefore,
|
||||
@@ -155,10 +179,11 @@ export class SimulationHarness {
|
||||
}
|
||||
|
||||
async getGoldenState() {
|
||||
const finalProjection = await this.contextManager.projectCompressedHistory();
|
||||
const finalProjection =
|
||||
await this.contextManager.projectCompressedHistory();
|
||||
return {
|
||||
tokenTrajectory: this.tokenTrajectory,
|
||||
finalProjection
|
||||
finalProjection,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,14 @@ import type { BaseLlmClient } from '../../core/baseLlmClient.js';
|
||||
expect.addSnapshotSerializer({
|
||||
test: (val) =>
|
||||
typeof val === 'string' &&
|
||||
(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val) ||
|
||||
/^\/tmp\/sim/.test(val)), // Mask temp directories and UUIDs
|
||||
print: (val) => (typeof val === 'string' && /^\/tmp\/sim/.test(val) ? '"<MOCKED_DIR>"' : '"<UUID>"'),
|
||||
(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||
val,
|
||||
) ||
|
||||
/^\/tmp\/sim/.test(val)), // Mask temp directories and UUIDs
|
||||
print: (val) =>
|
||||
typeof val === 'string' && /^\/tmp\/sim/.test(val)
|
||||
? '"<MOCKED_DIR>"'
|
||||
: '"<UUID>"',
|
||||
});
|
||||
|
||||
describe('System Lifecycle Golden Tests', () => {
|
||||
@@ -36,69 +41,106 @@ describe('System Lifecycle Golden Tests', () => {
|
||||
triggers: ['budget_exceeded'],
|
||||
processors: [
|
||||
{ processorId: 'BlobDegradationProcessor' },
|
||||
{ processorId: 'ToolMaskingProcessor', options: { stringLengthThresholdTokens: 50 } }, // Mask any tool string > 200 chars
|
||||
{ processorId: 'StateSnapshotProcessor', options: {} } // Squash old history
|
||||
]
|
||||
{
|
||||
processorId: 'ToolMaskingProcessor',
|
||||
options: { stringLengthThresholdTokens: 50 },
|
||||
}, // Mask any tool string > 200 chars
|
||||
{ processorId: 'StateSnapshotProcessor', options: {} }, // Squash old history
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Immediate Sanitization', // The magic string the projector is hardcoded to use
|
||||
execution: 'blocking',
|
||||
triggers: ['budget_exceeded'],
|
||||
processors: [
|
||||
{ processorId: 'EmergencyTruncationProcessor', options: {} }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ processorId: 'EmergencyTruncationProcessor', options: {} },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const mockLlmClient = {
|
||||
generateContent: vi.fn().mockResolvedValue({
|
||||
text: '<MOCKED_STATE_SNAPSHOT_SUMMARY>',
|
||||
})
|
||||
}),
|
||||
} as unknown as BaseLlmClient;
|
||||
|
||||
it('Scenario 1: Organic Growth with Huge Tool Output & Images', async () => {
|
||||
const harness = await SimulationHarness.create(getAggressiveConfig(), mockLlmClient);
|
||||
const harness = await SimulationHarness.create(
|
||||
getAggressiveConfig(),
|
||||
mockLlmClient,
|
||||
);
|
||||
|
||||
// Turn 0: System Prompt
|
||||
await harness.simulateTurn([
|
||||
{ role: 'user', parts: [{ text: 'System Instructions' }] },
|
||||
{ role: 'model', parts: [{ text: 'Ack.' }] }
|
||||
{ role: 'model', parts: [{ text: 'Ack.' }] },
|
||||
]);
|
||||
|
||||
// Turn 1: Normal conversation
|
||||
await harness.simulateTurn([
|
||||
{ role: 'user', parts: [{ text: 'Hello!' }] },
|
||||
{ role: 'model', parts: [{ text: 'Hi, how can I help?' }] }
|
||||
{ role: 'model', parts: [{ text: 'Hi, how can I help?' }] },
|
||||
]);
|
||||
|
||||
// Turn 2: Massive Tool Output (Should trigger ToolMaskingProcessor in background)
|
||||
await harness.simulateTurn([
|
||||
{ role: 'user', parts: [{ text: 'Read the logs.' }] },
|
||||
{ role: 'model', parts: [{ functionCall: { name: 'run_shell_command', args: { cmd: 'cat server.log' } } }] },
|
||||
{ role: 'user', parts: [{ functionResponse: { name: 'run_shell_command', response: { output: 'LOG '.repeat(5000) } } }] },
|
||||
{ role: 'model', parts: [{ text: 'The logs are very long.' }] }
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
functionCall: {
|
||||
name: 'run_shell_command',
|
||||
args: { cmd: 'cat server.log' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: 'run_shell_command',
|
||||
response: { output: 'LOG '.repeat(5000) },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: 'model', parts: [{ text: 'The logs are very long.' }] },
|
||||
]);
|
||||
|
||||
// Turn 3: Multi-modal blob (Should trigger BlobDegradationProcessor)
|
||||
await harness.simulateTurn([
|
||||
{ role: 'user', parts: [{ text: 'Look at this architecture diagram:' }, { inlineData: { mimeType: 'image/png', data: 'fake_base64_data_'.repeat(1000) } }] },
|
||||
{ role: 'model', parts: [{ text: 'Nice diagram.' }] }
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{ text: 'Look at this architecture diagram:' },
|
||||
{
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: 'fake_base64_data_'.repeat(1000),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: 'model', parts: [{ text: 'Nice diagram.' }] },
|
||||
]);
|
||||
|
||||
// Turn 4: More conversation to trigger StateSnapshot
|
||||
await harness.simulateTurn([
|
||||
{ role: 'user', parts: [{ text: 'Can we refactor?' }] },
|
||||
{ role: 'model', parts: [{ text: 'Yes we can.' }] }
|
||||
{ role: 'model', parts: [{ text: 'Yes we can.' }] },
|
||||
]);
|
||||
|
||||
// Get final state
|
||||
const goldenState = await harness.getGoldenState();
|
||||
|
||||
// In a perfectly functioning opportunistic system, the token trajectory should show
|
||||
|
||||
// In a perfectly functioning opportunistic system, the token trajectory should show
|
||||
// the massive spikes in Turn 2 and 3 being immediately resolved by the background tasks.
|
||||
// The final projection should fit neatly under the Max Tokens limit.
|
||||
|
||||
|
||||
expect(goldenState).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user