From 10e201eb3b54f74550df20caf87bbffa8a943a93 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 5 Jun 2026 17:07:09 +0800 Subject: [PATCH] fix(cli): remove dead --list-extensions handler from #4456 PR #4456 added a duplicate --list-extensions handler at line 965 and a preconnect guard, but PR #4673 (merged earlier) already implemented the same fix at line 470 using the existing handleListExtensions() function. The code from #4456 is unreachable because the early handler exits via process.exit(0) before execution reaches line 965. Removes: - Dead inline handler block in gemini.tsx (~40 lines) - Unnecessary preconnect guard for list-extensions - 3 test cases that tested the unreachable code path (~242 lines) --- packages/cli/src/gemini.test.tsx | 242 ------------------------------- packages/cli/src/gemini.tsx | 43 +----- 2 files changed, 1 insertion(+), 284 deletions(-) diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 60407d3b59d..10b991267d5 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -685,248 +685,6 @@ describe('gemini.tsx main function', () => { ); expect(runExitCleanupMock).toHaveBeenCalledTimes(1); }); - - it('should print "No extensions installed." and exit when --list-extensions is set and no extensions exist', async () => { - const { loadCliConfig, parseArguments } = await import( - './config/config.js' - ); - const { loadSettings } = await import('./config/settings.js'); - const { loadSandboxConfig } = await import('./config/sandboxConfig.js'); - const { relaunchAppInChildProcess } = await import('./utils/relaunch.js'); - const cleanupModule = await import('./utils/cleanup.js'); - const runExitCleanupMock = vi.mocked(cleanupModule.runExitCleanup); - runExitCleanupMock.mockResolvedValue(undefined); - const processExitSpy = vi - .spyOn(process, 'exit') - .mockImplementation((code) => { - throw new MockProcessExitError(code); - }); - const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - - vi.mocked(loadSandboxConfig).mockResolvedValue(undefined); - vi.mocked(relaunchAppInChildProcess).mockResolvedValue(undefined); - vi.mocked(parseArguments).mockResolvedValue({ - extensions: [], - } as never); - vi.mocked(loadSettings).mockReturnValue({ - errors: [], - merged: { - advanced: {}, - security: { auth: {} }, - ui: {}, - }, - setValue: vi.fn(), - forScope: () => ({ settings: {}, originalSettings: {}, path: '' }), - migrationWarnings: [], - getUserHooks: () => undefined, - getProjectHooks: () => undefined, - } as never); - vi.mocked(loadCliConfig).mockResolvedValue({ - isInteractive: () => false, - getQuestion: () => '', - getSandbox: () => false, - getDebugMode: () => false, - getListExtensions: () => true, - getExtensions: () => [], - getApprovalMode: () => 'suggest', - getMcpServers: () => ({}), - initialize: vi.fn().mockResolvedValue(undefined), - waitForMcpReady: vi.fn().mockResolvedValue(undefined), - getIdeMode: () => false, - getExperimentalZedIntegration: () => false, - getScreenReader: () => false, - getGeminiMdFileCount: () => 0, - getProjectRoot: () => '/', - getOutputFormat: () => OutputFormat.TEXT, - getWarnings: () => [], - getModelsConfig: () => ({ getCurrentAuthType: () => null }), - getSessionId: () => 'test-session-id', - } as unknown as Config); - - try { - await main(); - } catch (error) { - if (!(error instanceof MockProcessExitError)) { - throw error; - } - } - - expect(consoleLogSpy).toHaveBeenCalledWith('No extensions installed.'); - expect(processExitSpy).toHaveBeenCalledWith(0); - expect(runExitCleanupMock).toHaveBeenCalledTimes(1); - // Verify config.initialize() is called before getExtensions() — extensions are loaded during initialize - const configMock = (await vi.mocked(loadCliConfig).mock.results[0]! - .value) as unknown as { initialize: ReturnType }; - expect(configMock.initialize).toHaveBeenCalledTimes(1); - - consoleLogSpy.mockRestore(); - processExitSpy.mockRestore(); - }); - - it('should list extensions with [disabled] suffix when --list-extensions is set', async () => { - const { loadCliConfig, parseArguments } = await import( - './config/config.js' - ); - const { loadSettings } = await import('./config/settings.js'); - const { loadSandboxConfig } = await import('./config/sandboxConfig.js'); - const { relaunchAppInChildProcess } = await import('./utils/relaunch.js'); - const cleanupModule = await import('./utils/cleanup.js'); - const runExitCleanupMock = vi.mocked(cleanupModule.runExitCleanup); - runExitCleanupMock.mockResolvedValue(undefined); - const processExitSpy = vi - .spyOn(process, 'exit') - .mockImplementation((code) => { - throw new MockProcessExitError(code); - }); - const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - - vi.mocked(loadSandboxConfig).mockResolvedValue(undefined); - vi.mocked(relaunchAppInChildProcess).mockResolvedValue(undefined); - vi.mocked(parseArguments).mockResolvedValue({ - extensions: [], - } as never); - vi.mocked(loadSettings).mockReturnValue({ - errors: [], - merged: { - advanced: {}, - security: { auth: {} }, - ui: {}, - }, - setValue: vi.fn(), - forScope: () => ({ settings: {}, originalSettings: {}, path: '' }), - migrationWarnings: [], - getUserHooks: () => undefined, - getProjectHooks: () => undefined, - } as never); - vi.mocked(loadCliConfig).mockResolvedValue({ - isInteractive: () => false, - getQuestion: () => '', - getSandbox: () => false, - getDebugMode: () => false, - getListExtensions: () => true, - getExtensions: () => [ - { name: 'my-ext', version: '1.0.0', isActive: true }, - { name: 'old-ext', version: '0.5.2', isActive: false }, - { name: 'esc-ext', version: '2.0\x1b[31m.0', isActive: true }, - ], - getApprovalMode: () => 'suggest', - getMcpServers: () => ({}), - initialize: vi.fn().mockResolvedValue(undefined), - waitForMcpReady: vi.fn().mockResolvedValue(undefined), - getIdeMode: () => false, - getExperimentalZedIntegration: () => false, - getScreenReader: () => false, - getGeminiMdFileCount: () => 0, - getProjectRoot: () => '/', - getOutputFormat: () => OutputFormat.TEXT, - getWarnings: () => [], - getModelsConfig: () => ({ getCurrentAuthType: () => null }), - getSessionId: () => 'test-session-id', - } as unknown as Config); - - try { - await main(); - } catch (error) { - if (!(error instanceof MockProcessExitError)) { - throw error; - } - } - - expect(consoleLogSpy).toHaveBeenCalledWith('Installed extensions:'); - expect(consoleLogSpy).toHaveBeenCalledWith('- my-ext (v1.0.0)'); - expect(consoleLogSpy).toHaveBeenCalledWith('- old-ext (v0.5.2) [disabled]'); - // Verify non-printable characters are stripped from version output - expect(consoleLogSpy).toHaveBeenCalledWith('- esc-ext (v2.0[31m.0)'); - expect(processExitSpy).toHaveBeenCalledWith(0); - expect(runExitCleanupMock).toHaveBeenCalledTimes(1); - // Verify config.initialize() is called before getExtensions() — extensions are loaded during initialize - const configMock2 = (await vi.mocked(loadCliConfig).mock.results[0]! - .value) as unknown as { initialize: ReturnType }; - expect(configMock2.initialize).toHaveBeenCalledTimes(1); - - consoleLogSpy.mockRestore(); - processExitSpy.mockRestore(); - }); - - it('should exit with code 1 and print error when config.initialize() fails during --list-extensions', async () => { - const { loadCliConfig, parseArguments } = await import( - './config/config.js' - ); - const { loadSettings } = await import('./config/settings.js'); - const { loadSandboxConfig } = await import('./config/sandboxConfig.js'); - const { relaunchAppInChildProcess } = await import('./utils/relaunch.js'); - const cleanupModule = await import('./utils/cleanup.js'); - const runExitCleanupMock = vi.mocked(cleanupModule.runExitCleanup); - runExitCleanupMock.mockResolvedValue(undefined); - const processExitSpy = vi - .spyOn(process, 'exit') - .mockImplementation((code) => { - throw new MockProcessExitError(code); - }); - const stderrWriteSpy = vi - .spyOn(process.stderr, 'write') - .mockImplementation(() => true); - - vi.mocked(loadSandboxConfig).mockResolvedValue(undefined); - vi.mocked(relaunchAppInChildProcess).mockResolvedValue(undefined); - vi.mocked(parseArguments).mockResolvedValue({ - extensions: [], - } as never); - vi.mocked(loadSettings).mockReturnValue({ - errors: [], - merged: { - advanced: {}, - security: { auth: {} }, - ui: {}, - }, - setValue: vi.fn(), - forScope: () => ({ settings: {}, originalSettings: {}, path: '' }), - migrationWarnings: [], - getUserHooks: () => undefined, - getProjectHooks: () => undefined, - } as never); - vi.mocked(loadCliConfig).mockResolvedValue({ - isInteractive: () => false, - getQuestion: () => '', - getSandbox: () => false, - getDebugMode: () => false, - getListExtensions: () => true, - getExtensions: () => [], - getApprovalMode: () => 'suggest', - getMcpServers: () => ({}), - initialize: vi.fn().mockRejectedValue(new Error('config load failed')), - waitForMcpReady: vi.fn().mockResolvedValue(undefined), - getIdeMode: () => false, - getExperimentalZedIntegration: () => false, - getScreenReader: () => false, - getGeminiMdFileCount: () => 0, - getProjectRoot: () => '/', - getOutputFormat: () => OutputFormat.TEXT, - getWarnings: () => [], - getModelsConfig: () => ({ getCurrentAuthType: () => null }), - getSessionId: () => 'test-session-id', - } as unknown as Config); - - try { - await main(); - } catch (error) { - if (!(error instanceof MockProcessExitError)) { - throw error; - } - } - - expect(stderrWriteSpy).toHaveBeenCalledWith( - 'Error: failed to load extensions: config load failed\n', - ); - expect(processExitSpy).toHaveBeenCalledWith(1); - expect(runExitCleanupMock).toHaveBeenCalledTimes(1); - const configMock = (await vi.mocked(loadCliConfig).mock.results[0]! - .value) as unknown as { initialize: ReturnType }; - expect(configMock.initialize).toHaveBeenCalledTimes(1); - - stderrWriteSpy.mockRestore(); - processExitSpy.mockRestore(); - }); }); describe('gemini.tsx main function kitty protocol', () => { diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index d8506512a37..6036719b250 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -839,9 +839,7 @@ export async function main() { const authType = modelsConfig.getCurrentAuthType(); const resolvedBaseUrl = modelsConfig.getGenerationConfig().baseUrl; const proxy = config.getProxy(); - if (!config.getListExtensions()) { - preconnectApi(authType, { resolvedBaseUrl, proxy }); - } + preconnectApi(authType, { resolvedBaseUrl, proxy }); } catch (error) { // If we can't get authType, skip preconnect - it's optional optimization debugLogger.debug( @@ -962,45 +960,6 @@ export async function main() { // Render UI, passing necessary config values. Check that there is no command line question. profileCheckpoint('before_render'); - if (config.getListExtensions()) { - // Always initialize config to populate extensionCache via refreshCache(). - // Without this, getExtensions() returns [] because extensionCache is null. - try { - await config.initialize(); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - process.stderr.write(`Error: failed to load extensions: ${msg}\n`); - await runExitCleanup(); - process.exit(1); - } - const extensions = config.getExtensions(); - if (extensions.length === 0) { - // eslint-disable-next-line no-console -- CLI flag output - console.log('No extensions installed.'); - } else { - // eslint-disable-next-line no-console -- CLI flag output - console.log('Installed extensions:'); - for (const extension of extensions) { - const safeVersion = extension.version.replace( - // eslint-disable-next-line no-control-regex -- intentional: strip control chars for safety - /[\x00-\x1f\x7f-\x9f]/g, - '', - ); - const safeName = extension.name.replace( - // eslint-disable-next-line no-control-regex -- intentional: strip control chars for safety - /[\x00-\x1f\x7f-\x9f]/g, - '', - ); - // eslint-disable-next-line no-console -- CLI flag output - console.log( - `- ${safeName} (v${safeVersion})${extension.isActive ? '' : ' [disabled]'}`, - ); - } - } - await runExitCleanup(); - process.exit(0); - } - if (config.isInteractive()) { // --json-schema is a headless-only contract: the synthetic // structured_output tool only terminates the run inside