2026-04-13 15:02:22 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2026 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
2026-05-14 03:04:19 +00:00
|
|
|
import { randomUUID } from 'node:crypto';
|
2026-04-13 15:02:22 -07:00
|
|
|
import { testTruncateProfile } from './testing/testProfile.js';
|
|
|
|
|
import {
|
|
|
|
|
createSyntheticHistory,
|
|
|
|
|
createMockContextConfig,
|
|
|
|
|
setupContextComponentTest,
|
|
|
|
|
} from './testing/contextTestUtils.js';
|
|
|
|
|
|
|
|
|
|
describe('ContextManager Sync Pressure Barrier Tests', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.useFakeTimers();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.useRealTimers();
|
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should instantly truncate history when maxTokens is exceeded using truncate strategy', async () => {
|
|
|
|
|
// 1. Setup
|
|
|
|
|
const config = createMockContextConfig();
|
|
|
|
|
const { chatHistory, contextManager } = setupContextComponentTest(
|
|
|
|
|
config,
|
|
|
|
|
testTruncateProfile,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 2. Add System Prompt (Episode 0 - Protected)
|
|
|
|
|
chatHistory.set([
|
2026-05-14 03:04:19 +00:00
|
|
|
{ id: 'h1', content: { role: 'user', parts: [{ text: 'System prompt' }] } },
|
|
|
|
|
{ id: 'h2', content: { role: 'model', parts: [{ text: 'Understood.' }] } },
|
2026-04-13 15:02:22 -07:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 3. Add massive history that blows past the 150k maxTokens limit
|
2026-05-12 08:51:20 -07:00
|
|
|
// 20 turns * ~20,000 tokens/turn (10k user + 10k model) = ~400,000 tokens
|
2026-05-14 03:04:19 +00:00
|
|
|
const massiveHistory = createSyntheticHistory(20, 10000).map((c) => ({
|
|
|
|
|
id: randomUUID(),
|
|
|
|
|
content: c,
|
|
|
|
|
}));
|
2026-04-13 15:02:22 -07:00
|
|
|
chatHistory.set([...chatHistory.get(), ...massiveHistory]);
|
|
|
|
|
|
|
|
|
|
// 4. Add the Latest Turn (Protected)
|
|
|
|
|
chatHistory.set([
|
|
|
|
|
...chatHistory.get(),
|
2026-05-14 03:04:19 +00:00
|
|
|
{
|
|
|
|
|
id: 'h-last-user',
|
|
|
|
|
content: { role: 'user', parts: [{ text: 'Final question.' }] },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'h-last-model',
|
|
|
|
|
content: { role: 'model', parts: [{ text: 'Final answer.' }] },
|
|
|
|
|
},
|
2026-04-13 15:02:22 -07:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const rawHistoryLength = chatHistory.get().length;
|
|
|
|
|
|
|
|
|
|
// 5. Project History (Triggers Sync Barrier)
|
2026-05-01 15:04:39 -07:00
|
|
|
const { history: projection } = await contextManager.renderHistory();
|
2026-04-13 15:02:22 -07:00
|
|
|
|
|
|
|
|
// 6. Assertions
|
|
|
|
|
// The barrier should have dropped several older episodes to get under 150k.
|
|
|
|
|
|
|
|
|
|
expect(projection.length).toBeLessThan(rawHistoryLength);
|
|
|
|
|
|
2026-05-01 15:04:39 -07:00
|
|
|
// Verify Episode 0 (System) was pruned, so we now start with a sentinel due to role alternation
|
2026-05-14 03:04:19 +00:00
|
|
|
expect(projection[0].content.role).toBe('user');
|
2026-05-12 08:51:20 -07:00
|
|
|
const projectionString = JSON.stringify(projection);
|
|
|
|
|
expect(projectionString).toContain('User turn 17');
|
2026-04-13 15:02:22 -07:00
|
|
|
// Filter out synthetic Yield nodes (they are model responses without actual tool/text bodies)
|
|
|
|
|
const contentNodes = projection.filter(
|
|
|
|
|
(p) =>
|
2026-05-14 03:04:19 +00:00
|
|
|
p.content.parts &&
|
|
|
|
|
p.content.parts.some((part) => part.text && part.text !== 'Yield'),
|
2026-04-13 15:02:22 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Verify the latest turn is perfectly preserved at the back
|
2026-05-01 15:04:39 -07:00
|
|
|
// Note: The HistoryHardener appends a "Please continue." user turn if we end on model,
|
|
|
|
|
// so we look at the turns before the sentinel.
|
2026-05-14 03:04:19 +00:00
|
|
|
const lastSentinel = contentNodes[contentNodes.length - 1].content;
|
|
|
|
|
const lastModel = contentNodes[contentNodes.length - 2].content;
|
|
|
|
|
const lastUser = contentNodes[contentNodes.length - 3].content;
|
2026-05-01 15:04:39 -07:00
|
|
|
|
|
|
|
|
expect(lastSentinel.role).toBe('user');
|
|
|
|
|
expect(lastSentinel.parts![0].text).toBe('Please continue.');
|
2026-04-13 15:02:22 -07:00
|
|
|
|
|
|
|
|
expect(lastUser.role).toBe('user');
|
|
|
|
|
expect(lastUser.parts![0].text).toBe('Final question.');
|
|
|
|
|
|
|
|
|
|
expect(lastModel.role).toBe('model');
|
|
|
|
|
expect(lastModel.parts![0].text).toBe('Final answer.');
|
|
|
|
|
});
|
|
|
|
|
});
|