| |
| |
| |
| |
| |
|
|
| vi.mock('../ui/commands/profileCommand.js', async () => { |
| const { CommandKind } = await import('../ui/commands/types.js'); |
| return { |
| profileCommand: { |
| name: 'profile', |
| description: 'Profile command', |
| kind: CommandKind.BUILT_IN, |
| }, |
| }; |
| }); |
|
|
| vi.mock('../ui/commands/aboutCommand.js', async () => { |
| const { CommandKind } = await import('../ui/commands/types.js'); |
| return { |
| aboutCommand: { |
| name: 'about', |
| description: 'About the CLI', |
| kind: CommandKind.BUILT_IN, |
| }, |
| }; |
| }); |
|
|
| vi.mock('../ui/commands/ideCommand.js', async () => { |
| const { CommandKind } = await import('../ui/commands/types.js'); |
| return { |
| ideCommand: vi.fn().mockResolvedValue({ |
| name: 'ide', |
| description: 'IDE command', |
| kind: CommandKind.BUILT_IN, |
| }), |
| }; |
| }); |
| vi.mock('../ui/commands/restoreCommand.js', () => ({ |
| restoreCommand: vi.fn(), |
| })); |
| vi.mock('../ui/commands/permissionsCommand.js', async () => { |
| const { CommandKind } = await import('../ui/commands/types.js'); |
| return { |
| permissionsCommand: { |
| name: 'permissions', |
| description: 'Permissions command', |
| kind: CommandKind.BUILT_IN, |
| }, |
| }; |
| }); |
|
|
| import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; |
| import { BuiltinCommandLoader } from './BuiltinCommandLoader.js'; |
| import { isNightly, type Config } from '@google/gemini-cli-core'; |
| import { CommandKind } from '../ui/commands/types.js'; |
|
|
| import { restoreCommand } from '../ui/commands/restoreCommand.js'; |
|
|
| vi.mock('@google/gemini-cli-core', async (importOriginal) => { |
| const actual = |
| await importOriginal<typeof import('@google/gemini-cli-core')>(); |
| return { |
| ...actual, |
| isNightly: vi.fn().mockResolvedValue(false), |
| }; |
| }); |
|
|
| vi.mock('../ui/commands/authCommand.js', () => ({ authCommand: {} })); |
| vi.mock('../ui/commands/agentsCommand.js', () => ({ |
| agentsCommand: { name: 'agents' }, |
| })); |
| vi.mock('../ui/commands/bugCommand.js', () => ({ bugCommand: {} })); |
| vi.mock('../ui/commands/bugMemoryCommand.js', () => ({ |
| bugMemoryCommand: { name: 'bug-memory' }, |
| })); |
| vi.mock('../ui/commands/chatCommand.js', () => ({ |
| chatCommand: { |
| name: 'chat', |
| subCommands: [ |
| { name: 'list' }, |
| { name: 'save' }, |
| { name: 'resume' }, |
| { name: 'delete' }, |
| { name: 'share' }, |
| { name: 'checkpoints', hidden: true, subCommands: [{ name: 'list' }] }, |
| ], |
| }, |
| debugCommand: { name: 'debug' }, |
| })); |
| vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: {} })); |
| vi.mock('../ui/commands/compressCommand.js', () => ({ compressCommand: {} })); |
| vi.mock('../ui/commands/corgiCommand.js', () => ({ corgiCommand: {} })); |
| vi.mock('../ui/commands/docsCommand.js', () => ({ docsCommand: {} })); |
| vi.mock('../ui/commands/editorCommand.js', () => ({ editorCommand: {} })); |
| vi.mock('../ui/commands/extensionsCommand.js', () => ({ |
| extensionsCommand: () => ({}), |
| })); |
| vi.mock('../ui/commands/helpCommand.js', () => ({ helpCommand: {} })); |
| vi.mock('../ui/commands/shortcutsCommand.js', () => ({ |
| shortcutsCommand: {}, |
| })); |
| vi.mock('../ui/commands/memoryCommand.js', () => ({ |
| memoryCommand: () => ({}), |
| })); |
| vi.mock('../ui/commands/modelCommand.js', () => ({ |
| modelCommand: { name: 'model' }, |
| })); |
| vi.mock('../ui/commands/privacyCommand.js', () => ({ privacyCommand: {} })); |
| vi.mock('../ui/commands/quitCommand.js', () => ({ quitCommand: {} })); |
| vi.mock('../ui/commands/resumeCommand.js', () => ({ |
| resumeCommand: { |
| name: 'resume', |
| subCommands: [ |
| { name: 'list' }, |
| { name: 'save' }, |
| { name: 'resume' }, |
| { name: 'delete' }, |
| { name: 'share' }, |
| { name: 'checkpoints', hidden: true, subCommands: [{ name: 'list' }] }, |
| ], |
| }, |
| })); |
| vi.mock('../ui/commands/statsCommand.js', () => ({ statsCommand: {} })); |
| vi.mock('../ui/commands/themeCommand.js', () => ({ themeCommand: {} })); |
| vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} })); |
| vi.mock('../ui/commands/skillsCommand.js', () => ({ |
| skillsCommand: { name: 'skills' }, |
| })); |
| vi.mock('../ui/commands/planCommand.js', async () => { |
| const { CommandKind } = await import('../ui/commands/types.js'); |
| return { |
| planCommand: { |
| name: 'plan', |
| description: 'Plan command', |
| kind: CommandKind.BUILT_IN, |
| }, |
| }; |
| }); |
|
|
| vi.mock('../ui/commands/mcpCommand.js', () => ({ |
| mcpCommand: { |
| name: 'mcp', |
| description: 'MCP command', |
| kind: 'BUILT_IN', |
| }, |
| })); |
|
|
| vi.mock('../ui/commands/upgradeCommand.js', () => ({ |
| upgradeCommand: { |
| name: 'upgrade', |
| description: 'Upgrade command', |
| kind: 'BUILT_IN', |
| }, |
| })); |
|
|
| describe('BuiltinCommandLoader', () => { |
| let mockConfig: Config; |
|
|
| const restoreCommandMock = restoreCommand as Mock; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
| mockConfig = { |
| getFolderTrust: vi.fn().mockReturnValue(true), |
| isPlanEnabled: vi.fn().mockReturnValue(true), |
| getEnableExtensionReloading: () => false, |
| getEnableHooks: () => false, |
| getEnableHooksUI: () => false, |
| getExtensionsEnabled: vi.fn().mockReturnValue(true), |
| isSkillsSupportEnabled: vi.fn().mockReturnValue(true), |
| isAgentsEnabled: vi.fn().mockReturnValue(false), |
| getMcpEnabled: vi.fn().mockReturnValue(true), |
| getSkillManager: vi.fn().mockReturnValue({ |
| getAllSkills: vi.fn().mockReturnValue([]), |
| isAdminEnabled: vi.fn().mockReturnValue(true), |
| }), |
| isVoiceModeEnabled: vi.fn().mockReturnValue(true), |
| getContentGeneratorConfig: vi.fn().mockReturnValue({ |
| authType: 'other', |
| }), |
| } as unknown as Config; |
|
|
| restoreCommandMock.mockReturnValue({ |
| name: 'restore', |
| description: 'Restore command', |
| kind: CommandKind.BUILT_IN, |
| }); |
| }); |
|
|
| it('should include upgrade command when authType is login_with_google', async () => { |
| const { AuthType } = await import('@google/gemini-cli-core'); |
| (mockConfig.getContentGeneratorConfig as Mock).mockReturnValue({ |
| authType: AuthType.LOGIN_WITH_GOOGLE, |
| }); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const upgradeCmd = commands.find((c) => c.name === 'upgrade'); |
| expect(upgradeCmd).toBeDefined(); |
| }); |
|
|
| it('should exclude upgrade command when authType is NOT login_with_google', async () => { |
| (mockConfig.getContentGeneratorConfig as Mock).mockReturnValue({ |
| authType: 'other', |
| }); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const upgradeCmd = commands.find((c) => c.name === 'upgrade'); |
| expect(upgradeCmd).toBeUndefined(); |
| }); |
|
|
| it('should correctly pass the config object to restore command factory', async () => { |
| const loader = new BuiltinCommandLoader(mockConfig); |
| await loader.loadCommands(new AbortController().signal); |
|
|
| |
| expect(restoreCommandMock).toHaveBeenCalledTimes(1); |
| expect(restoreCommandMock).toHaveBeenCalledWith(mockConfig); |
| }); |
|
|
| it('should filter out null command definitions returned by factories', async () => { |
| |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
|
|
| |
| const ideCmd = commands.find((c) => c.name === 'ide'); |
| expect(ideCmd).toBeDefined(); |
|
|
| |
| const aboutCmd = commands.find((c) => c.name === 'about'); |
| expect(aboutCmd).toBeDefined(); |
| }); |
|
|
| it('should handle a null config gracefully when calling factories', async () => { |
| const loader = new BuiltinCommandLoader(null); |
| await loader.loadCommands(new AbortController().signal); |
| |
| expect(restoreCommandMock).toHaveBeenCalledTimes(1); |
| expect(restoreCommandMock).toHaveBeenCalledWith(null); |
| }); |
|
|
| it('should return a list of all loaded commands', async () => { |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
|
|
| const aboutCmd = commands.find((c) => c.name === 'about'); |
| expect(aboutCmd).toBeDefined(); |
| expect(aboutCmd?.kind).toBe(CommandKind.BUILT_IN); |
|
|
| const ideCmd = commands.find((c) => c.name === 'ide'); |
| expect(ideCmd).toBeDefined(); |
|
|
| const mcpCmd = commands.find((c) => c.name === 'mcp'); |
| expect(mcpCmd).toBeDefined(); |
| }); |
|
|
| it('should include permissions command when folder trust is enabled', async () => { |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const permissionsCmd = commands.find((c) => c.name === 'permissions'); |
| expect(permissionsCmd).toBeDefined(); |
| }); |
|
|
| it('should exclude permissions command when folder trust is disabled', async () => { |
| (mockConfig.getFolderTrust as Mock).mockReturnValue(false); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const permissionsCmd = commands.find((c) => c.name === 'permissions'); |
| expect(permissionsCmd).toBeUndefined(); |
| }); |
|
|
| it('should include policies command when message bus integration is enabled', async () => { |
| const mockConfigWithMessageBus = { |
| |
| ...mockConfig, |
| getEnableHooks: () => false, |
| getMcpEnabled: () => true, |
| } as unknown as Config; |
| const loader = new BuiltinCommandLoader(mockConfigWithMessageBus); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const policiesCmd = commands.find((c) => c.name === 'policies'); |
| expect(policiesCmd).toBeDefined(); |
| }); |
|
|
| it('should include agents command when agents are enabled', async () => { |
| mockConfig.isAgentsEnabled = vi.fn().mockReturnValue(true); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const agentsCmd = commands.find((c) => c.name === 'agents'); |
| expect(agentsCmd).toBeDefined(); |
| }); |
|
|
| it('should include plan command when plan mode is enabled', async () => { |
| (mockConfig.isPlanEnabled as Mock).mockReturnValue(true); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const planCmd = commands.find((c) => c.name === 'plan'); |
| expect(planCmd).toBeDefined(); |
| }); |
|
|
| it('should exclude plan command when plan mode is disabled', async () => { |
| (mockConfig.isPlanEnabled as Mock).mockReturnValue(false); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const planCmd = commands.find((c) => c.name === 'plan'); |
| expect(planCmd).toBeUndefined(); |
| }); |
|
|
| it('should exclude agents command when agents are disabled', async () => { |
| mockConfig.isAgentsEnabled = vi.fn().mockReturnValue(false); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const agentsCmd = commands.find((c) => c.name === 'agents'); |
| expect(agentsCmd).toBeUndefined(); |
| }); |
|
|
| describe('chat debug command', () => { |
| it('should NOT add debug subcommand to chat/resume commands if not a nightly build', async () => { |
| vi.mocked(isNightly).mockResolvedValue(false); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
|
|
| const chatCmd = commands.find((c) => c.name === 'chat'); |
| expect(chatCmd?.subCommands).toBeDefined(); |
| const hasDebug = chatCmd!.subCommands!.some((c) => c.name === 'debug'); |
| expect(hasDebug).toBe(false); |
|
|
| const resumeCmd = commands.find((c) => c.name === 'resume'); |
| const resumeHasDebug = |
| resumeCmd?.subCommands?.some((c) => c.name === 'debug') ?? false; |
| expect(resumeHasDebug).toBe(false); |
|
|
| const chatCheckpointsCmd = chatCmd?.subCommands?.find( |
| (c) => c.name === 'checkpoints', |
| ); |
| const chatCheckpointHasDebug = |
| chatCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ?? |
| false; |
| expect(chatCheckpointHasDebug).toBe(false); |
|
|
| const resumeCheckpointsCmd = resumeCmd?.subCommands?.find( |
| (c) => c.name === 'checkpoints', |
| ); |
| const resumeCheckpointHasDebug = |
| resumeCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ?? |
| false; |
| expect(resumeCheckpointHasDebug).toBe(false); |
| }); |
|
|
| it('should add debug subcommand to chat/resume commands if it is a nightly build', async () => { |
| vi.mocked(isNightly).mockResolvedValue(true); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
|
|
| const chatCmd = commands.find((c) => c.name === 'chat'); |
| expect(chatCmd?.subCommands).toBeDefined(); |
| const hasDebug = chatCmd!.subCommands!.some((c) => c.name === 'debug'); |
| expect(hasDebug).toBe(true); |
|
|
| const resumeCmd = commands.find((c) => c.name === 'resume'); |
| const resumeHasDebug = |
| resumeCmd?.subCommands?.some((c) => c.name === 'debug') ?? false; |
| expect(resumeHasDebug).toBe(true); |
|
|
| const chatCheckpointsCmd = chatCmd?.subCommands?.find( |
| (c) => c.name === 'checkpoints', |
| ); |
| const chatCheckpointHasDebug = |
| chatCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ?? |
| false; |
| expect(chatCheckpointHasDebug).toBe(true); |
|
|
| const resumeCheckpointsCmd = resumeCmd?.subCommands?.find( |
| (c) => c.name === 'checkpoints', |
| ); |
| const resumeCheckpointHasDebug = |
| resumeCheckpointsCmd?.subCommands?.some((c) => c.name === 'debug') ?? |
| false; |
| expect(resumeCheckpointHasDebug).toBe(true); |
| }); |
| }); |
| }); |
|
|
| describe('BuiltinCommandLoader profile', () => { |
| let mockConfig: Config; |
|
|
| beforeEach(() => { |
| vi.resetModules(); |
| mockConfig = { |
| getFolderTrust: vi.fn().mockReturnValue(false), |
| isPlanEnabled: vi.fn().mockReturnValue(true), |
| getCheckpointingEnabled: () => false, |
| getEnableExtensionReloading: () => false, |
| getEnableHooks: () => false, |
| getEnableHooksUI: () => false, |
| getExtensionsEnabled: vi.fn().mockReturnValue(true), |
| isSkillsSupportEnabled: vi.fn().mockReturnValue(true), |
| isAgentsEnabled: vi.fn().mockReturnValue(false), |
| getMcpEnabled: vi.fn().mockReturnValue(true), |
| getSkillManager: vi.fn().mockReturnValue({ |
| getAllSkills: vi.fn().mockReturnValue([]), |
| isAdminEnabled: vi.fn().mockReturnValue(true), |
| }), |
| isVoiceModeEnabled: vi.fn().mockReturnValue(true), |
| getContentGeneratorConfig: vi.fn().mockReturnValue({ |
| authType: 'other', |
| }), |
| } as unknown as Config; |
| }); |
|
|
| it('should not include profile command when isDevelopment is false', async () => { |
| process.env['NODE_ENV'] = 'production'; |
| const { BuiltinCommandLoader } = await import('./BuiltinCommandLoader.js'); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const profileCmd = commands.find((c) => c.name === 'profile'); |
| expect(profileCmd).toBeUndefined(); |
| }); |
|
|
| it('should include profile command when isDevelopment is true', async () => { |
| process.env['NODE_ENV'] = 'development'; |
| const { BuiltinCommandLoader } = await import('./BuiltinCommandLoader.js'); |
| const loader = new BuiltinCommandLoader(mockConfig); |
| const commands = await loader.loadCommands(new AbortController().signal); |
| const profileCmd = commands.find((c) => c.name === 'profile'); |
| expect(profileCmd).toBeDefined(); |
| }); |
| }); |
|
|