Files
gemini-cli/packages/cli/src/services/SlashCommandResolver.test.ts

178 lines
5.9 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { SlashCommandResolver } from './SlashCommandResolver.js';
import { CommandKind, type SlashCommand } from '../ui/commands/types.js';
const createMockCommand = (name: string, kind: CommandKind): SlashCommand => ({
name,
description: `Description for ${name}`,
kind,
action: vi.fn(),
});
describe('SlashCommandResolver', () => {
describe('resolve', () => {
it('should return all commands when there are no conflicts', () => {
const cmdA = createMockCommand('a', CommandKind.BUILT_IN);
const cmdB = createMockCommand('b', CommandKind.USER_FILE);
const { finalCommands, conflicts } = SlashCommandResolver.resolve([
cmdA,
cmdB,
]);
expect(finalCommands).toHaveLength(2);
expect(conflicts).toHaveLength(0);
});
it('should rename extension commands when they conflict with built-in', () => {
const builtin = createMockCommand('deploy', CommandKind.BUILT_IN);
const extension = {
...createMockCommand('deploy', CommandKind.EXTENSION_FILE),
extensionName: 'firebase',
};
const { finalCommands, conflicts } = SlashCommandResolver.resolve([
builtin,
extension,
]);
expect(finalCommands.map((c) => c.name)).toContain('deploy');
expect(finalCommands.map((c) => c.name)).toContain('firebase.deploy');
expect(conflicts).toHaveLength(1);
});
it('should prefix both user and workspace commands when they conflict', () => {
const userCmd = createMockCommand('sync', CommandKind.USER_FILE);
const workspaceCmd = createMockCommand(
'sync',
CommandKind.WORKSPACE_FILE,
);
const { finalCommands, conflicts } = SlashCommandResolver.resolve([
userCmd,
workspaceCmd,
]);
const names = finalCommands.map((c) => c.name);
expect(names).not.toContain('sync');
expect(names).toContain('user.sync');
expect(names).toContain('workspace.sync');
expect(conflicts).toHaveLength(1);
expect(conflicts[0].losers).toHaveLength(2); // Both are considered losers
});
it('should prefix file commands but keep built-in names during conflicts', () => {
const builtin = createMockCommand('help', CommandKind.BUILT_IN);
const user = createMockCommand('help', CommandKind.USER_FILE);
const { finalCommands } = SlashCommandResolver.resolve([builtin, user]);
const names = finalCommands.map((c) => c.name);
expect(names).toContain('help');
expect(names).toContain('user.help');
});
it('should prefix both commands when MCP and user file conflict', () => {
const mcp = {
...createMockCommand('test', CommandKind.MCP_PROMPT),
mcpServerName: 'test-server',
};
const user = createMockCommand('test', CommandKind.USER_FILE);
const { finalCommands } = SlashCommandResolver.resolve([mcp, user]);
const names = finalCommands.map((c) => c.name);
expect(names).not.toContain('test');
expect(names).toContain('test-server.test');
expect(names).toContain('user.test');
});
it('should prefix MCP commands with server name when they conflict with built-in', () => {
const builtin = createMockCommand('help', CommandKind.BUILT_IN);
const mcp = {
...createMockCommand('help', CommandKind.MCP_PROMPT),
mcpServerName: 'test-server',
};
const { finalCommands } = SlashCommandResolver.resolve([builtin, mcp]);
const names = finalCommands.map((c) => c.name);
expect(names).toContain('help');
expect(names).toContain('test-server.help');
});
it('should prefix both MCP commands when they conflict with each other', () => {
const mcp1 = {
...createMockCommand('test', CommandKind.MCP_PROMPT),
mcpServerName: 'server1',
};
const mcp2 = {
...createMockCommand('test', CommandKind.MCP_PROMPT),
mcpServerName: 'server2',
};
const { finalCommands } = SlashCommandResolver.resolve([mcp1, mcp2]);
const names = finalCommands.map((c) => c.name);
expect(names).not.toContain('test');
expect(names).toContain('server1.test');
expect(names).toContain('server2.test');
});
it('should favor the last built-in command silently during conflicts', () => {
const builtin1 = {
...createMockCommand('help', CommandKind.BUILT_IN),
description: 'first',
};
const builtin2 = {
...createMockCommand('help', CommandKind.BUILT_IN),
description: 'second',
};
const { finalCommands } = SlashCommandResolver.resolve([
builtin1,
builtin2,
]);
expect(finalCommands).toHaveLength(1);
expect(finalCommands[0].description).toBe('second');
});
it('should fallback to numeric suffixes when both prefix and kind-based prefix are missing', () => {
const cmd1 = createMockCommand('test', CommandKind.BUILT_IN);
const cmd2 = {
...createMockCommand('test', 'unknown' as CommandKind),
};
const { finalCommands } = SlashCommandResolver.resolve([cmd1, cmd2]);
const names = finalCommands.map((c) => c.name);
expect(names).toContain('test');
expect(names).toContain('test1');
});
it('should apply numeric suffixes when renames also conflict', () => {
const user1 = createMockCommand('deploy', CommandKind.USER_FILE);
const user2 = createMockCommand('gcp.deploy', CommandKind.USER_FILE);
const extension = {
...createMockCommand('deploy', CommandKind.EXTENSION_FILE),
extensionName: 'gcp',
};
const { finalCommands } = SlashCommandResolver.resolve([
user1,
user2,
extension,
]);
expect(finalCommands.find((c) => c.name === 'gcp.deploy1')).toBeDefined();
});
});
});