mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-15 06:12:50 -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:
@@ -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 { DEFAULT_GEMINI_MODEL } from '../packages/core/src/config/models.js';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import * as pty from '@lydell/node-pty';
|
import * as pty from '@lydell/node-pty';
|
||||||
|
import stripAnsi from 'strip-ansi';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -112,6 +113,15 @@ export function validateModelOutput(
|
|||||||
return true;
|
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 {
|
interface ParsedLog {
|
||||||
attributes?: {
|
attributes?: {
|
||||||
'event.name'?: string;
|
'event.name'?: string;
|
||||||
@@ -134,6 +144,7 @@ export class TestRig {
|
|||||||
testDir: string | null;
|
testDir: string | null;
|
||||||
testName?: string;
|
testName?: string;
|
||||||
_lastRunStdout?: string;
|
_lastRunStdout?: string;
|
||||||
|
_interactiveOutput = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.bundlePath = join(__dirname, '..', 'bundle/gemini.js');
|
this.bundlePath = join(__dirname, '..', 'bundle/gemini.js');
|
||||||
@@ -782,6 +793,20 @@ export class TestRig {
|
|||||||
return null;
|
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[]): {
|
runInteractive(...args: string[]): {
|
||||||
ptyProcess: pty.IPty;
|
ptyProcess: pty.IPty;
|
||||||
promise: Promise<{ exitCode: number; signal?: number; output: string }>;
|
promise: Promise<{ exitCode: number; signal?: number; output: string }>;
|
||||||
@@ -789,6 +814,8 @@ export class TestRig {
|
|||||||
const { command, initialArgs } = this._getCommandAndArgs(['--yolo']);
|
const { command, initialArgs } = this._getCommandAndArgs(['--yolo']);
|
||||||
const commandArgs = [...initialArgs, ...args];
|
const commandArgs = [...initialArgs, ...args];
|
||||||
|
|
||||||
|
this._interactiveOutput = ''; // Reset output for the new run
|
||||||
|
|
||||||
const ptyProcess = pty.spawn(command, commandArgs, {
|
const ptyProcess = pty.spawn(command, commandArgs, {
|
||||||
name: 'xterm-color',
|
name: 'xterm-color',
|
||||||
cols: 80,
|
cols: 80,
|
||||||
@@ -797,9 +824,8 @@ export class TestRig {
|
|||||||
env: process.env as { [key: string]: string },
|
env: process.env as { [key: string]: string },
|
||||||
});
|
});
|
||||||
|
|
||||||
let output = '';
|
|
||||||
ptyProcess.onData((data) => {
|
ptyProcess.onData((data) => {
|
||||||
output += data;
|
this._interactiveOutput += data;
|
||||||
if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') {
|
if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') {
|
||||||
process.stdout.write(data);
|
process.stdout.write(data);
|
||||||
}
|
}
|
||||||
@@ -811,7 +837,7 @@ export class TestRig {
|
|||||||
output: string;
|
output: string;
|
||||||
}>((resolve) => {
|
}>((resolve) => {
|
||||||
ptyProcess.onExit(({ exitCode, signal }) => {
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
||||||
resolve({ exitCode, signal, output });
|
resolve({ exitCode, signal, output: this._interactiveOutput });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
+4
-3
@@ -47,6 +47,7 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"react-devtools-core": "^4.28.5",
|
"react-devtools-core": "^4.28.5",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
|
"strip-ansi": "^7.1.2",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
@@ -14736,9 +14737,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^6.0.1"
|
"ansi-regex": "^6.0.1"
|
||||||
|
|||||||
@@ -99,6 +99,7 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"react-devtools-core": "^4.28.5",
|
"react-devtools-core": "^4.28.5",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
|
"strip-ansi": "^7.1.2",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
|
|||||||
Reference in New Issue
Block a user