mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 03:24:42 -07:00
feat(cli): implement dot-prefixing for slash command conflicts (#20979)
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Helper to find and invoke the registered conflict event listener.
|
||||
*/
|
||||
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);
|
||||
|
||||
// Should emit two feedbacks (one for each unique command name)
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user