diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 5e740de80a..802320814f 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -1474,6 +1474,49 @@ describe('gemini.tsx main function exit codes', () => { expect(refreshAuthSpy).toHaveBeenCalledWith(AuthType.USE_GEMINI); }); + + it('should exit with FATAL_AUTHENTICATION_ERROR when refreshAuth rejects in non-interactive mode', async () => { + const refreshAuthSpy = vi + .fn() + .mockRejectedValue(new Error('ECONNRESET while listing experiments')); + vi.mocked(loadCliConfig).mockResolvedValue( + createMockConfig({ + isInteractive: () => false, + getQuestion: () => 'test prompt', + getSandbox: () => undefined, + refreshAuth: refreshAuthSpy, + }), + ); + vi.mocked(validateNonInteractiveAuth).mockResolvedValue( + AuthType.USE_GEMINI, + ); + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + merged: { security: { auth: { selectedType: undefined } }, ui: {} }, + }), + ); + vi.mocked(parseArguments).mockResolvedValue({ + enabled: true, + allowedPaths: [], + networkAccess: false, + } as unknown as CliArgs); + + runNonInteractiveSpy.mockImplementation(() => Promise.resolve()); + + vi.stubEnv('GEMINI_API_KEY', 'test-key'); + try { + await main(); + expect.fail('Should have thrown MockProcessExitError'); + } catch (e) { + expect(e).toBeInstanceOf(MockProcessExitError); + expect((e as MockProcessExitError).code).toBe( + ExitCodes.FATAL_AUTHENTICATION_ERROR, + ); + } + + expect(refreshAuthSpy).toHaveBeenCalledWith(AuthType.USE_GEMINI); + expect(runNonInteractiveSpy).not.toHaveBeenCalled(); + }); }); describe('validateDnsResolutionOrder', () => { diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 2c76df95f9..5739f242fc 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -864,7 +864,18 @@ export async function main() { config, settings, ); - await config.refreshAuth(authType); + try { + await config.refreshAuth(authType); + } catch (err) { + // Surface auth/network errors (e.g. ECONNRESET while fetching experiments + // or refreshing OAuth tokens) instead of letting the rejection bubble out + // as an unhandled crash with a raw stack trace. + const message = err instanceof Error ? err.message : String(err); + writeToStderr(`Failed to authenticate: ${message}\n`); + debugLogger.error('refreshAuth failed in non-interactive mode:', err); + await runExitCleanup(); + process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR); + } if (config.getDebugMode()) { debugLogger.log('Session ID: %s', sessionId);