|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { mockProcessExit } = vi.hoisted(() => ({ |
|
|
mockProcessExit: vi.fn((_code?: number): never => undefined as never), |
|
|
})); |
|
|
|
|
|
vi.mock('node:process', () => ({ |
|
|
default: { |
|
|
exit: mockProcessExit, |
|
|
cwd: vi.fn(() => '/mock/cwd'), |
|
|
get env() { |
|
|
return process.env; |
|
|
}, |
|
|
platform: 'test-platform', |
|
|
version: 'test-node-version', |
|
|
memoryUsage: vi.fn(() => ({ |
|
|
rss: 12345678, |
|
|
heapTotal: 23456789, |
|
|
heapUsed: 10234567, |
|
|
external: 1234567, |
|
|
arrayBuffers: 123456, |
|
|
})), |
|
|
}, |
|
|
|
|
|
exit: mockProcessExit, |
|
|
cwd: vi.fn(() => '/mock/cwd'), |
|
|
get env() { |
|
|
return process.env; |
|
|
}, |
|
|
platform: 'test-platform', |
|
|
version: 'test-node-version', |
|
|
memoryUsage: vi.fn(() => ({ |
|
|
rss: 12345678, |
|
|
heapTotal: 23456789, |
|
|
heapUsed: 10234567, |
|
|
external: 1234567, |
|
|
arrayBuffers: 123456, |
|
|
})), |
|
|
})); |
|
|
|
|
|
vi.mock('node:fs/promises', () => ({ |
|
|
readFile: vi.fn(), |
|
|
writeFile: vi.fn(), |
|
|
mkdir: vi.fn(), |
|
|
})); |
|
|
|
|
|
const mockGetCliVersionFn = vi.fn(() => Promise.resolve('0.1.0')); |
|
|
vi.mock('../../utils/version.js', () => ({ |
|
|
getCliVersion: (...args: []) => mockGetCliVersionFn(...args), |
|
|
})); |
|
|
|
|
|
import { act, renderHook } from '@testing-library/react'; |
|
|
import { vi, describe, it, expect, beforeEach, afterEach, Mock } from 'vitest'; |
|
|
import open from 'open'; |
|
|
import { |
|
|
useSlashCommandProcessor, |
|
|
type SlashCommandActionReturn, |
|
|
} from './slashCommandProcessor.js'; |
|
|
import { MessageType } from '../types.js'; |
|
|
import { |
|
|
Config, |
|
|
MCPDiscoveryState, |
|
|
MCPServerStatus, |
|
|
getMCPDiscoveryState, |
|
|
getMCPServerStatus, |
|
|
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'; |
|
|
|
|
|
vi.mock('../contexts/SessionContext.js', () => ({ |
|
|
useSessionStats: vi.fn(), |
|
|
})); |
|
|
|
|
|
vi.mock('./useShowMemoryCommand.js', () => ({ |
|
|
SHOW_MEMORY_COMMAND_NAME: '/memory show', |
|
|
createShowMemoryAction: vi.fn(() => vi.fn()), |
|
|
})); |
|
|
|
|
|
vi.mock('open', () => ({ |
|
|
default: vi.fn(), |
|
|
})); |
|
|
|
|
|
describe('useSlashCommandProcessor', () => { |
|
|
let mockAddItem: ReturnType<typeof vi.fn>; |
|
|
let mockClearItems: ReturnType<typeof vi.fn>; |
|
|
let mockLoadHistory: ReturnType<typeof vi.fn>; |
|
|
let mockRefreshStatic: ReturnType<typeof vi.fn>; |
|
|
let mockSetShowHelp: ReturnType<typeof vi.fn>; |
|
|
let mockOnDebugMessage: ReturnType<typeof vi.fn>; |
|
|
let mockOpenThemeDialog: ReturnType<typeof vi.fn>; |
|
|
let mockOpenAuthDialog: ReturnType<typeof vi.fn>; |
|
|
let mockOpenEditorDialog: ReturnType<typeof vi.fn>; |
|
|
let mockPerformMemoryRefresh: ReturnType<typeof vi.fn>; |
|
|
let mockSetQuittingMessages: ReturnType<typeof vi.fn>; |
|
|
let mockTryCompressChat: ReturnType<typeof vi.fn>; |
|
|
let mockGeminiClient: GeminiClient; |
|
|
let mockConfig: Config; |
|
|
let mockCorgiMode: ReturnType<typeof vi.fn>; |
|
|
const mockUseSessionStats = useSessionStats as Mock; |
|
|
|
|
|
beforeEach(() => { |
|
|
mockAddItem = vi.fn(); |
|
|
mockClearItems = vi.fn(); |
|
|
mockLoadHistory = vi.fn(); |
|
|
mockRefreshStatic = vi.fn(); |
|
|
mockSetShowHelp = vi.fn(); |
|
|
mockOnDebugMessage = vi.fn(); |
|
|
mockOpenThemeDialog = vi.fn(); |
|
|
mockOpenAuthDialog = vi.fn(); |
|
|
mockOpenEditorDialog = vi.fn(); |
|
|
mockPerformMemoryRefresh = vi.fn().mockResolvedValue(undefined); |
|
|
mockSetQuittingMessages = vi.fn(); |
|
|
mockTryCompressChat = vi.fn(); |
|
|
mockGeminiClient = { |
|
|
tryCompressChat: mockTryCompressChat, |
|
|
} as unknown as GeminiClient; |
|
|
mockConfig = { |
|
|
getDebugMode: vi.fn(() => false), |
|
|
getGeminiClient: () => mockGeminiClient, |
|
|
getSandbox: vi.fn(() => 'test-sandbox'), |
|
|
getModel: vi.fn(() => 'test-model'), |
|
|
getProjectRoot: vi.fn(() => '/test/dir'), |
|
|
getCheckpointingEnabled: vi.fn(() => true), |
|
|
getBugCommand: vi.fn(() => undefined), |
|
|
} as unknown as Config; |
|
|
mockCorgiMode = vi.fn(); |
|
|
mockUseSessionStats.mockReturnValue({ |
|
|
stats: { |
|
|
sessionStartTime: new Date('2025-01-01T00:00:00.000Z'), |
|
|
cumulative: { |
|
|
turnCount: 0, |
|
|
promptTokenCount: 0, |
|
|
candidatesTokenCount: 0, |
|
|
totalTokenCount: 0, |
|
|
cachedContentTokenCount: 0, |
|
|
toolUsePromptTokenCount: 0, |
|
|
thoughtsTokenCount: 0, |
|
|
}, |
|
|
}, |
|
|
}); |
|
|
|
|
|
(open as Mock).mockClear(); |
|
|
mockProcessExit.mockClear(); |
|
|
(ShowMemoryCommandModule.createShowMemoryAction as Mock).mockClear(); |
|
|
mockPerformMemoryRefresh.mockClear(); |
|
|
process.env = { ...globalThis.process.env }; |
|
|
}); |
|
|
|
|
|
const getProcessorHook = (showToolDescriptions: boolean = false) => { |
|
|
const settings = { |
|
|
merged: { |
|
|
contextFileName: 'GEMINI.md', |
|
|
}, |
|
|
} as LoadedSettings; |
|
|
return renderHook(() => |
|
|
useSlashCommandProcessor( |
|
|
mockConfig, |
|
|
settings, |
|
|
[], |
|
|
mockAddItem, |
|
|
mockClearItems, |
|
|
mockLoadHistory, |
|
|
mockRefreshStatic, |
|
|
mockSetShowHelp, |
|
|
mockOnDebugMessage, |
|
|
mockOpenThemeDialog, |
|
|
mockOpenAuthDialog, |
|
|
mockOpenEditorDialog, |
|
|
mockPerformMemoryRefresh, |
|
|
mockCorgiMode, |
|
|
showToolDescriptions, |
|
|
mockSetQuittingMessages, |
|
|
), |
|
|
); |
|
|
}; |
|
|
|
|
|
const getProcessor = (showToolDescriptions: boolean = false) => |
|
|
getProcessorHook(showToolDescriptions).result.current; |
|
|
|
|
|
describe('/memory add', () => { |
|
|
it('should return tool scheduling info on valid input', async () => { |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
const fact = 'Remember this fact'; |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand(`/memory add ${fact}`); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
1, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.USER, |
|
|
text: `/memory add ${fact}`, |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.INFO, |
|
|
text: `Attempting to save to memory: "${fact}"`, |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
|
|
|
expect(commandResult).toEqual({ |
|
|
shouldScheduleTool: true, |
|
|
toolName: 'save_memory', |
|
|
toolArgs: { fact }, |
|
|
}); |
|
|
|
|
|
|
|
|
expect(mockPerformMemoryRefresh).not.toHaveBeenCalled(); |
|
|
}); |
|
|
|
|
|
it('should show usage error and return true if no text is provided', async () => { |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/memory add '); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.ERROR, |
|
|
text: 'Usage: /memory add <text to remember>', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/memory show', () => { |
|
|
it('should call the showMemoryAction and return true', async () => { |
|
|
const mockReturnedShowAction = vi.fn(); |
|
|
vi.mocked(ShowMemoryCommandModule.createShowMemoryAction).mockReturnValue( |
|
|
mockReturnedShowAction, |
|
|
); |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/memory show'); |
|
|
}); |
|
|
expect( |
|
|
ShowMemoryCommandModule.createShowMemoryAction, |
|
|
).toHaveBeenCalledWith( |
|
|
mockConfig, |
|
|
expect.any(Object), |
|
|
expect.any(Function), |
|
|
); |
|
|
expect(mockReturnedShowAction).toHaveBeenCalled(); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/memory refresh', () => { |
|
|
it('should call performMemoryRefresh and return true', async () => { |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/memory refresh'); |
|
|
}); |
|
|
expect(mockPerformMemoryRefresh).toHaveBeenCalled(); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('Unknown /memory subcommand', () => { |
|
|
it('should show an error for unknown /memory subcommand and return true', async () => { |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/memory foobar'); |
|
|
}); |
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.ERROR, |
|
|
text: 'Unknown /memory command: foobar. Available: show, refresh, add', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/stats command', () => { |
|
|
it('should show detailed session statistics', async () => { |
|
|
|
|
|
const cumulativeStats = { |
|
|
totalTokenCount: 900, |
|
|
promptTokenCount: 200, |
|
|
candidatesTokenCount: 400, |
|
|
cachedContentTokenCount: 100, |
|
|
turnCount: 1, |
|
|
toolUsePromptTokenCount: 50, |
|
|
thoughtsTokenCount: 150, |
|
|
}; |
|
|
mockUseSessionStats.mockReturnValue({ |
|
|
stats: { |
|
|
sessionStartTime: new Date('2025-01-01T00:00:00.000Z'), |
|
|
cumulative: cumulativeStats, |
|
|
}, |
|
|
}); |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
const mockDate = new Date('2025-01-01T01:02:03.000Z'); |
|
|
vi.setSystemTime(mockDate); |
|
|
|
|
|
|
|
|
await act(async () => { |
|
|
handleSlashCommand('/stats'); |
|
|
}); |
|
|
|
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.STATS, |
|
|
stats: cumulativeStats, |
|
|
duration: '1h 2m 3s', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
|
|
|
vi.useRealTimers(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/about command', () => { |
|
|
it('should show the about box with all details including auth and project', async () => { |
|
|
|
|
|
mockGetCliVersionFn.mockResolvedValue('test-version'); |
|
|
process.env.SANDBOX = 'gemini-sandbox'; |
|
|
process.env.GOOGLE_CLOUD_PROJECT = 'test-gcp-project'; |
|
|
vi.mocked(mockConfig.getModel).mockReturnValue('test-model-from-config'); |
|
|
|
|
|
const settings = { |
|
|
merged: { |
|
|
selectedAuthType: 'test-auth-type', |
|
|
contextFileName: 'GEMINI.md', |
|
|
}, |
|
|
} as LoadedSettings; |
|
|
|
|
|
const { result } = renderHook(() => |
|
|
useSlashCommandProcessor( |
|
|
mockConfig, |
|
|
settings, |
|
|
[], |
|
|
mockAddItem, |
|
|
mockClearItems, |
|
|
mockLoadHistory, |
|
|
mockRefreshStatic, |
|
|
mockSetShowHelp, |
|
|
mockOnDebugMessage, |
|
|
mockOpenThemeDialog, |
|
|
mockOpenAuthDialog, |
|
|
mockOpenEditorDialog, |
|
|
mockPerformMemoryRefresh, |
|
|
mockCorgiMode, |
|
|
false, |
|
|
mockSetQuittingMessages, |
|
|
), |
|
|
); |
|
|
|
|
|
|
|
|
await act(async () => { |
|
|
await result.current.handleSlashCommand('/about'); |
|
|
}); |
|
|
|
|
|
|
|
|
expect(mockAddItem).toHaveBeenCalledTimes(2); |
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: 'about', |
|
|
cliVersion: 'test-version', |
|
|
osVersion: 'test-platform', |
|
|
sandboxEnv: 'gemini-sandbox', |
|
|
modelVersion: 'test-model-from-config', |
|
|
selectedAuthType: 'test-auth-type', |
|
|
gcpProject: 'test-gcp-project', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
}); |
|
|
|
|
|
it('should show sandbox-exec profile when applicable', async () => { |
|
|
|
|
|
mockGetCliVersionFn.mockResolvedValue('test-version'); |
|
|
process.env.SANDBOX = 'sandbox-exec'; |
|
|
process.env.SEATBELT_PROFILE = 'test-profile'; |
|
|
vi.mocked(mockConfig.getModel).mockReturnValue('test-model-from-config'); |
|
|
|
|
|
const { result } = getProcessorHook(); |
|
|
|
|
|
|
|
|
await act(async () => { |
|
|
await result.current.handleSlashCommand('/about'); |
|
|
}); |
|
|
|
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
sandboxEnv: 'sandbox-exec (test-profile)', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('Other commands', () => { |
|
|
it('/help should open help and return true', async () => { |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/help'); |
|
|
}); |
|
|
expect(mockSetShowHelp).toHaveBeenCalledWith(true); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('/clear should clear items, reset chat, and refresh static', async () => { |
|
|
const mockResetChat = vi.fn(); |
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getGeminiClient: () => ({ |
|
|
resetChat: mockResetChat, |
|
|
}), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/clear'); |
|
|
}); |
|
|
|
|
|
expect(mockClearItems).toHaveBeenCalled(); |
|
|
expect(mockResetChat).toHaveBeenCalled(); |
|
|
expect(mockRefreshStatic).toHaveBeenCalled(); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('/editor should open editor dialog and return true', async () => { |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/editor'); |
|
|
}); |
|
|
expect(mockOpenEditorDialog).toHaveBeenCalled(); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/bug command', () => { |
|
|
const originalEnv = process.env; |
|
|
beforeEach(() => { |
|
|
vi.resetModules(); |
|
|
mockGetCliVersionFn.mockResolvedValue('0.1.0'); |
|
|
process.env = { ...originalEnv }; |
|
|
}); |
|
|
|
|
|
afterEach(() => { |
|
|
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'; |
|
|
|
|
|
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: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand(`/bug ${bugDescription}`); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenCalledTimes(2); |
|
|
expect(open).toHaveBeenCalledWith(expectedUrl); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
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: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand(`/bug ${bugDescription}`); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenCalledTimes(2); |
|
|
expect(open).toHaveBeenCalledWith(expectedUrl); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/quit and /exit commands', () => { |
|
|
beforeEach(() => { |
|
|
vi.useFakeTimers(); |
|
|
}); |
|
|
|
|
|
afterEach(() => { |
|
|
vi.useRealTimers(); |
|
|
}); |
|
|
|
|
|
it.each([['/quit'], ['/exit']])( |
|
|
'should handle %s, set quitting messages, and exit the process', |
|
|
async (command) => { |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
const mockDate = new Date('2025-01-01T01:02:03.000Z'); |
|
|
vi.setSystemTime(mockDate); |
|
|
|
|
|
await act(async () => { |
|
|
handleSlashCommand(command); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).not.toHaveBeenCalled(); |
|
|
expect(mockSetQuittingMessages).toHaveBeenCalledWith([ |
|
|
{ |
|
|
type: 'user', |
|
|
text: command, |
|
|
id: expect.any(Number), |
|
|
}, |
|
|
{ |
|
|
type: 'quit', |
|
|
stats: expect.any(Object), |
|
|
duration: '1h 2m 3s', |
|
|
id: expect.any(Number), |
|
|
}, |
|
|
]); |
|
|
|
|
|
|
|
|
await act(async () => { |
|
|
vi.advanceTimersByTime(100); |
|
|
}); |
|
|
expect(mockProcessExit).toHaveBeenCalledWith(0); |
|
|
}, |
|
|
); |
|
|
}); |
|
|
|
|
|
describe('Unknown command', () => { |
|
|
it('should show an error and return true for a general unknown command', async () => { |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/unknowncommand'); |
|
|
}); |
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.ERROR, |
|
|
text: 'Unknown command: /unknowncommand', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/tools command', () => { |
|
|
it('should show an error if tool registry is not available', async () => { |
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue(undefined), |
|
|
} as unknown as Config; |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/tools'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.ERROR, |
|
|
text: 'Could not retrieve tools.', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should show an error if getAllTools returns undefined', async () => { |
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getAllTools: vi.fn().mockReturnValue(undefined), |
|
|
}), |
|
|
} as unknown as Config; |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/tools'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.ERROR, |
|
|
text: 'Could not retrieve tools.', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should display only Gemini CLI tools (filtering out MCP tools)', async () => { |
|
|
|
|
|
const mockTools = [ |
|
|
{ name: 'tool1', displayName: 'Tool1' }, |
|
|
{ name: 'tool2', displayName: 'Tool2' }, |
|
|
{ name: 'mcp_tool1', serverName: 'mcp-server1' }, |
|
|
{ name: 'mcp_tool2', serverName: 'mcp-server1' }, |
|
|
]; |
|
|
|
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getAllTools: vi.fn().mockReturnValue(mockTools), |
|
|
}), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/tools'); |
|
|
}); |
|
|
|
|
|
|
|
|
const message = mockAddItem.mock.calls[1][0].text; |
|
|
expect(message).toContain('Tool1'); |
|
|
expect(message).toContain('Tool2'); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should display a message when no Gemini CLI tools are available', async () => { |
|
|
|
|
|
const mockTools = [ |
|
|
{ name: 'mcp_tool1', serverName: 'mcp-server1' }, |
|
|
{ name: 'mcp_tool2', serverName: 'mcp-server1' }, |
|
|
]; |
|
|
|
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getAllTools: vi.fn().mockReturnValue(mockTools), |
|
|
}), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/tools'); |
|
|
}); |
|
|
|
|
|
const message = mockAddItem.mock.calls[1][0].text; |
|
|
expect(message).toContain('No tools available'); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should display tool descriptions when /tools desc is used', async () => { |
|
|
const mockTools = [ |
|
|
{ |
|
|
name: 'tool1', |
|
|
displayName: 'Tool1', |
|
|
description: 'Description for Tool1', |
|
|
}, |
|
|
{ |
|
|
name: 'tool2', |
|
|
displayName: 'Tool2', |
|
|
description: 'Description for Tool2', |
|
|
}, |
|
|
]; |
|
|
|
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getAllTools: vi.fn().mockReturnValue(mockTools), |
|
|
}), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/tools desc'); |
|
|
}); |
|
|
|
|
|
const message = mockAddItem.mock.calls[1][0].text; |
|
|
expect(message).toContain('Tool1'); |
|
|
expect(message).toContain('Description for Tool1'); |
|
|
expect(message).toContain('Tool2'); |
|
|
expect(message).toContain('Description for Tool2'); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/mcp command', () => { |
|
|
beforeEach(() => { |
|
|
|
|
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => { |
|
|
const actual = await importOriginal(); |
|
|
return { |
|
|
...actual, |
|
|
MCPServerStatus: { |
|
|
CONNECTED: 'connected', |
|
|
CONNECTING: 'connecting', |
|
|
DISCONNECTED: 'disconnected', |
|
|
}, |
|
|
MCPDiscoveryState: { |
|
|
NOT_STARTED: 'not_started', |
|
|
IN_PROGRESS: 'in_progress', |
|
|
COMPLETED: 'completed', |
|
|
}, |
|
|
getMCPServerStatus: vi.fn(), |
|
|
getMCPDiscoveryState: vi.fn(), |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
|
|
|
it('should show an error if tool registry is not available', async () => { |
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue(undefined), |
|
|
} as unknown as Config; |
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/mcp'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.ERROR, |
|
|
text: 'Could not retrieve tool registry.', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should display a message with a URL when no MCP servers are configured in a sandbox', async () => { |
|
|
process.env.SANDBOX = 'sandbox'; |
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getToolsByServer: vi.fn().mockReturnValue([]), |
|
|
}), |
|
|
getMcpServers: vi.fn().mockReturnValue({}), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/mcp'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.INFO, |
|
|
text: `No MCP servers configured. Please open the following URL in your browser to view documentation:\nhttps://goo.gle/gemini-cli-docs-mcp`, |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(commandResult).toBe(true); |
|
|
delete process.env.SANDBOX; |
|
|
}); |
|
|
|
|
|
it('should display a message and open a URL when no MCP servers are configured outside a sandbox', async () => { |
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getToolsByServer: vi.fn().mockReturnValue([]), |
|
|
}), |
|
|
getMcpServers: vi.fn().mockReturnValue({}), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/mcp'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.INFO, |
|
|
text: 'No MCP servers configured. Opening documentation in your browser: https://goo.gle/gemini-cli-docs-mcp', |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
expect(open).toHaveBeenCalledWith('https://goo.gle/gemini-cli-docs-mcp'); |
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should display configured MCP servers with status indicators and their tools', async () => { |
|
|
|
|
|
const mockMcpServers = { |
|
|
server1: { command: 'cmd1' }, |
|
|
server2: { command: 'cmd2' }, |
|
|
server3: { command: 'cmd3' }, |
|
|
}; |
|
|
|
|
|
|
|
|
vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { |
|
|
if (serverName === 'server1') return MCPServerStatus.CONNECTED; |
|
|
if (serverName === 'server2') return MCPServerStatus.CONNECTED; |
|
|
return MCPServerStatus.DISCONNECTED; |
|
|
}); |
|
|
|
|
|
|
|
|
vi.mocked(getMCPDiscoveryState).mockReturnValue( |
|
|
MCPDiscoveryState.COMPLETED, |
|
|
); |
|
|
|
|
|
|
|
|
const mockServer1Tools = [ |
|
|
{ name: 'server1_tool1' }, |
|
|
{ name: 'server1_tool2' }, |
|
|
]; |
|
|
|
|
|
const mockServer2Tools = [{ name: 'server2_tool1' }]; |
|
|
|
|
|
const mockServer3Tools = [{ name: 'server3_tool1' }]; |
|
|
|
|
|
const mockGetToolsByServer = vi.fn().mockImplementation((serverName) => { |
|
|
if (serverName === 'server1') return mockServer1Tools; |
|
|
if (serverName === 'server2') return mockServer2Tools; |
|
|
if (serverName === 'server3') return mockServer3Tools; |
|
|
return []; |
|
|
}); |
|
|
|
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getToolsByServer: mockGetToolsByServer, |
|
|
}), |
|
|
getMcpServers: vi.fn().mockReturnValue(mockMcpServers), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/mcp'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.INFO, |
|
|
text: expect.stringContaining('Configured MCP servers:'), |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
|
|
|
|
|
|
const message = mockAddItem.mock.calls[1][0].text; |
|
|
|
|
|
expect(message).toContain( |
|
|
'🟢 \u001b[1mserver1\u001b[0m - Ready (2 tools)', |
|
|
); |
|
|
expect(message).toContain('\u001b[36mserver1_tool1\u001b[0m'); |
|
|
expect(message).toContain('\u001b[36mserver1_tool2\u001b[0m'); |
|
|
|
|
|
|
|
|
expect(message).toContain( |
|
|
'🟢 \u001b[1mserver2\u001b[0m - Ready (1 tools)', |
|
|
); |
|
|
expect(message).toContain('\u001b[36mserver2_tool1\u001b[0m'); |
|
|
|
|
|
|
|
|
expect(message).toContain( |
|
|
'🔴 \u001b[1mserver3\u001b[0m - Disconnected (1 tools cached)', |
|
|
); |
|
|
expect(message).toContain('\u001b[36mserver3_tool1\u001b[0m'); |
|
|
|
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should display tool descriptions when showToolDescriptions is true', async () => { |
|
|
|
|
|
const mockMcpServers = { |
|
|
server1: { |
|
|
command: 'cmd1', |
|
|
description: 'This is a server description', |
|
|
}, |
|
|
}; |
|
|
|
|
|
|
|
|
vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { |
|
|
if (serverName === 'server1') return MCPServerStatus.CONNECTED; |
|
|
return MCPServerStatus.DISCONNECTED; |
|
|
}); |
|
|
|
|
|
|
|
|
vi.mocked(getMCPDiscoveryState).mockReturnValue( |
|
|
MCPDiscoveryState.COMPLETED, |
|
|
); |
|
|
|
|
|
|
|
|
const mockServerTools = [ |
|
|
{ name: 'tool1', description: 'This is tool 1 description' }, |
|
|
{ name: 'tool2', description: 'This is tool 2 description' }, |
|
|
]; |
|
|
|
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getToolsByServer: vi.fn().mockReturnValue(mockServerTools), |
|
|
}), |
|
|
getMcpServers: vi.fn().mockReturnValue(mockMcpServers), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(true); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/mcp'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.INFO, |
|
|
text: expect.stringContaining('Configured MCP servers:'), |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
|
|
|
const message = mockAddItem.mock.calls[1][0].text; |
|
|
|
|
|
|
|
|
expect(message).toContain('\u001b[1mserver1\u001b[0m - Ready (2 tools)'); |
|
|
expect(message).toContain( |
|
|
'\u001b[32mThis is a server description\u001b[0m', |
|
|
); |
|
|
|
|
|
|
|
|
expect(message).toContain('\u001b[36mtool1\u001b[0m'); |
|
|
expect(message).toContain( |
|
|
'\u001b[32mThis is tool 1 description\u001b[0m', |
|
|
); |
|
|
expect(message).toContain('\u001b[36mtool2\u001b[0m'); |
|
|
expect(message).toContain( |
|
|
'\u001b[32mThis is tool 2 description\u001b[0m', |
|
|
); |
|
|
|
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should indicate when a server has no tools', async () => { |
|
|
|
|
|
const mockMcpServers = { |
|
|
server1: { command: 'cmd1' }, |
|
|
server2: { command: 'cmd2' }, |
|
|
}; |
|
|
|
|
|
|
|
|
vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { |
|
|
if (serverName === 'server1') return MCPServerStatus.CONNECTED; |
|
|
if (serverName === 'server2') return MCPServerStatus.DISCONNECTED; |
|
|
return MCPServerStatus.DISCONNECTED; |
|
|
}); |
|
|
|
|
|
|
|
|
vi.mocked(getMCPDiscoveryState).mockReturnValue( |
|
|
MCPDiscoveryState.COMPLETED, |
|
|
); |
|
|
|
|
|
|
|
|
const mockServer1Tools = [{ name: 'server1_tool1' }]; |
|
|
|
|
|
const mockServer2Tools = []; |
|
|
|
|
|
const mockGetToolsByServer = vi.fn().mockImplementation((serverName) => { |
|
|
if (serverName === 'server1') return mockServer1Tools; |
|
|
if (serverName === 'server2') return mockServer2Tools; |
|
|
return []; |
|
|
}); |
|
|
|
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getToolsByServer: mockGetToolsByServer, |
|
|
}), |
|
|
getMcpServers: vi.fn().mockReturnValue(mockMcpServers), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/mcp'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.INFO, |
|
|
text: expect.stringContaining('Configured MCP servers:'), |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
|
|
|
|
|
|
const message = mockAddItem.mock.calls[1][0].text; |
|
|
expect(message).toContain( |
|
|
'🟢 \u001b[1mserver1\u001b[0m - Ready (1 tools)', |
|
|
); |
|
|
expect(message).toContain('\u001b[36mserver1_tool1\u001b[0m'); |
|
|
expect(message).toContain( |
|
|
'🔴 \u001b[1mserver2\u001b[0m - Disconnected (0 tools cached)', |
|
|
); |
|
|
expect(message).toContain('No tools available'); |
|
|
|
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should show startup indicator when servers are connecting', async () => { |
|
|
|
|
|
const mockMcpServers = { |
|
|
server1: { command: 'cmd1' }, |
|
|
server2: { command: 'cmd2' }, |
|
|
}; |
|
|
|
|
|
|
|
|
vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { |
|
|
if (serverName === 'server1') return MCPServerStatus.CONNECTED; |
|
|
if (serverName === 'server2') return MCPServerStatus.CONNECTING; |
|
|
return MCPServerStatus.DISCONNECTED; |
|
|
}); |
|
|
|
|
|
|
|
|
vi.mocked(getMCPDiscoveryState).mockReturnValue( |
|
|
MCPDiscoveryState.IN_PROGRESS, |
|
|
); |
|
|
|
|
|
|
|
|
const mockServer1Tools = [{ name: 'server1_tool1' }]; |
|
|
const mockServer2Tools = [{ name: 'server2_tool1' }]; |
|
|
|
|
|
const mockGetToolsByServer = vi.fn().mockImplementation((serverName) => { |
|
|
if (serverName === 'server1') return mockServer1Tools; |
|
|
if (serverName === 'server2') return mockServer2Tools; |
|
|
return []; |
|
|
}); |
|
|
|
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getToolsByServer: mockGetToolsByServer, |
|
|
}), |
|
|
getMcpServers: vi.fn().mockReturnValue(mockMcpServers), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/mcp'); |
|
|
}); |
|
|
|
|
|
const message = mockAddItem.mock.calls[1][0].text; |
|
|
|
|
|
|
|
|
expect(message).toContain( |
|
|
'⏳ MCP servers are starting up (1 initializing)...', |
|
|
); |
|
|
expect(message).toContain( |
|
|
'Note: First startup may take longer. Tool availability will update automatically.', |
|
|
); |
|
|
|
|
|
|
|
|
expect(message).toContain( |
|
|
'🟢 \u001b[1mserver1\u001b[0m - Ready (1 tools)', |
|
|
); |
|
|
expect(message).toContain( |
|
|
'🔄 \u001b[1mserver2\u001b[0m - Starting... (first startup may take longer) (tools will appear when ready)', |
|
|
); |
|
|
|
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/mcp schema', () => { |
|
|
it('should display tool schemas and descriptions', async () => { |
|
|
|
|
|
const mockMcpServers = { |
|
|
server1: { |
|
|
command: 'cmd1', |
|
|
description: 'This is a server description', |
|
|
}, |
|
|
}; |
|
|
|
|
|
|
|
|
vi.mocked(getMCPServerStatus).mockImplementation((serverName) => { |
|
|
if (serverName === 'server1') return MCPServerStatus.CONNECTED; |
|
|
return MCPServerStatus.DISCONNECTED; |
|
|
}); |
|
|
|
|
|
|
|
|
vi.mocked(getMCPDiscoveryState).mockReturnValue( |
|
|
MCPDiscoveryState.COMPLETED, |
|
|
); |
|
|
|
|
|
|
|
|
const mockServerTools = [ |
|
|
{ |
|
|
name: 'tool1', |
|
|
description: 'This is tool 1 description', |
|
|
schema: { |
|
|
parameters: [{ name: 'param1', type: 'string' }], |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
name: 'tool2', |
|
|
description: 'This is tool 2 description', |
|
|
schema: { |
|
|
parameters: [{ name: 'param2', type: 'number' }], |
|
|
}, |
|
|
}, |
|
|
]; |
|
|
|
|
|
mockConfig = { |
|
|
...mockConfig, |
|
|
getToolRegistry: vi.fn().mockResolvedValue({ |
|
|
getToolsByServer: vi.fn().mockReturnValue(mockServerTools), |
|
|
}), |
|
|
getMcpServers: vi.fn().mockReturnValue(mockMcpServers), |
|
|
} as unknown as Config; |
|
|
|
|
|
const { handleSlashCommand } = getProcessor(true); |
|
|
let commandResult: SlashCommandActionReturn | boolean = false; |
|
|
await act(async () => { |
|
|
commandResult = await handleSlashCommand('/mcp schema'); |
|
|
}); |
|
|
|
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.INFO, |
|
|
text: expect.stringContaining('Configured MCP servers:'), |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
|
|
|
const message = mockAddItem.mock.calls[1][0].text; |
|
|
|
|
|
|
|
|
expect(message).toContain('Ready (2 tools)'); |
|
|
expect(message).toContain('This is a server description'); |
|
|
|
|
|
|
|
|
expect(message).toContain('tool 1 description'); |
|
|
expect(message).toContain('param1'); |
|
|
expect(message).toContain('string'); |
|
|
expect(message).toContain('tool 2 description'); |
|
|
expect(message).toContain('param2'); |
|
|
expect(message).toContain('number'); |
|
|
|
|
|
expect(commandResult).toBe(true); |
|
|
}); |
|
|
}); |
|
|
|
|
|
describe('/compress command', () => { |
|
|
it('should call tryCompressChat(true)', async () => { |
|
|
const hook = getProcessorHook(); |
|
|
mockTryCompressChat.mockImplementationOnce(async (force?: boolean) => { |
|
|
expect(force).toBe(true); |
|
|
await act(async () => { |
|
|
hook.rerender(); |
|
|
}); |
|
|
expect(hook.result.current.pendingHistoryItems).toContainEqual({ |
|
|
type: MessageType.COMPRESSION, |
|
|
compression: { |
|
|
isPending: true, |
|
|
originalTokenCount: null, |
|
|
newTokenCount: null, |
|
|
}, |
|
|
}); |
|
|
return { |
|
|
originalTokenCount: 100, |
|
|
newTokenCount: 50, |
|
|
}; |
|
|
}); |
|
|
|
|
|
await act(async () => { |
|
|
hook.result.current.handleSlashCommand('/compress'); |
|
|
}); |
|
|
await act(async () => { |
|
|
hook.rerender(); |
|
|
}); |
|
|
expect(hook.result.current.pendingHistoryItems).toEqual([]); |
|
|
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledWith(true); |
|
|
expect(mockAddItem).toHaveBeenNthCalledWith( |
|
|
2, |
|
|
expect.objectContaining({ |
|
|
type: MessageType.COMPRESSION, |
|
|
compression: { |
|
|
isPending: false, |
|
|
originalTokenCount: 100, |
|
|
newTokenCount: 50, |
|
|
}, |
|
|
}), |
|
|
expect.any(Number), |
|
|
); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|