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