Compare commits

...

8 Commits

Author SHA1 Message Date
gemini-cli-robot b05fb545ef chore(release): v0.20.0 2025-12-09 20:18:11 +00:00
gemini-cli-robot d0ce3c4c5d chore(release): v0.20.0-preview.5 2025-12-09 01:52:41 +00:00
gemini-cli-robot 8872ee0ace fix(patch): cherry-pick 171103a to release/v0.20.0-preview.2-pr-14742 to patch version v0.20.0-preview.2 and create version 0.20.0-preview.5 (#14752)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
Co-authored-by: christine betts <chrstn@uw.edu>
2025-12-09 01:01:46 +00:00
gemini-cli-robot 9d7b9e6cd1 chore(release): v0.20.0-preview.2 2025-12-08 20:39:26 +00:00
gemini-cli-robot f9997f92c9 fix(patch): cherry-pick 828afe1 to release/v0.20.0-preview.1-pr-14159 to patch version v0.20.0-preview.1 and create version 0.20.0-preview.2 (#14733)
Co-authored-by: Allen Hutchison <adh@google.com>
2025-12-08 19:54:40 +00:00
gemini-cli-robot aae64683ce chore(release): v0.20.0-preview.1 2025-12-05 04:50:06 +00:00
gemini-cli-robot 356eb7ced0 fix(patch): cherry-pick d284fa6 to release/v0.20.0-preview.0-pr-14545 [CONFLICTS] (#14559)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2025-12-04 20:33:32 -08:00
gemini-cli-robot 2908555435 chore(release): v0.20.0-preview.0 2025-12-02 23:05:16 +00:00
23 changed files with 510 additions and 62 deletions
+54
View File
@@ -0,0 +1,54 @@
description="Injects context of all relevant cli files"
prompt = """
The source code contains the content of absolutely every source code file in
packages/cli. In addition to the source code, the following test files are
included as they are test files that conform to the project's testing standards:
`packages/cli/src/ui/components/InputPrompt.test.tsx` and `packages/cli/src/ui/App.test.tsx`.
You should very rarely need to read any other files from packages/cli to resolve
prompts.
!{find packages/cli -path packages/cli/dist -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\; && echo "--- packages/cli/src/ui/components/InputPrompt.test.tsx ---" && cat packages/cli/src/ui/components/InputPrompt.test.tsx && echo "--- packages/cli/src/ui/App.test.tsx ---" && cat packages/cli/src/ui/App.test.tsx}
**Pay extremely close attention to these files.** They define the project's
core architecture, component patterns, and testing standards.
In addition to the code context, you MUST strictly adhere to the following frontend-specific development guidelines while adding code to packages/cli.
## Testing Standards
* **Async Testing**: ALWAYS use `waitFor` from `packages/cli/src/test-utils/async.ts` instead of `vi.waitFor` to prevent flakiness and `act` warnings. NEVER use fixed waits (e.g., `await delay(100)`).
* **State Changes**: Wrap all blocks that change component state in `act`.
* **Snapshots**: Use `toMatchSnapshot` to verify rendering.
* **Rendering**: Use `render` or `renderWithProviders` from `packages/cli/src/test-utils/render.tsx` instead of `ink-testing-library` directly.
* **Mocking**:
* Reuse existing mocks/fakes where possible.
* **Parameterized Tests**: Use parameterized tests with explicit types to reduce duplication and ensure type safety.
* Avoid mocking the file system, os, or child_process if at all possible; if you have to mock the Mock critical dependencies (`fs`, `os`, `child_process`) do so ONLY at the top of the file.
## React & Ink Architecture
* **Keyboard Handling**: You MUST use `useKeyPress.ts` from Gemini CLI for keyboard handling, NOT the standard ink library. This is critical for supporting slow terminals and multiple events per frame.
* Handle multiple events gracefully (often requires a reducer pattern, see `text-buffer.ts`).
* **State Management**:
* NEVER trigger side effects from within the body of a `setState` callback. Use a reducer or `useRef` if necessary.
* Initialize state explicitly (e.g., use `undefined` rather than `true` if unknown).
* **Performance**:
* Avoid synchronous file I/O in components.
* Do not introduce excessive property drilling; leverage or extend existing providers.
## Configuration & Settings
* **Settings vs Args**: Use settings for user-configurable options; do not add new CLI arguments.
* **Schema**: Add new settings to `packages/cli/src/config/settingsSchema.ts`.
* **Documentation**:
* If `showInDialog: true`, document in `docs/get-started/configuration.md`.
* Ensure `requiresRestart` is correctly set.
## Keyboard Shortcuts
* **Registration**: Define new shortcuts in `packages/cli/src/config/keyBindings.ts`.
* **Documentation**: Document all new shortcuts in `docs/cli/keyboard-shortcuts.md`.
* **Compatibility**: Avoid function keys and common VSCode shortcuts. Be cautious with `Meta` key (Mac-limited support).
## General
* **Logging**: Use `debugLogger` for errors. NEVER leave `console.log/warn/error` in the code.
* **TypeScript**: Avoid the non-null assertion operator (`!`).
{{args}}.
"""
+57
View File
@@ -0,0 +1,57 @@
description="Injects context of all relevant cli files"
prompt = """
The following output contains the complete
source code of the gemini cli library (`packages/cli`), and {packages/core} and representative test files
(`packages/cli/src/ui/components/InputPrompt.test.tsx` and `packages/cli/src/ui/App.test.tsx`) that best conform to the project's testing standards.
**Pay extremely close attention to these files.** They define the project's
core architecture, component patterns, and testing standards.
The source code contains the content of absolutely every source code file in
packages/cli and packages/core. In addition to the source code, the following test files are
included as they are test files that conform to the project's testing standards:
`packages/cli/src/ui/components/InputPrompt.test.tsx` and `packages/cli/src/ui/App.test.tsx`.
You should very rarely need to read any other files from packages/cli to resolve
prompts.
!{find packages/cli packages/core \\( -path packages/cli/dist -o -path packages/core/dist -o -name node_modules \\) -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\; && echo "--- packages/cli/src/ui/components/InputPrompt.test.tsx ---" && cat packages/cli/src/ui/components/InputPrompt.test.tsx && echo "--- packages/cli/src/ui/App.test.tsx ---" && cat packages/cli/src/ui/App.test.tsx}
In addition to the code context, you MUST strictly adhere to the following frontend-specific development guidelines when writing code in packages/cli.
## Testing Standards
* **Async Testing**: ALWAYS use `waitFor` from `packages/cli/src/test-utils/async.ts` instead of `vi.waitFor` to prevent flakiness and `act` warnings. NEVER use fixed waits (e.g., `await delay(100)`).
* **State Changes**: Wrap all blocks that change component state in `act`.
* **Snapshots**: Use `toMatchSnapshot` to verify rendering.
* **Rendering**: Use `render` or `renderWithProviders` from `packages/cli/src/test-utils/render.tsx` instead of `ink-testing-library` directly.
* **Mocking**:
* Reuse existing mocks/fakes where possible.
* **Parameterized Tests**: Use parameterized tests with explicit types to reduce duplication and ensure type safety.
* Avoid mocking the file system, os, or child_process if at all possible; if you have to mock the Mock critical dependencies (`fs`, `os`, `child_process`) do so ONLY at the top of the file.
## React & Ink Architecture
* **Keyboard Handling**: You MUST use `useKeyPress.ts` from Gemini CLI for keyboard handling, NOT the standard ink library. This is critical for supporting slow terminals and multiple events per frame.
* Handle multiple events gracefully (often requires a reducer pattern, see `text-buffer.ts`).
* **State Management**:
* NEVER trigger side effects from within the body of a `setState` callback. Use a reducer or `useRef` if necessary.
* Initialize state explicitly (e.g., use `undefined` rather than `true` if unknown).
* **Performance**:
* Avoid synchronous file I/O in components.
* Do not introduce excessive property drilling; leverage or extend existing providers.
## Configuration & Settings
* **Settings vs Args**: Use settings for user-configurable options; do not add new CLI arguments.
* **Schema**: Add new settings to `packages/cli/src/config/settingsSchema.ts`.
* **Documentation**:
* If `showInDialog: true`, document in `docs/get-started/configuration.md`.
* Ensure `requiresRestart` is correctly set.
## Keyboard Shortcuts
* **Registration**: Define new shortcuts in `packages/cli/src/config/keyBindings.ts`.
* **Documentation**: Document all new shortcuts in `docs/cli/keyboard-shortcuts.md`.
* **Compatibility**: Avoid function keys and common VSCode shortcuts. Be cautious with `Meta` key (Mac-limited support).
## General
* **Logging**: Use `debugLogger` for errors. NEVER leave `console.log/warn/error` in the code.
* **TypeScript**: Avoid the non-null assertion operator (`!`).
{{args}}.
"""
+7 -7
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"workspaces": [
"packages/*"
],
@@ -17614,7 +17614,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0",
@@ -17904,7 +17904,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
@@ -18005,7 +18005,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"dependencies": {
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
@@ -18149,7 +18149,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -18160,7 +18160,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.20.0-nightly.20251201.2fe609cb6"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.20.0"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.20.0-nightly.20251201.2fe609cb6"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.20.0"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
+1 -1
View File
@@ -50,7 +50,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
),
),
patchStdio: vi.fn(() => () => {}),
createInkStdio: vi.fn(() => ({
createWorkingStdio: vi.fn(() => ({
stdout: {
write: vi.fn((...args) =>
process.stdout.write(
+2 -2
View File
@@ -47,7 +47,7 @@ import {
recordSlowRender,
coreEvents,
CoreEvent,
createInkStdio,
createWorkingStdio,
patchStdio,
writeToStdout,
writeToStderr,
@@ -203,7 +203,7 @@ export async function startInteractiveUI(
consolePatcher.patch();
registerCleanup(consolePatcher.cleanup);
const { stdout: inkStdout, stderr: inkStderr } = createInkStdio();
const { stdout: inkStdout, stderr: inkStderr } = createWorkingStdio();
// Create wrapper component to use hooks inside render
const AppWrapper = () => {
+1 -1
View File
@@ -24,7 +24,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
...actual,
writeToStdout: vi.fn(),
patchStdio: vi.fn(() => () => {}),
createInkStdio: vi.fn(() => ({
createWorkingStdio: vi.fn(() => ({
stdout: {
write: vi.fn(),
columns: 80,
@@ -68,6 +68,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
getMetrics: vi.fn(),
},
coreEvents: mockCoreEvents,
createWorkingStdio: vi.fn(() => ({
stdout: process.stdout,
stderr: process.stderr,
})),
};
});
+3 -1
View File
@@ -28,6 +28,7 @@ import {
debugLogger,
coreEvents,
CoreEvent,
createWorkingStdio,
} from '@google/gemini-cli-core';
import type { Content, Part } from '@google/genai';
@@ -70,7 +71,8 @@ export async function runNonInteractive({
coreEvents.emitConsoleLog(msg.type, msg.content);
},
});
const textOutput = new TextOutput();
const { stdout: workingStdout } = createWorkingStdio();
const textOutput = new TextOutput(workingStdout);
const handleUserFeedback = (payload: UserFeedbackPayload) => {
const prefix = payload.severity.toUpperCase();
+1 -1
View File
@@ -65,7 +65,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
),
),
patchStdio: vi.fn(() => () => {}),
createInkStdio: vi.fn(() => ({
createWorkingStdio: vi.fn(() => ({
stdout: process.stdout,
stderr: process.stderr,
})),
+6 -1
View File
@@ -13,6 +13,11 @@ import stripAnsi from 'strip-ansi';
export class TextOutput {
private atStartOfLine = true;
private outputStream: NodeJS.WriteStream;
constructor(outputStream: NodeJS.WriteStream = process.stdout) {
this.outputStream = outputStream;
}
/**
* Writes a string to stdout.
@@ -22,7 +27,7 @@ export class TextOutput {
if (str.length === 0) {
return;
}
process.stdout.write(str);
this.outputStream.write(str);
const strippedStr = stripAnsi(str);
if (strippedStr.length > 0) {
this.atStartOfLine = strippedStr.endsWith('\n');
@@ -30,6 +30,7 @@ import {
debugLogger,
ReadManyFilesTool,
getEffectiveModel,
createWorkingStdio,
startupProfiler,
} from '@google/gemini-cli-core';
import * as acp from './acp.js';
@@ -51,15 +52,10 @@ export async function runZedIntegration(
settings: LoadedSettings,
argv: CliArgs,
) {
const stdout = Writable.toWeb(process.stdout) as WritableStream;
const { stdout: workingStdout } = createWorkingStdio();
const stdout = Writable.toWeb(workingStdout) as WritableStream;
const stdin = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;
// Stdout is used to send messages to the client, so console.log/console.info
// messages to stderr so that they don't interfere with ACP.
console.log = console.error;
console.info = console.error;
console.debug = console.error;
new acp.AgentSideConnection(
(client: acp.Client) => new GeminiAgent(config, settings, argv, client),
stdout,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"description": "Gemini CLI Core",
"repository": {
"type": "git",
+1 -1
View File
@@ -48,7 +48,7 @@ vi.mock('../utils/browser.js', () => ({
vi.mock('../utils/stdio.js', () => ({
writeToStdout: vi.fn(),
writeToStderr: vi.fn(),
createInkStdio: vi.fn(() => ({
createWorkingStdio: vi.fn(() => ({
stdout: process.stdout,
stderr: process.stderr,
})),
+2 -2
View File
@@ -33,7 +33,7 @@ import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
import { debugLogger } from '../utils/debugLogger.js';
import {
writeToStdout,
createInkStdio,
createWorkingStdio,
writeToStderr,
} from '../utils/stdio.js';
import {
@@ -334,7 +334,7 @@ async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
const code = await new Promise<string>((resolve, _) => {
const rl = readline.createInterface({
input: process.stdin,
output: createInkStdio().stdout,
output: createWorkingStdio().stdout,
terminal: true,
});
@@ -4,13 +4,24 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import EventEmitter from 'node:events';
import type { Readable } from 'node:stream';
import { type ChildProcess } from 'node:child_process';
import type { ShellOutputEvent } from './shellExecutionService.js';
import type {
ShellOutputEvent,
ShellExecutionConfig,
} from './shellExecutionService.js';
import { ShellExecutionService } from './shellExecutionService.js';
import type { AnsiOutput } from '../utils/terminalSerializer.js';
import type { AnsiOutput, AnsiToken } from '../utils/terminalSerializer.js';
// Hoisted Mocks
const mockPtySpawn = vi.hoisted(() => vi.fn());
@@ -64,7 +75,7 @@ const mockProcessKill = vi
.spyOn(process, 'kill')
.mockImplementation(() => true);
const shellExecutionConfig = {
const shellExecutionConfig: ShellExecutionConfig = {
terminalWidth: 80,
terminalHeight: 24,
pager: 'cat',
@@ -74,21 +85,19 @@ const shellExecutionConfig = {
const createExpectedAnsiOutput = (text: string | string[]): AnsiOutput => {
const lines = Array.isArray(text) ? text : text.split('\n');
const expected: AnsiOutput = Array.from(
{ length: shellExecutionConfig.terminalHeight },
(_, i) => [
{
text: expect.stringMatching((lines[i] || '').trim()),
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
fg: '',
bg: '',
},
],
);
const len = (shellExecutionConfig.terminalHeight ?? 24) as number;
const expected: AnsiOutput = Array.from({ length: len }, (_, i) => [
{
text: expect.stringMatching((lines[i] || '').trim()),
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
fg: '',
bg: '',
} as AnsiToken,
]);
return expected;
};
@@ -242,6 +251,95 @@ describe('ShellExecutionService', () => {
);
});
it('should capture large output (10000 lines)', async () => {
const lineCount = 10000;
const lines = Array.from({ length: lineCount }, (_, i) => `line ${i}`);
const expectedOutput = lines.join('\n');
const { result } = await simulateExecution(
'large-output-command',
(pty) => {
// Send data in chunks to simulate realistic streaming
// Use \r\n to ensure the terminal moves the cursor to the start of the line
const chunkSize = 1000;
for (let i = 0; i < lineCount; i += chunkSize) {
const chunk = lines.slice(i, i + chunkSize).join('\r\n') + '\r\n';
pty.onData.mock.calls[0][0](chunk);
}
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
},
);
expect(result.exitCode).toBe(0);
// The terminal buffer output includes trailing spaces for each line (up to terminal width).
// We trim each line to match our expected simple string.
const processedOutput = result.output
.split('\n')
.map((l) => l.trimEnd())
.join('\n')
.trim();
expect(processedOutput).toBe(expectedOutput);
expect(result.output.split('\n').length).toBeGreaterThanOrEqual(
lineCount,
);
});
it('should not add extra padding but preserve explicit trailing whitespace', async () => {
const { result } = await simulateExecution('cmd', (pty) => {
// "value" should not get terminal-width padding
// "value2 " should keep its spaces
pty.onData.mock.calls[0][0]('value\r\nvalue2 ');
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
});
expect(result.output).toBe('value\nvalue2 ');
});
it('should truncate output exceeding the scrollback limit', async () => {
const scrollbackLimit = 100;
const totalLines = 150;
// Generate lines: "line 0", "line 1", ...
const lines = Array.from({ length: totalLines }, (_, i) => `line ${i}`);
const { result } = await simulateExecution(
'overflow-command',
(pty) => {
const chunk = lines.join('\r\n') + '\r\n';
pty.onData.mock.calls[0][0](chunk);
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
},
{ ...shellExecutionConfig, scrollback: scrollbackLimit },
);
expect(result.exitCode).toBe(0);
// The terminal should keep the *last* 'scrollbackLimit' lines + lines in the viewport.
// xterm.js scrollback is the number of lines *above* the viewport.
// So total lines retained = scrollback + rows.
// However, our `getFullBufferText` implementation iterates the *active* buffer.
// In headless xterm, the buffer length grows.
// Let's verify that we have fewer lines than totalLines.
const outputLines = result.output
.trim()
.split('\n')
.map((l) => l.trimEnd());
// We expect the *start* of the output to be truncated.
// The first retained line should be > "line 0".
// Specifically, if we sent 150 lines and have space for roughly 100 + viewport(24),
// we should miss the first ~26 lines.
// Check that we lost some lines from the beginning
expect(outputLines.length).toBeLessThan(totalLines);
expect(outputLines[0]).not.toBe('line 0');
// Check that we have the *last* lines
expect(outputLines[outputLines.length - 1]).toBe(
`line ${totalLines - 1}`,
);
});
it('should call onPid with the process id', async () => {
const abortController = new AbortController();
const handle = await ShellExecutionService.execute(
@@ -1136,3 +1234,170 @@ describe('ShellExecutionService execution method selection', () => {
expect(result.executionMethod).toBe('child_process');
});
});
describe('ShellExecutionService environment variables', () => {
let mockPtyProcess: EventEmitter & {
pid: number;
kill: Mock;
onData: Mock;
onExit: Mock;
write: Mock;
resize: Mock;
};
let mockChildProcess: EventEmitter & Partial<ChildProcess>;
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules(); // Reset modules to ensure process.env changes are fresh
// Mock for pty
mockPtyProcess = new EventEmitter() as EventEmitter & {
pid: number;
kill: Mock;
onData: Mock;
onExit: Mock;
write: Mock;
resize: Mock;
};
mockPtyProcess.pid = 12345;
mockPtyProcess.kill = vi.fn();
mockPtyProcess.onData = vi.fn();
mockPtyProcess.onExit = vi.fn();
mockPtyProcess.write = vi.fn();
mockPtyProcess.resize = vi.fn();
mockPtySpawn.mockReturnValue(mockPtyProcess);
mockGetPty.mockResolvedValue({
module: { spawn: mockPtySpawn },
name: 'mock-pty',
});
// Mock for child_process
mockChildProcess = new EventEmitter() as EventEmitter &
Partial<ChildProcess>;
mockChildProcess.stdout = new EventEmitter() as Readable;
mockChildProcess.stderr = new EventEmitter() as Readable;
mockChildProcess.kill = vi.fn();
Object.defineProperty(mockChildProcess, 'pid', {
value: 54321,
configurable: true,
});
mockCpSpawn.mockReturnValue(mockChildProcess);
// Default exit behavior for mocks
mockPtyProcess.onExit.mockImplementationOnce(({ exitCode, signal }) => {
// Small delay to allow async ops to complete
setTimeout(() => mockPtyProcess.emit('exit', { exitCode, signal }), 0);
});
mockChildProcess.on('exit', (code, signal) => {
// Small delay to allow async ops to complete
setTimeout(() => mockChildProcess.emit('close', code, signal), 0);
});
});
afterEach(() => {
// Clean up process.env after each test
vi.unstubAllEnvs();
});
it('should use a sanitized environment when in a GitHub run', async () => {
// Mock the environment to simulate a GitHub Actions run
vi.stubEnv('GITHUB_SHA', 'test-sha');
vi.stubEnv('MY_SENSITIVE_VAR', 'secret-value'); // This should be stripped out
vi.stubEnv('PATH', '/test/path'); // An essential var that should be kept
vi.stubEnv('GEMINI_CLI_TEST_VAR', 'test-value'); // A test var that should be kept
vi.resetModules();
const { ShellExecutionService } = await import(
'./shellExecutionService.js'
);
// Test pty path
await ShellExecutionService.execute(
'test-pty-command',
'/',
vi.fn(),
new AbortController().signal,
true,
shellExecutionConfig,
);
const ptyEnv = mockPtySpawn.mock.calls[0][2].env;
expect(ptyEnv).not.toHaveProperty('MY_SENSITIVE_VAR');
expect(ptyEnv).toHaveProperty('PATH', '/test/path');
expect(ptyEnv).toHaveProperty('GEMINI_CLI_TEST_VAR', 'test-value');
// Ensure pty process exits for next test
mockPtyProcess.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
await new Promise(process.nextTick);
// Test child_process path
mockGetPty.mockResolvedValue(null); // Force fallback
await ShellExecutionService.execute(
'test-cp-command',
'/',
vi.fn(),
new AbortController().signal,
true,
shellExecutionConfig,
);
const cpEnv = mockCpSpawn.mock.calls[0][2].env;
expect(cpEnv).not.toHaveProperty('MY_SENSITIVE_VAR');
expect(cpEnv).toHaveProperty('PATH', '/test/path');
expect(cpEnv).toHaveProperty('GEMINI_CLI_TEST_VAR', 'test-value');
// Ensure child_process exits
mockChildProcess.emit('exit', 0, null);
mockChildProcess.emit('close', 0, null);
await new Promise(process.nextTick);
});
it('should include the full process.env when not in a GitHub run', async () => {
vi.stubEnv('MY_TEST_VAR', 'test-value');
vi.stubEnv('GITHUB_SHA', '');
vi.stubEnv('SURFACE', '');
vi.resetModules();
const { ShellExecutionService } = await import(
'./shellExecutionService.js'
);
// Test pty path
await ShellExecutionService.execute(
'test-pty-command-no-github',
'/',
vi.fn(),
new AbortController().signal,
true,
shellExecutionConfig,
);
expect(mockPtySpawn).toHaveBeenCalled();
const ptyEnv = mockPtySpawn.mock.calls[0][2].env;
expect(ptyEnv).toHaveProperty('MY_TEST_VAR', 'test-value');
expect(ptyEnv).toHaveProperty('GEMINI_CLI', '1');
// Ensure pty process exits
mockPtyProcess.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
await new Promise(process.nextTick);
// Test child_process path (forcing fallback by making pty unavailable)
mockGetPty.mockResolvedValue(null);
await ShellExecutionService.execute(
'test-cp-command-no-github',
'/',
vi.fn(),
new AbortController().signal,
true, // Still tries pty, but it will fall back
shellExecutionConfig,
);
expect(mockCpSpawn).toHaveBeenCalled();
const cpEnv = mockCpSpawn.mock.calls[0][2].env;
expect(cpEnv).toHaveProperty('MY_TEST_VAR', 'test-value');
expect(cpEnv).toHaveProperty('GEMINI_CLI', '1');
// Ensure child_process exits
mockChildProcess.emit('exit', 0, null);
mockChildProcess.emit('close', 0, null);
await new Promise(process.nextTick);
});
});
@@ -24,6 +24,11 @@ const { Terminal } = pkg;
const SIGKILL_TIMEOUT_MS = 200;
const MAX_CHILD_PROCESS_BUFFER_SIZE = 16 * 1024 * 1024; // 16MB
// We want to allow shell outputs that are close to the context window in size.
// 300,000 lines is roughly equivalent to a large context window, ensuring
// we capture significant output from long-running commands.
export const SCROLLBACK_LIMIT = 300000;
const BASH_SHOPT_OPTIONS = 'promptvars nullglob extglob nocaseglob dotglob';
const BASH_SHOPT_GUARD = `shopt -u ${BASH_SHOPT_OPTIONS};`;
@@ -77,6 +82,7 @@ export interface ShellExecutionConfig {
defaultBg?: string;
// Used for testing
disableDynamicLineTrimming?: boolean;
scrollback?: number;
}
/**
@@ -110,12 +116,70 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
const lines: string[] = [];
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
const lineContent = line ? line.translateToString() : '';
const lineContent = line ? line.translateToString(true) : '';
lines.push(lineContent);
}
return lines.join('\n').trimEnd();
// Remove trailing empty lines
while (lines.length > 0 && lines[lines.length - 1] === '') {
lines.pop();
}
return lines.join('\n');
};
function getSanitizedEnv(): NodeJS.ProcessEnv {
const isRunningInGithub =
process.env['GITHUB_SHA'] || process.env['SURFACE'] === 'Github';
if (!isRunningInGithub) {
// For local runs, we want to preserve the user's full environment.
return { ...process.env };
}
// For CI runs (GitHub), we sanitize the environment for security.
const env: NodeJS.ProcessEnv = {};
const essentialVars = [
// Cross-platform
'PATH',
// Windows specific
'Path',
'SYSTEMROOT',
'SystemRoot',
'COMSPEC',
'ComSpec',
'PATHEXT',
'WINDIR',
'TEMP',
'TMP',
'USERPROFILE',
'SYSTEMDRIVE',
'SystemDrive',
// Unix/Linux/macOS specific
'HOME',
'LANG',
'SHELL',
'TMPDIR',
'USER',
'LOGNAME',
];
for (const key of essentialVars) {
if (process.env[key] !== undefined) {
env[key] = process.env[key];
}
}
// Always carry over test-specific variables for our own integration tests.
for (const key in process.env) {
if (key.startsWith('GEMINI_CLI_TEST')) {
env[key] = process.env[key];
}
}
return env;
}
/**
* A centralized service for executing shell commands with robust process
* management, cross-platform compatibility, and streaming output capabilities.
@@ -217,7 +281,7 @@ export class ShellExecutionService {
shell: false,
detached: !isWindows,
env: {
...process.env,
...getSanitizedEnv(),
GEMINI_CLI: '1',
TERM: 'xterm-256color',
PAGER: 'cat',
@@ -431,7 +495,7 @@ export class ShellExecutionService {
cols,
rows,
env: {
...process.env,
...getSanitizedEnv(),
GEMINI_CLI: '1',
TERM: 'xterm-256color',
PAGER: shellExecutionConfig.pager ?? 'cat',
@@ -445,6 +509,7 @@ export class ShellExecutionService {
allowProposedApi: true,
cols,
rows,
scrollback: shellExecutionConfig.scrollback ?? SCROLLBACK_LIMIT,
});
headlessTerminal.scrollToTop();
+5 -5
View File
@@ -5,7 +5,7 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { patchStdio, createInkStdio } from './stdio.js';
import { patchStdio, createWorkingStdio } from './stdio.js';
import { coreEvents } from './events.js';
vi.mock('./events.js', () => ({
@@ -53,14 +53,14 @@ describe('stdio utils', () => {
expect(process.stderr.write).toBe(originalStderrWrite);
});
it('createInkStdio writes to real stdout/stderr bypassing patch', () => {
it('createWorkingStdio writes to real stdout/stderr bypassing patch', () => {
const cleanup = patchStdio();
const { stdout: inkStdout, stderr: inkStderr } = createInkStdio();
const { stdout, stderr } = createWorkingStdio();
inkStdout.write('ink stdout');
stdout.write('working stdout');
expect(coreEvents.emitOutput).not.toHaveBeenCalled();
inkStderr.write('ink stderr');
stderr.write('working stderr');
expect(coreEvents.emitOutput).not.toHaveBeenCalled();
cleanup();
+2 -2
View File
@@ -80,9 +80,9 @@ export function patchStdio(): () => void {
/**
* Creates proxies for process.stdout and process.stderr that use the real write methods
* (writeToStdout and writeToStderr) bypassing any monkey patching.
* This is used by Ink to render to the real output.
* This is used to write to the real output even when stdio is patched.
*/
export function createInkStdio() {
export function createWorkingStdio() {
const inkStdout = new Proxy(process.stdout, {
get(target, prop, receiver) {
if (prop === 'write') {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
"version": "0.20.0-nightly.20251201.2fe609cb6",
"version": "0.20.0",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {