diff options
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.test.ts | 163 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.ts | 67 |
2 files changed, 228 insertions, 2 deletions
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index f7f1bb5e..a17fcd1e 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -21,6 +21,7 @@ vi.mock('node:fs/promises', () => ({ import { act, renderHook } from '@testing-library/react'; import { vi, describe, it, expect, beforeEach, Mock } from 'vitest'; +import open from 'open'; import { useSlashCommandProcessor } from './slashCommandProcessor.js'; import { MessageType } from '../types.js'; import * as memoryUtils from '../../config/memoryUtils.js'; @@ -39,6 +40,10 @@ vi.mock('./useShowMemoryCommand.js', () => ({ // Spy on the static method we want to mock const performAddMemoryEntrySpy = vi.spyOn(MemoryTool, 'performAddMemoryEntry'); +vi.mock('open', () => ({ + default: vi.fn(), +})); + describe('useSlashCommandProcessor', () => { let mockAddItem: ReturnType<typeof vi.fn>; let mockClearItems: ReturnType<typeof vi.fn>; @@ -58,7 +63,11 @@ describe('useSlashCommandProcessor', () => { mockOnDebugMessage = vi.fn(); mockOpenThemeDialog = vi.fn(); mockPerformMemoryRefresh = vi.fn().mockResolvedValue(undefined); - mockConfig = { getDebugMode: vi.fn(() => false) } as unknown as Config; + mockConfig = { + getDebugMode: vi.fn(() => false), + getSandbox: vi.fn(() => 'test-sandbox'), // Added mock + getModel: vi.fn(() => 'test-model'), // Added mock + } as unknown as Config; mockCorgiMode = vi.fn(); // Clear mocks for fsPromises if they were used directly or indirectly @@ -66,7 +75,8 @@ describe('useSlashCommandProcessor', () => { vi.mocked(fsPromises.writeFile).mockClear(); vi.mocked(fsPromises.mkdir).mockClear(); - performAddMemoryEntrySpy.mockReset(); // Reset the spy + performAddMemoryEntrySpy.mockReset(); + (open as Mock).mockClear(); vi.spyOn(memoryUtils, 'deleteLastMemoryEntry').mockImplementation(vi.fn()); vi.spyOn(memoryUtils, 'deleteAllAddedMemoryEntries').mockImplementation( vi.fn(), @@ -232,6 +242,155 @@ describe('useSlashCommandProcessor', () => { }); }); + describe('/bug command', () => { + const getExpectedUrl = ( + description?: string, + sandboxEnvVar?: string, + seatbeltProfileVar?: string, + ) => { + const cliVersion = process.env.npm_package_version || 'Unknown'; + const osVersion = `${process.platform} ${process.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'; // From mockConfig + + const diagnosticInfo = ` +## Describe the bug +A clear and concise description of what the bug is. + +## Additional context +Add any other context about the problem here. + +## Diagnostic Information +* **CLI Version:** ${cliVersion} +* **Operating System:** ${osVersion} +* **Sandbox Environment:** ${sandboxEnvStr} +* **Model Version:** ${modelVersion} +`; + let url = + 'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.md'; + if (description) { + url += `&title=${encodeURIComponent(description)}`; + } + url += `&body=${encodeURIComponent(diagnosticInfo)}`; + return url; + }; + + it('should call open with the correct GitHub issue URL', async () => { + 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, + ); + + await act(async () => { + handleSlashCommand(`/bug ${bugDescription}`); + }); + + expect(mockAddItem).toHaveBeenNthCalledWith( + 1, // User command + expect.objectContaining({ + type: MessageType.USER, + text: `/bug ${bugDescription}`, + }), + expect.any(Number), + ); + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, // Info message + expect.objectContaining({ + type: MessageType.INFO, + text: `To submit your bug report, please open the following URL in your browser:\n${expectedUrl}`, + }), + expect.any(Number), // Timestamps are numbers from Date.now() + ); + expect(open).toHaveBeenCalledWith(expectedUrl); + delete process.env.SANDBOX; + delete process.env.SEATBELT_PROFILE; + }); + + it('should open the generic issue page if no bug description is provided', async () => { + process.env.SANDBOX = 'sandbox-exec'; + process.env.SEATBELT_PROFILE = 'minimal'; + const { handleSlashCommand } = getProcessor(); + const expectedUrl = getExpectedUrl( + undefined, + process.env.SANDBOX, + process.env.SEATBELT_PROFILE, + ); + await act(async () => { + handleSlashCommand('/bug '); + }); + expect(open).toHaveBeenCalledWith(expectedUrl); + expect(mockAddItem).toHaveBeenNthCalledWith( + 1, // User command + expect.objectContaining({ + type: MessageType.USER, + text: '/bug', // Ensure this matches the input + }), + expect.any(Number), + ); + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, // Info message + expect.objectContaining({ + type: MessageType.INFO, + text: `To submit your bug report, please open the following URL in your browser:\n${expectedUrl}`, + }), + expect.any(Number), // Timestamps are numbers from Date.now() + ); + delete process.env.SANDBOX; + delete process.env.SEATBELT_PROFILE; + }); + + it('should handle errors when open fails', async () => { + // Test with no SANDBOX env var + delete process.env.SANDBOX; + delete process.env.SEATBELT_PROFILE; + const { handleSlashCommand } = getProcessor(); + const bugDescription = 'Another bug'; + const expectedUrl = getExpectedUrl(bugDescription); + const openError = new Error('Failed to open browser'); + (open as Mock).mockRejectedValue(openError); + + await act(async () => { + handleSlashCommand(`/bug ${bugDescription}`); + }); + + expect(open).toHaveBeenCalledWith(expectedUrl); + expect(mockAddItem).toHaveBeenNthCalledWith( + 1, // User command + expect.objectContaining({ + type: MessageType.USER, + text: `/bug ${bugDescription}`, + }), + expect.any(Number), + ); + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, // Info message before open attempt + expect.objectContaining({ + type: MessageType.INFO, + text: `To submit your bug report, please open the following URL in your browser:\n${expectedUrl}`, + }), + expect.any(Number), // Timestamps are numbers from Date.now() + ); + expect(mockAddItem).toHaveBeenNthCalledWith( + 3, // Error message after open fails + expect.objectContaining({ + type: MessageType.ERROR, + text: `Could not open URL in browser: ${openError.message}`, + }), + expect.any(Number), // Timestamps are numbers from Date.now() + ); + }); + }); + describe('Unknown command', () => { it('should show an error for a general unknown command', async () => { const { handleSlashCommand } = getProcessor(); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 095f4ad7..9f6a6d5e 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -6,6 +6,7 @@ import { useCallback, useMemo } from 'react'; import { type PartListUnion } from '@google/genai'; +import open from 'open'; import { UseHistoryManagerReturn } from './useHistoryManager.js'; import { Config } from '@gemini-code/server'; import { Message, MessageType, HistoryItemWithoutId } from '../types.js'; @@ -139,6 +140,71 @@ export const useSlashCommandProcessor = ( }, }, { + name: 'bug', + description: 'Submit a bug report.', + action: (_mainCommand, _subCommand, args) => { + let bugDescription = _subCommand || ''; + if (args) { + bugDescription += ` ${args}`; + } + bugDescription = bugDescription.trim(); + + const cliVersion = process.env.npm_package_version || 'Unknown'; + 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 diagnosticInfo = ` +## Describe the bug +A clear and concise description of what the bug is. + +## Additional context +Add any other context about the problem here. + +## Diagnostic Information +* **CLI Version:** ${cliVersion} +* **Operating System:** ${osVersion} +* **Sandbox Environment:** ${sandboxEnv} +* **Model Version:** ${modelVersion} +`; + + let bugReportUrl = + 'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.md'; + if (bugDescription) { + const encodedArgs = encodeURIComponent(bugDescription); + bugReportUrl += `&title=${encodedArgs}`; + } + const encodedBody = encodeURIComponent(diagnosticInfo); + bugReportUrl += `&body=${encodedBody}`; + + addMessage({ + type: MessageType.INFO, + content: `To submit your bug report, please open the following URL in your browser:\n${bugReportUrl}`, + timestamp: new Date(), + }); + // Open the URL in the default browser + (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', @@ -159,6 +225,7 @@ export const useSlashCommandProcessor = ( addMemoryAction, addMessage, toggleCorgiMode, + config, // Added config to dependency array ], ); |
