update `/bug` to new slash command arch (#4246)

Co-authored-by: Abhi <abhipatel@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
This commit is contained in:
Harold Mciver 2025-07-16 21:46:35 -04:00 committed by GitHub
parent 0c76affe6d
commit 01e66bb123
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 186 additions and 177 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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);
});
});

View File

@ -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(),
);
}
},
};

View File

@ -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();

View File

@ -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,
@ -205,69 +202,6 @@ export const useSlashCommandProcessor = (
toggleCorgiMode();
},
},
{
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',