test: integration tests for /compress command in interactive mode (#10154)

Co-authored-by: Taneja Hriday <hridayt@google.com>
This commit is contained in:
hritan 2025-09-30 19:31:51 +00:00 committed by GitHub
parent d991c4607d
commit 178e89a914
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 150 additions and 6 deletions

View 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);
},
);
});

View file

@ -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 });
});
});

7
package-lock.json generated
View file

@ -47,6 +47,7 @@
"prettier": "^3.5.3",
"react-devtools-core": "^4.28.5",
"semver": "^7.7.2",
"strip-ansi": "^7.1.2",
"tsx": "^4.20.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.2.4",
@ -14736,9 +14737,9 @@
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"

View file

@ -99,6 +99,7 @@
"prettier": "^3.5.3",
"react-devtools-core": "^4.28.5",
"semver": "^7.7.2",
"strip-ansi": "^7.1.2",
"tsx": "^4.20.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.2.4",