diff --git a/packages/cli/src/nonInteractiveCliAgentSession.test.ts b/packages/cli/src/nonInteractiveCliAgentSession.test.ts index ebfad9d009..1059223b60 100644 --- a/packages/cli/src/nonInteractiveCliAgentSession.test.ts +++ b/packages/cli/src/nonInteractiveCliAgentSession.test.ts @@ -686,7 +686,7 @@ describe('runNonInteractive', () => { input: 'Trigger loop', prompt_id: 'prompt-id-6', }), - ).rejects.toThrow('process.exit(53) called'); + ).rejects.toThrow('Reached max session turns for this session'); }); it('should preprocess @include commands before sending to the model', async () => { @@ -1194,14 +1194,7 @@ describe('runNonInteractive', () => { () => process.stdin, ); - // Spy on handleCancellationError to verify it's called - const errors = await import('./utils/errors.js'); - const cancellationSentinel = new Error('Cancelled'); - const handleCancellationErrorSpy = vi - .spyOn(errors, 'handleCancellationError') - .mockImplementation(() => { - throw cancellationSentinel; - }); + // Cancellation will throw FatalCancellationError directly const events: ServerGeminiStreamEvent[] = [ { type: GeminiEventType.Content, value: 'Thinking...' }, @@ -1249,10 +1242,7 @@ describe('runNonInteractive', () => { keypressHandler('\u0003', { ctrl: true, name: 'c' }); } - // The Ctrl+C path should route through handleCancellationError rather than - // surfacing the raw stream abort. - await expect(runPromise).rejects.toBe(cancellationSentinel); - expect(handleCancellationErrorSpy).toHaveBeenCalledTimes(1); + await expect(runPromise).rejects.toThrow('Operation cancelled.'); expect( processStderrSpy.mock.calls.some( @@ -1261,8 +1251,6 @@ describe('runNonInteractive', () => { ), ).toBe(true); - handleCancellationErrorSpy.mockRestore(); - // Restore original values Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, @@ -1311,13 +1299,7 @@ describe('runNonInteractive', () => { () => process.stdin, ); - const errors = await import('./utils/errors.js'); - const cancellationSentinel = new Error('Cancelled before send'); - const handleCancellationErrorSpy = vi - .spyOn(errors, 'handleCancellationError') - .mockImplementation(() => { - throw cancellationSentinel; - }); + // Cancellation will throw FatalCancellationError directly const { LegacyAgentSession } = await import('@google/gemini-cli-core'); const sendSpy = vi.spyOn(LegacyAgentSession.prototype, 'send'); @@ -1329,13 +1311,10 @@ describe('runNonInteractive', () => { input: 'Cancelled query', prompt_id: 'prompt-id-pre-send-cancel', }), - ).rejects.toBe(cancellationSentinel); + ).rejects.toThrow('Operation cancelled.'); - expect(handleCancellationErrorSpy).toHaveBeenCalledTimes(1); expect(sendSpy).not.toHaveBeenCalled(); expect(stdinOnSpy).toHaveBeenCalled(); - - handleCancellationErrorSpy.mockRestore(); sendSpy.mockRestore(); Object.defineProperty(process.stdin, 'isTTY', { diff --git a/packages/cli/src/nonInteractiveCliAgentSession.ts b/packages/cli/src/nonInteractiveCliAgentSession.ts index bf1f4e4d6d..78fc18be4e 100644 --- a/packages/cli/src/nonInteractiveCliAgentSession.ts +++ b/packages/cli/src/nonInteractiveCliAgentSession.ts @@ -46,12 +46,7 @@ import stripAnsi from 'strip-ansi'; import { handleSlashCommand } from './nonInteractiveCliCommands.js'; import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; import { handleAtCommand } from './ui/hooks/atCommandProcessor.js'; -import { - handleError, - handleToolError, - handleCancellationError, - handleMaxTurnsExceededError, -} from './utils/errors.js'; +import { handleError, handleToolError } from './utils/errors.js'; import { TextOutput } from './ui/utils/textOutput.js'; interface RunNonInteractiveParams { @@ -188,7 +183,6 @@ export async function runNonInteractive({ }; let errorToHandle: unknown | undefined; - let terminalProcessExitHandled = false; let abortSession = () => {}; try { consolePatcher.patch(); @@ -213,6 +207,8 @@ export async function runNonInteractive({ process.stdout.on('error', (err: NodeJS.ErrnoException) => { if (err.code === 'EPIPE') { // Exit gracefully if the pipe is closed. + cleanupStdinCancellation(); + consolePatcher.cleanup(); process.exit(0); } }); @@ -303,7 +299,7 @@ export async function runNonInteractive({ }; abortController.signal.addEventListener('abort', abortSession); if (abortController.signal.aborted) { - return handleCancellationError(config); + throw new FatalCancellationError('Operation cancelled.'); } // Start the agentic loop (runs in background) @@ -417,11 +413,6 @@ export async function runNonInteractive({ return errToThrow; }; - const runTerminalExitHandler = (handler: () => never): never => { - terminalProcessExitHandled = true; - return handler(); - }; - // Consume AgentEvents for output formatting let responseText = ''; let preToolResponseText: string | undefined; @@ -515,17 +506,12 @@ export async function runNonInteractive({ } if (event.data?.['errorType'] === ToolErrorType.NO_SPACE_LEFT) { - terminalProcessExitHandled = true; - handleToolError( - event.name, - new Error(errorMsg), - config, - typeof event.data?.['errorType'] === 'string' - ? event.data['errorType'] - : undefined, - displayText, + throw new FatalToolExecutionError( + 'Error executing tool ' + + event.name + + ': ' + + (displayText || errorMsg), ); - return; } handleToolError( event.name, @@ -570,15 +556,15 @@ export async function runNonInteractive({ } case 'agent_end': { if (event.reason === 'aborted') { - runTerminalExitHandler(() => handleCancellationError(config)); + throw new FatalCancellationError('Operation cancelled.'); } else if (event.reason === 'max_turns') { const isConfiguredTurnLimit = typeof event.data?.['maxTurns'] === 'number' || typeof event.data?.['turnCount'] === 'number'; if (isConfiguredTurnLimit) { - runTerminalExitHandler(() => - handleMaxTurnsExceededError(config), + throw new FatalTurnLimitedError( + 'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', ); } else if (streamFormatter) { streamFormatter.emitEvent({ @@ -629,9 +615,6 @@ export async function runNonInteractive({ } if (errorToHandle) { - if (terminalProcessExitHandled) { - throw errorToHandle; - } handleError(errorToHandle, config); } });