diff --git a/integration-tests/mixed-input-crash.test.ts b/integration-tests/mixed-input-crash.test.ts new file mode 100644 index 0000000000..e2db647316 --- /dev/null +++ b/integration-tests/mixed-input-crash.test.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { TestRig } from './test-helper.js'; + +describe('mixed input crash prevention', () => { + it('should not crash when using mixed prompt inputs', async () => { + const rig = new TestRig(); + rig.setup('should not crash when using mixed prompt inputs'); + + // Test: echo "say '1'." | gemini --prompt-interactive="say '2'." say '3'. + const stdinContent = "say '1'."; + + try { + await rig.run( + { stdin: stdinContent }, + '--prompt-interactive', + "say '2'.", + "say '3'.", + ); + throw new Error('Expected the command to fail, but it succeeded'); + } catch (error: unknown) { + expect(error).toBeInstanceOf(Error); + const err = error as Error; + + expect(err.message).toContain('Process exited with code 1'); + expect(err.message).toContain( + '--prompt-interactive flag cannot be used when input is piped', + ); + expect(err.message).not.toContain('setRawMode is not a function'); + expect(err.message).not.toContain('unexpected critical error'); + } + + const lastRequest = rig.readLastApiRequest(); + expect(lastRequest).toBeNull(); + }); + + it('should provide clear error message for mixed input', async () => { + const rig = new TestRig(); + rig.setup('should provide clear error message for mixed input'); + + try { + await rig.run( + { stdin: 'test input' }, + '--prompt-interactive', + 'test prompt', + ); + throw new Error('Expected the command to fail, but it succeeded'); + } catch (error: unknown) { + expect(error).toBeInstanceOf(Error); + const err = error as Error; + + expect(err.message).toContain( + '--prompt-interactive flag cannot be used when input is piped', + ); + } + }); +}); diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 3ed8d6ea80..7d6a522572 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -174,6 +174,15 @@ describe('gemini.tsx main function kitty protocol', () => { (process.stdin as any).setRawMode = vi.fn(); } setRawModeSpy = vi.spyOn(process.stdin, 'setRawMode'); + + Object.defineProperty(process.stdin, 'isTTY', { + value: true, + configurable: true, + }); + Object.defineProperty(process.stdin, 'isRaw', { + value: false, + configurable: true, + }); }); it('should call setRawMode and detectAndEnableKittyProtocol when isInteractive is true', async () => { diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 9b4a3acf8f..af2d0d6c9b 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -209,9 +209,17 @@ export async function main() { argv, ); + // Check for invalid input combinations early to prevent crashes + if (argv.promptInteractive && !process.stdin.isTTY) { + console.error( + 'Error: The --prompt-interactive flag cannot be used when input is piped from stdin.', + ); + process.exit(1); + } + const wasRaw = process.stdin.isRaw; let kittyProtocolDetectionComplete: Promise | undefined; - if (config.isInteractive() && !wasRaw) { + if (config.isInteractive() && !wasRaw && process.stdin.isTTY) { // Set this as early as possible to avoid spurious characters from // input showing up in the output. process.stdin.setRawMode(true); @@ -248,13 +256,6 @@ export async function main() { validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder), ); - if (argv.promptInteractive && !process.stdin.isTTY) { - console.error( - 'Error: The --prompt-interactive flag is not supported when piping input from stdin.', - ); - process.exit(1); - } - if (config.getListExtensions()) { console.log('Installed extensions:'); for (const extension of extensions) {