mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-14 05:17:18 -07:00
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
|
|
import { IrMapper } from './ir/mapper.js';
|
||
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright 2026 Google LLC
|
||
|
|
* SPDX-License-Identifier: Apache-2.0
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import {
|
||
|
|
createMockContextConfig,
|
||
|
|
setupContextComponentTest,
|
||
|
|
} from './testing/contextTestUtils.js';
|
||
|
|
|
||
|
|
describe('ContextManager Barrier Tests', () => {
|
||
|
|
it('Soft Barrier (retainedTokens): should inject ready variants and shrink projection', async () => {
|
||
|
|
const config = createMockContextConfig();
|
||
|
|
const { chatHistory, contextManager } = setupContextComponentTest(config);
|
||
|
|
|
||
|
|
// 1. Shrink limits: 1 char = 1 token. RetainedTokens = 10. MaxTokens = 100.
|
||
|
|
IrMapper.setConfig({ charsPerToken: 1 });
|
||
|
|
|
||
|
|
contextManager['sidecar'].budget.retainedTokens = 5;
|
||
|
|
contextManager['sidecar'].budget.maxTokens = 100;
|
||
|
|
|
||
|
|
// 2. Build tiny history: 5 turns (10 messages). 2 tokens per turn.
|
||
|
|
const tinyHistory = [];
|
||
|
|
for (let i = 0; i < 5; i++) {
|
||
|
|
tinyHistory.push({ role: 'user', parts: [{ text: `U${i}` }] });
|
||
|
|
tinyHistory.push({ role: 'model', parts: [{ text: `M${i}` }] });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set history directly to avoid event races
|
||
|
|
await chatHistory.set(tinyHistory);
|
||
|
|
|
||
|
|
// 3. Pre-verify baseline length.
|
||
|
|
const baseline = await contextManager.projectCompressedHistory();
|
||
|
|
expect(baseline.length).toBe(10);
|
||
|
|
|
||
|
|
// 4. Emit a fake snapshot covering the first 3 pairs (6 messages)
|
||
|
|
const targetEp = contextManager['pristineEpisodes'][2];
|
||
|
|
const replacedIds = contextManager['pristineEpisodes']
|
||
|
|
.slice(0, 3)
|
||
|
|
.map((ep) => ep.id);
|
||
|
|
|
||
|
|
contextManager['eventBus'].emitVariantReady({
|
||
|
|
targetId: targetEp.id,
|
||
|
|
variantId: 'snapshot',
|
||
|
|
variant: {
|
||
|
|
status: 'ready',
|
||
|
|
type: 'snapshot',
|
||
|
|
replacedEpisodeIds: replacedIds,
|
||
|
|
episode: {
|
||
|
|
id: 'snapshot-ep',
|
||
|
|
timestamp: Date.now(),
|
||
|
|
trigger: {
|
||
|
|
id: 't1',
|
||
|
|
type: 'USER_PROMPT',
|
||
|
|
semanticParts: [],
|
||
|
|
metadata: {
|
||
|
|
originalTokens: 0,
|
||
|
|
currentTokens: 0,
|
||
|
|
transformations: [],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
yield: {
|
||
|
|
id: 'y1',
|
||
|
|
type: 'AGENT_YIELD',
|
||
|
|
text: '<SNAP>',
|
||
|
|
metadata: {
|
||
|
|
originalTokens: 5,
|
||
|
|
currentTokens: 5,
|
||
|
|
transformations: [],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
steps: [],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// 5. Verify Projection shrinks: 6 original messages replaced by 1 snapshot episode (1 text part) -> length 5.
|
||
|
|
const projection = await contextManager.projectCompressedHistory();
|
||
|
|
expect(projection.length).toBe(5);
|
||
|
|
// console.dir(projection, {depth: null});
|
||
|
|
// projection[0] should be the snapshot yield
|
||
|
|
expect(projection[0].parts![0].text).toBe('<SNAP>');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('Hard Barrier (maxTokens): should ruthlessly truncate unprotected episodes', async () => {
|
||
|
|
const config = createMockContextConfig();
|
||
|
|
const { chatHistory, contextManager } = setupContextComponentTest(config);
|
||
|
|
|
||
|
|
// 1. Shrink limits: maxTokens = 15.
|
||
|
|
IrMapper.setConfig({ charsPerToken: 1 });
|
||
|
|
contextManager['sidecar'].budget.maxTokens = 15;
|
||
|
|
|
||
|
|
// 2. Build history: 2 turns. Total = 24 tokens.
|
||
|
|
const history = [
|
||
|
|
{ role: 'user', parts: [{ text: 'U0' }] },
|
||
|
|
{ role: 'model', parts: [{ text: 'M0_LARGE!!' }] },
|
||
|
|
{ role: 'user', parts: [{ text: 'U1' }] },
|
||
|
|
{ role: 'model', parts: [{ text: 'M1_LARGE!!' }] },
|
||
|
|
];
|
||
|
|
await chatHistory.set(history);
|
||
|
|
|
||
|
|
const projection = await contextManager.projectCompressedHistory();
|
||
|
|
|
||
|
|
// Because Turn 0 is architecturally protected (system prompt/initialization), it SURVIVES!
|
||
|
|
// Turn 1 is dropped to satisfy the maxTokens constraint.
|
||
|
|
expect(projection.length).toBe(2);
|
||
|
|
expect(projection[0].parts![0].text).toBe('U0');
|
||
|
|
expect(projection[1].parts![0].text).toBe('M0_LARGE!!');
|
||
|
|
});
|
||
|
|
});
|