diff options
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/services/CommandService.test.ts | 8 | ||||
| -rw-r--r-- | packages/cli/src/services/CommandService.ts | 2 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/bugCommand.test.ts | 98 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/bugCommand.ts | 78 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.test.ts | 109 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.ts | 68 |
6 files changed, 186 insertions, 177 deletions
diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts index b94e265b..8f5b1421 100644 --- a/packages/cli/src/services/CommandService.test.ts +++ b/packages/cli/src/services/CommandService.test.ts @@ -24,6 +24,7 @@ import { toolsCommand } from '../ui/commands/toolsCommand.js'; import { compressCommand } from '../ui/commands/compressCommand.js'; import { mcpCommand } from '../ui/commands/mcpCommand.js'; import { editorCommand } from '../ui/commands/editorCommand.js'; +import { bugCommand } from '../ui/commands/bugCommand.js'; // Mock the command modules to isolate the service from the command implementations. vi.mock('../ui/commands/memoryCommand.js', () => ({ @@ -71,9 +72,12 @@ vi.mock('../ui/commands/mcpCommand.js', () => ({ vi.mock('../ui/commands/editorCommand.js', () => ({ editorCommand: { name: 'editor', description: 'Mock Editor' }, })); +vi.mock('../ui/commands/bugCommand.js', () => ({ + bugCommand: { name: 'bug', description: 'Mock Bug' }, +})); describe('CommandService', () => { - const subCommandLen = 15; + const subCommandLen = 16; let mockConfig: Mocked<Config>; beforeEach(() => { @@ -110,6 +114,7 @@ describe('CommandService', () => { const commandNames = tree.map((cmd) => cmd.name); expect(commandNames).toContain('auth'); + expect(commandNames).toContain('bug'); expect(commandNames).toContain('memory'); expect(commandNames).toContain('help'); expect(commandNames).toContain('clear'); @@ -167,6 +172,7 @@ describe('CommandService', () => { expect(loadedTree).toEqual([ aboutCommand, authCommand, + bugCommand, chatCommand, clearCommand, compressCommand, diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts index acd73dd9..31914556 100644 --- a/packages/cli/src/services/CommandService.ts +++ b/packages/cli/src/services/CommandService.ts @@ -22,6 +22,7 @@ import { extensionsCommand } from '../ui/commands/extensionsCommand.js'; import { toolsCommand } from '../ui/commands/toolsCommand.js'; import { compressCommand } from '../ui/commands/compressCommand.js'; import { ideCommand } from '../ui/commands/ideCommand.js'; +import { bugCommand } from '../ui/commands/bugCommand.js'; const loadBuiltInCommands = async ( config: Config | null, @@ -29,6 +30,7 @@ const loadBuiltInCommands = async ( const allCommands = [ aboutCommand, authCommand, + bugCommand, chatCommand, clearCommand, compressCommand, diff --git a/packages/cli/src/ui/commands/bugCommand.test.ts b/packages/cli/src/ui/commands/bugCommand.test.ts new file mode 100644 index 00000000..1a618fd1 --- /dev/null +++ b/packages/cli/src/ui/commands/bugCommand.test.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import open from 'open'; +import { bugCommand } from './bugCommand.js'; +import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; +import { getCliVersion } from '../../utils/version.js'; +import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; +import { formatMemoryUsage } from '../utils/formatters.js'; + +// Mock dependencies +vi.mock('open'); +vi.mock('../../utils/version.js'); +vi.mock('../utils/formatters.js'); +vi.mock('node:process', () => ({ + default: { + platform: 'test-platform', + version: 'v20.0.0', + // Keep other necessary process properties if needed by other parts of the code + env: process.env, + memoryUsage: () => ({ rss: 0 }), + }, +})); + +describe('bugCommand', () => { + beforeEach(() => { + vi.mocked(getCliVersion).mockResolvedValue('0.1.0'); + vi.mocked(formatMemoryUsage).mockReturnValue('100 MB'); + vi.stubEnv('SANDBOX', 'gemini-test'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.clearAllMocks(); + }); + + it('should generate the default GitHub issue URL', async () => { + const mockContext = createMockCommandContext({ + services: { + config: { + getModel: () => 'gemini-pro', + getBugCommand: () => undefined, + }, + }, + }); + + if (!bugCommand.action) throw new Error('Action is not defined'); + await bugCommand.action(mockContext, 'A test bug'); + + const expectedInfo = ` +* **CLI Version:** 0.1.0 +* **Git Commit:** ${GIT_COMMIT_INFO} +* **Operating System:** test-platform v20.0.0 +* **Sandbox Environment:** test +* **Model Version:** gemini-pro +* **Memory Usage:** 100 MB +`; + const expectedUrl = + 'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title=A%20test%20bug&info=' + + encodeURIComponent(expectedInfo); + + expect(open).toHaveBeenCalledWith(expectedUrl); + }); + + it('should use a custom URL template from config if provided', async () => { + const customTemplate = + 'https://internal.bug-tracker.com/new?desc={title}&details={info}'; + const mockContext = createMockCommandContext({ + services: { + config: { + getModel: () => 'gemini-pro', + getBugCommand: () => ({ urlTemplate: customTemplate }), + }, + }, + }); + + if (!bugCommand.action) throw new Error('Action is not defined'); + await bugCommand.action(mockContext, 'A custom bug'); + + const expectedInfo = ` +* **CLI Version:** 0.1.0 +* **Git Commit:** ${GIT_COMMIT_INFO} +* **Operating System:** test-platform v20.0.0 +* **Sandbox Environment:** test +* **Model Version:** gemini-pro +* **Memory Usage:** 100 MB +`; + const expectedUrl = customTemplate + .replace('{title}', encodeURIComponent('A custom bug')) + .replace('{info}', encodeURIComponent(expectedInfo)); + + expect(open).toHaveBeenCalledWith(expectedUrl); + }); +}); diff --git a/packages/cli/src/ui/commands/bugCommand.ts b/packages/cli/src/ui/commands/bugCommand.ts new file mode 100644 index 00000000..c1b99db9 --- /dev/null +++ b/packages/cli/src/ui/commands/bugCommand.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import open from 'open'; +import process from 'node:process'; +import { type CommandContext, type SlashCommand } from './types.js'; +import { MessageType } from '../types.js'; +import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; +import { formatMemoryUsage } from '../utils/formatters.js'; +import { getCliVersion } from '../../utils/version.js'; + +export const bugCommand: SlashCommand = { + name: 'bug', + description: 'submit a bug report', + action: async (context: CommandContext, args?: string): Promise<void> => { + const bugDescription = (args || '').trim(); + const { config } = context.services; + + const osVersion = `${process.platform} ${process.version}`; + let sandboxEnv = 'no sandbox'; + if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { + sandboxEnv = process.env.SANDBOX.replace(/^gemini-(?:code-)?/, ''); + } else if (process.env.SANDBOX === 'sandbox-exec') { + sandboxEnv = `sandbox-exec (${ + process.env.SEATBELT_PROFILE || 'unknown' + })`; + } + const modelVersion = config?.getModel() || 'Unknown'; + const cliVersion = await getCliVersion(); + const memoryUsage = formatMemoryUsage(process.memoryUsage().rss); + + const info = ` +* **CLI Version:** ${cliVersion} +* **Git Commit:** ${GIT_COMMIT_INFO} +* **Operating System:** ${osVersion} +* **Sandbox Environment:** ${sandboxEnv} +* **Model Version:** ${modelVersion} +* **Memory Usage:** ${memoryUsage} +`; + + let bugReportUrl = + 'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}'; + + const bugCommandSettings = config?.getBugCommand(); + if (bugCommandSettings?.urlTemplate) { + bugReportUrl = bugCommandSettings.urlTemplate; + } + + bugReportUrl = bugReportUrl + .replace('{title}', encodeURIComponent(bugDescription)) + .replace('{info}', encodeURIComponent(info)); + + context.ui.addItem( + { + type: MessageType.INFO, + text: `To submit your bug report, please open the following URL in your browser:\n${bugReportUrl}`, + }, + Date.now(), + ); + + try { + await open(bugReportUrl); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + context.ui.addItem( + { + type: MessageType.ERROR, + text: `Could not open URL in browser: ${errorMessage}`, + }, + Date.now(), + ); + } + }, +}; diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index ab16d813..71c18dd7 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -71,7 +71,6 @@ import { Config, GeminiClient } from '@google/gemini-cli-core'; import { useSessionStats } from '../contexts/SessionContext.js'; import { LoadedSettings } from '../../config/settings.js'; import * as ShowMemoryCommandModule from './useShowMemoryCommand.js'; -import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; import { CommandService } from '../../services/CommandService.js'; import { SlashCommand } from '../commands/types.js'; @@ -453,114 +452,6 @@ describe('useSlashCommandProcessor', () => { }); }); - describe('/bug command', () => { - const originalEnv = process.env; - beforeEach(() => { - vi.resetModules(); - mockGetCliVersionFn.mockResolvedValue('0.1.0'); - process.env = { ...originalEnv }; - }); - - afterEach(() => { - vi.useRealTimers(); - process.env = originalEnv; - }); - - const getExpectedUrl = ( - description?: string, - sandboxEnvVar?: string, - seatbeltProfileVar?: string, - cliVersion?: string, - ) => { - const osVersion = 'test-platform test-node-version'; - let sandboxEnvStr = 'no sandbox'; - if (sandboxEnvVar && sandboxEnvVar !== 'sandbox-exec') { - sandboxEnvStr = sandboxEnvVar.replace(/^gemini-(?:code-)?/, ''); - } else if (sandboxEnvVar === 'sandbox-exec') { - sandboxEnvStr = `sandbox-exec (${seatbeltProfileVar || 'unknown'})`; - } - const modelVersion = 'test-model'; - // Use the mocked memoryUsage value - const memoryUsage = '11.8 MB'; - - const info = ` -* **CLI Version:** ${cliVersion} -* **Git Commit:** ${GIT_COMMIT_INFO} -* **Operating System:** ${osVersion} -* **Sandbox Environment:** ${sandboxEnvStr} -* **Model Version:** ${modelVersion} -* **Memory Usage:** ${memoryUsage} -`; - let url = - 'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml'; - if (description) { - url += `&title=${encodeURIComponent(description)}`; - } - url += `&info=${encodeURIComponent(info)}`; - return url; - }; - - it('should call open with the correct GitHub issue URL and return true', async () => { - mockGetCliVersionFn.mockResolvedValue('test-version'); - process.env.SANDBOX = 'gemini-sandbox'; - process.env.SEATBELT_PROFILE = 'test_profile'; - const { handleSlashCommand } = getProcessor(); - const bugDescription = 'This is a test bug'; - const expectedUrl = getExpectedUrl( - bugDescription, - process.env.SANDBOX, - process.env.SEATBELT_PROFILE, - 'test-version', - ); - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand(`/bug ${bugDescription}`); - }); - - expect(mockAddItem).toHaveBeenCalledTimes(2); - expect(open).toHaveBeenCalledWith(expectedUrl); - expect(commandResult).toEqual({ type: 'handled' }); - }); - - it('should use the custom bug command URL from config if available', async () => { - process.env.CLI_VERSION = '0.1.0'; - process.env.SANDBOX = 'sandbox-exec'; - process.env.SEATBELT_PROFILE = 'permissive-open'; - const bugCommand = { - urlTemplate: - 'https://custom-bug-tracker.com/new?title={title}&info={info}', - }; - mockConfig = { - ...mockConfig, - getBugCommand: vi.fn(() => bugCommand), - } as unknown as Config; - process.env.CLI_VERSION = '0.1.0'; - - const { handleSlashCommand } = getProcessor(); - const bugDescription = 'This is a custom bug'; - const info = ` -* **CLI Version:** 0.1.0 -* **Git Commit:** ${GIT_COMMIT_INFO} -* **Operating System:** test-platform test-node-version -* **Sandbox Environment:** sandbox-exec (permissive-open) -* **Model Version:** test-model -* **Memory Usage:** 11.8 MB -`; - const expectedUrl = bugCommand.urlTemplate - .replace('{title}', encodeURIComponent(bugDescription)) - .replace('{info}', encodeURIComponent(info)); - - let commandResult: SlashCommandProcessorResult | false = false; - await act(async () => { - commandResult = await handleSlashCommand(`/bug ${bugDescription}`); - }); - - expect(mockAddItem).toHaveBeenCalledTimes(2); - expect(open).toHaveBeenCalledWith(expectedUrl); - expect(commandResult).toEqual({ type: 'handled' }); - }); - }); - describe('/quit and /exit commands', () => { beforeEach(() => { vi.useFakeTimers(); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 237356fa..35371265 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -6,7 +6,6 @@ import { useCallback, useMemo, useEffect, useState } from 'react'; import { type PartListUnion } from '@google/genai'; -import open from 'open'; import process from 'node:process'; import { UseHistoryManagerReturn } from './useHistoryManager.js'; import { useStateAndRef } from './useStateAndRef.js'; @@ -21,9 +20,7 @@ import { } from '../types.js'; import { promises as fs } from 'fs'; import path from 'path'; -import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; -import { formatDuration, formatMemoryUsage } from '../utils/formatters.js'; -import { getCliVersion } from '../../utils/version.js'; +import { formatDuration } from '../utils/formatters.js'; import { LoadedSettings } from '../../config/settings.js'; import { type CommandContext, @@ -206,69 +203,6 @@ export const useSlashCommandProcessor = ( }, }, { - name: 'bug', - description: 'submit a bug report', - action: async (_mainCommand, _subCommand, args) => { - let bugDescription = _subCommand || ''; - if (args) { - bugDescription += ` ${args}`; - } - bugDescription = bugDescription.trim(); - - const osVersion = `${process.platform} ${process.version}`; - let sandboxEnv = 'no sandbox'; - if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { - sandboxEnv = process.env.SANDBOX.replace(/^gemini-(?:code-)?/, ''); - } else if (process.env.SANDBOX === 'sandbox-exec') { - sandboxEnv = `sandbox-exec (${ - process.env.SEATBELT_PROFILE || 'unknown' - })`; - } - const modelVersion = config?.getModel() || 'Unknown'; - const cliVersion = await getCliVersion(); - const memoryUsage = formatMemoryUsage(process.memoryUsage().rss); - - const info = ` -* **CLI Version:** ${cliVersion} -* **Git Commit:** ${GIT_COMMIT_INFO} -* **Operating System:** ${osVersion} -* **Sandbox Environment:** ${sandboxEnv} -* **Model Version:** ${modelVersion} -* **Memory Usage:** ${memoryUsage} -`; - - let bugReportUrl = - 'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}'; - const bugCommand = config?.getBugCommand(); - if (bugCommand?.urlTemplate) { - bugReportUrl = bugCommand.urlTemplate; - } - bugReportUrl = bugReportUrl - .replace('{title}', encodeURIComponent(bugDescription)) - .replace('{info}', encodeURIComponent(info)); - - addMessage({ - type: MessageType.INFO, - content: `To submit your bug report, please open the following URL in your browser:\n${bugReportUrl}`, - timestamp: new Date(), - }); - (async () => { - try { - await open(bugReportUrl); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - addMessage({ - type: MessageType.ERROR, - content: `Could not open URL in browser: ${errorMessage}`, - timestamp: new Date(), - }); - } - })(); - }, - }, - - { name: 'quit', altName: 'exit', description: 'exit the cli', |
