From e1ed660eaf630e4b524a4cdd45c43d5f289eb2ba Mon Sep 17 00:00:00 2001 From: Deevi Eswar Date: Tue, 12 May 2026 22:57:23 +0530 Subject: [PATCH 1/2] fix(cli): handle refreshAuth rejection in non-interactive prompt path The second refreshAuth call inside main()'s non-interactive prompt flow ran unguarded, so a transient network error during OAuth token refresh or the experiments fetch (e.g. ECONNRESET against cloudcode-pa.googleapis.com) surfaced as an unhandled promise rejection and crashed the CLI before any user-facing message could be printed. Wrap the call in a try/catch that reports the error to stderr, logs the full detail via debugLogger, and exits with FATAL_AUTHENTICATION_ERROR after running the registered cleanup hooks, matching the existing pattern used by the earlier pre-sandbox refreshAuth attempt. Fixes #25739 --- packages/cli/src/gemini.test.tsx | 45 ++++++++++++++++++++++++++++++++ packages/cli/src/gemini.tsx | 13 ++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 6795c2a1b0..843c8aaa6b 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -1432,6 +1432,51 @@ 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()); + + process.env['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, + ); + } finally { + delete process.env['GEMINI_API_KEY']; + } + + 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 04fc5c2169..a0b63e85ab 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -865,7 +865,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); From 524d5bb522dfa547986b572f5ec4b36cd796bc2b Mon Sep 17 00:00:00 2001 From: Deevi Eswar Date: Sat, 16 May 2026 12:26:35 +0530 Subject: [PATCH 2/2] test(cli): use stubEnv for auth failure test --- packages/cli/src/gemini.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 843c8aaa6b..ca0695153c 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -1461,7 +1461,7 @@ describe('gemini.tsx main function exit codes', () => { runNonInteractiveSpy.mockImplementation(() => Promise.resolve()); - process.env['GEMINI_API_KEY'] = 'test-key'; + vi.stubEnv('GEMINI_API_KEY', 'test-key'); try { await main(); expect.fail('Should have thrown MockProcessExitError'); @@ -1470,8 +1470,6 @@ describe('gemini.tsx main function exit codes', () => { expect((e as MockProcessExitError).code).toBe( ExitCodes.FATAL_AUTHENTICATION_ERROR, ); - } finally { - delete process.env['GEMINI_API_KEY']; } expect(refreshAuthSpy).toHaveBeenCalledWith(AuthType.USE_GEMINI);