| |
| |
| |
| |
| |
|
|
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
| import { SlashCommandConflictHandler } from './SlashCommandConflictHandler.js'; |
| import { |
| coreEvents, |
| CoreEvent, |
| type SlashCommandConflictsPayload, |
| type SlashCommandConflict, |
| } from '@google/gemini-cli-core'; |
| import { CommandKind } from '../ui/commands/types.js'; |
|
|
| vi.mock('@google/gemini-cli-core', async (importOriginal) => { |
| const actual = |
| await importOriginal<typeof import('@google/gemini-cli-core')>(); |
| return { |
| ...actual, |
| coreEvents: { |
| on: vi.fn(), |
| off: vi.fn(), |
| emitFeedback: vi.fn(), |
| }, |
| }; |
| }); |
|
|
| describe('SlashCommandConflictHandler', () => { |
| let handler: SlashCommandConflictHandler; |
|
|
| |
| |
| |
| const simulateEvent = (conflicts: SlashCommandConflict[]) => { |
| const callback = vi |
| .mocked(coreEvents.on) |
| .mock.calls.find( |
| (call) => call[0] === CoreEvent.SlashCommandConflicts, |
| )![1] as (payload: SlashCommandConflictsPayload) => void; |
| callback({ conflicts }); |
| }; |
|
|
| beforeEach(() => { |
| vi.useFakeTimers(); |
| handler = new SlashCommandConflictHandler(); |
| handler.start(); |
| }); |
|
|
| afterEach(() => { |
| handler.stop(); |
| vi.clearAllMocks(); |
| vi.useRealTimers(); |
| }); |
|
|
| it('should listen for conflict events on start', () => { |
| expect(coreEvents.on).toHaveBeenCalledWith( |
| CoreEvent.SlashCommandConflicts, |
| expect.any(Function), |
| ); |
| }); |
|
|
| it('should display a descriptive message for a single extension conflict', () => { |
| simulateEvent([ |
| { |
| name: 'deploy', |
| renamedTo: 'firebase.deploy', |
| loserExtensionName: 'firebase', |
| loserKind: CommandKind.EXTENSION_FILE, |
| winnerKind: CommandKind.BUILT_IN, |
| }, |
| ]); |
|
|
| vi.advanceTimersByTime(600); |
|
|
| expect(coreEvents.emitFeedback).toHaveBeenCalledWith( |
| 'info', |
| "Extension 'firebase' command '/deploy' was renamed to '/firebase.deploy' because it conflicts with built-in command.", |
| ); |
| }); |
|
|
| it('should display a descriptive message for a single MCP conflict', () => { |
| simulateEvent([ |
| { |
| name: 'pickle', |
| renamedTo: 'test-server.pickle', |
| loserMcpServerName: 'test-server', |
| loserKind: CommandKind.MCP_PROMPT, |
| winnerExtensionName: 'pickle-rick', |
| winnerKind: CommandKind.EXTENSION_FILE, |
| }, |
| ]); |
|
|
| vi.advanceTimersByTime(600); |
|
|
| expect(coreEvents.emitFeedback).toHaveBeenCalledWith( |
| 'info', |
| "MCP server 'test-server' command '/pickle' was renamed to '/test-server.pickle' because it conflicts with extension 'pickle-rick' command.", |
| ); |
| }); |
|
|
| it('should group multiple conflicts for the same command name', () => { |
| simulateEvent([ |
| { |
| name: 'launch', |
| renamedTo: 'user.launch', |
| loserKind: CommandKind.USER_FILE, |
| winnerKind: CommandKind.WORKSPACE_FILE, |
| }, |
| { |
| name: 'launch', |
| renamedTo: 'workspace.launch', |
| loserKind: CommandKind.WORKSPACE_FILE, |
| winnerKind: CommandKind.USER_FILE, |
| }, |
| ]); |
|
|
| vi.advanceTimersByTime(600); |
|
|
| expect(coreEvents.emitFeedback).toHaveBeenCalledWith( |
| 'info', |
| `Conflicts detected for command '/launch': |
| - User command '/launch' was renamed to '/user.launch' |
| - Workspace command '/launch' was renamed to '/workspace.launch'`, |
| ); |
| }); |
|
|
| it('should debounce multiple events within the flush window', () => { |
| simulateEvent([ |
| { |
| name: 'a', |
| renamedTo: 'user.a', |
| loserKind: CommandKind.USER_FILE, |
| winnerKind: CommandKind.BUILT_IN, |
| }, |
| ]); |
|
|
| vi.advanceTimersByTime(200); |
|
|
| simulateEvent([ |
| { |
| name: 'b', |
| renamedTo: 'user.b', |
| loserKind: CommandKind.USER_FILE, |
| winnerKind: CommandKind.BUILT_IN, |
| }, |
| ]); |
|
|
| vi.advanceTimersByTime(600); |
|
|
| |
| expect(coreEvents.emitFeedback).toHaveBeenCalledTimes(2); |
| }); |
|
|
| it('should deduplicate already notified conflicts', () => { |
| const conflict = { |
| name: 'deploy', |
| renamedTo: 'firebase.deploy', |
| loserExtensionName: 'firebase', |
| loserKind: CommandKind.EXTENSION_FILE, |
| winnerKind: CommandKind.BUILT_IN, |
| }; |
|
|
| simulateEvent([conflict]); |
| vi.advanceTimersByTime(600); |
| expect(coreEvents.emitFeedback).toHaveBeenCalledTimes(1); |
|
|
| vi.mocked(coreEvents.emitFeedback).mockClear(); |
|
|
| simulateEvent([conflict]); |
| vi.advanceTimersByTime(600); |
| expect(coreEvents.emitFeedback).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should display a descriptive message for a skill conflict', () => { |
| simulateEvent([ |
| { |
| name: 'chat', |
| renamedTo: 'google-workspace.chat', |
| loserExtensionName: 'google-workspace', |
| loserKind: CommandKind.SKILL, |
| winnerKind: CommandKind.BUILT_IN, |
| }, |
| ]); |
|
|
| vi.advanceTimersByTime(600); |
|
|
| expect(coreEvents.emitFeedback).toHaveBeenCalledWith( |
| 'info', |
| "Extension 'google-workspace' skill '/chat' was renamed to '/google-workspace.chat' because it conflicts with built-in command.", |
| ); |
| }); |
| }); |
|
|