mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-14 07:10:34 -07:00
test: integration tests for /compress command in interactive mode (#10154)
Co-authored-by: Taneja Hriday <hridayt@google.com>
This commit is contained in:
116
integration-tests/context-compress-interactive.test.ts
Normal file
116
integration-tests/context-compress-interactive.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { expect, describe, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, type } from './test-helper.js';
|
||||
|
||||
describe('Interactive Mode', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'should trigger chat compression with /compress command',
|
||||
async () => {
|
||||
await rig.setup('interactive-compress-test');
|
||||
|
||||
const { ptyProcess } = rig.runInteractive();
|
||||
|
||||
let fullOutput = '';
|
||||
ptyProcess.onData((data) => (fullOutput += data));
|
||||
|
||||
const authDialogAppeared = await rig.waitForText(
|
||||
'How would you like to authenticate',
|
||||
5000,
|
||||
);
|
||||
|
||||
// select the second option if auth dialog come's up
|
||||
if (authDialogAppeared) {
|
||||
ptyProcess.write('2');
|
||||
}
|
||||
|
||||
// Wait for the app to be ready
|
||||
const isReady = await rig.waitForText('Type your message', 15000);
|
||||
expect(
|
||||
isReady,
|
||||
'CLI did not start up in interactive mode correctly',
|
||||
).toBe(true);
|
||||
|
||||
const longPrompt =
|
||||
'Dont do anything except returning a 1000 token long paragragh with the <name of the scientist who discovered theory of relativity> at the end to indicate end of response. This is a moderately long sentence.';
|
||||
|
||||
await type(ptyProcess, longPrompt);
|
||||
await type(ptyProcess, '\r');
|
||||
|
||||
await rig.waitForText('einstein', 25000);
|
||||
|
||||
await type(ptyProcess, '/compress');
|
||||
// A small delay to allow React to re-render the command list.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await type(ptyProcess, '\r');
|
||||
|
||||
const foundEvent = await rig.waitForTelemetryEvent(
|
||||
'chat_compression',
|
||||
90000,
|
||||
);
|
||||
expect(foundEvent, 'chat_compression telemetry event was not found').toBe(
|
||||
true,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'should handle compression failure on token inflation',
|
||||
async () => {
|
||||
await rig.setup('interactive-compress-test');
|
||||
|
||||
const { ptyProcess } = rig.runInteractive();
|
||||
|
||||
let fullOutput = '';
|
||||
ptyProcess.onData((data) => (fullOutput += data));
|
||||
|
||||
const authDialogAppeared = await rig.waitForText(
|
||||
'How would you like to authenticate',
|
||||
5000,
|
||||
);
|
||||
|
||||
// select the second option if auth dialog come's up
|
||||
if (authDialogAppeared) {
|
||||
ptyProcess.write('2');
|
||||
}
|
||||
|
||||
// Wait for the app to be ready
|
||||
const isReady = await rig.waitForText('Type your message', 25000);
|
||||
expect(
|
||||
isReady,
|
||||
'CLI did not start up in interactive mode correctly',
|
||||
).toBe(true);
|
||||
|
||||
await type(ptyProcess, '/compress');
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await type(ptyProcess, '\r');
|
||||
|
||||
const foundEvent = await rig.waitForTelemetryEvent(
|
||||
'chat_compression',
|
||||
90000,
|
||||
);
|
||||
expect(foundEvent).toBe(true);
|
||||
|
||||
const compressionFailed = await rig.waitForText(
|
||||
'compression was not beneficial',
|
||||
25000,
|
||||
);
|
||||
|
||||
expect(compressionFailed).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { env } from 'node:process';
|
||||
import { DEFAULT_GEMINI_MODEL } from '../packages/core/src/config/models.js';
|
||||
import fs from 'node:fs';
|
||||
import * as pty from '@lydell/node-pty';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -112,6 +113,15 @@ export function validateModelOutput(
|
||||
return true;
|
||||
}
|
||||
|
||||
// Simulates typing a string one character at a time to avoid paste detection.
|
||||
export async function type(ptyProcess: pty.IPty, text: string) {
|
||||
const delay = 5;
|
||||
for (const char of text) {
|
||||
ptyProcess.write(char);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
interface ParsedLog {
|
||||
attributes?: {
|
||||
'event.name'?: string;
|
||||
@@ -134,6 +144,7 @@ export class TestRig {
|
||||
testDir: string | null;
|
||||
testName?: string;
|
||||
_lastRunStdout?: string;
|
||||
_interactiveOutput = '';
|
||||
|
||||
constructor() {
|
||||
this.bundlePath = join(__dirname, '..', 'bundle/gemini.js');
|
||||
@@ -782,6 +793,20 @@ export class TestRig {
|
||||
return null;
|
||||
}
|
||||
|
||||
async waitForText(text: string, timeout?: number): Promise<boolean> {
|
||||
if (!timeout) {
|
||||
timeout = this.getDefaultTimeout();
|
||||
}
|
||||
return this.poll(
|
||||
() =>
|
||||
stripAnsi(this._interactiveOutput)
|
||||
.toLowerCase()
|
||||
.includes(text.toLowerCase()),
|
||||
timeout,
|
||||
200,
|
||||
);
|
||||
}
|
||||
|
||||
runInteractive(...args: string[]): {
|
||||
ptyProcess: pty.IPty;
|
||||
promise: Promise<{ exitCode: number; signal?: number; output: string }>;
|
||||
@@ -789,6 +814,8 @@ export class TestRig {
|
||||
const { command, initialArgs } = this._getCommandAndArgs(['--yolo']);
|
||||
const commandArgs = [...initialArgs, ...args];
|
||||
|
||||
this._interactiveOutput = ''; // Reset output for the new run
|
||||
|
||||
const ptyProcess = pty.spawn(command, commandArgs, {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
@@ -797,9 +824,8 @@ export class TestRig {
|
||||
env: process.env as { [key: string]: string },
|
||||
});
|
||||
|
||||
let output = '';
|
||||
ptyProcess.onData((data) => {
|
||||
output += data;
|
||||
this._interactiveOutput += data;
|
||||
if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') {
|
||||
process.stdout.write(data);
|
||||
}
|
||||
@@ -811,7 +837,7 @@ export class TestRig {
|
||||
output: string;
|
||||
}>((resolve) => {
|
||||
ptyProcess.onExit(({ exitCode, signal }) => {
|
||||
resolve({ exitCode, signal, output });
|
||||
resolve({ exitCode, signal, output: this._interactiveOutput });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user