| |
| |
| |
| |
| |
|
|
| import { vi, describe, it, expect, beforeEach } from 'vitest'; |
| import { agentsCommand } from './agentsCommand.js'; |
| import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; |
| import type { Config } from '@google/gemini-cli-core'; |
| import type { LoadedSettings } from '../../config/settings.js'; |
| import { MessageType } from '../types.js'; |
| import { enableAgent, disableAgent } from '../../utils/agentSettings.js'; |
| import { renderAgentActionFeedback } from '../../utils/agentUtils.js'; |
|
|
| vi.mock('../../utils/agentSettings.js', () => ({ |
| enableAgent: vi.fn(), |
| disableAgent: vi.fn(), |
| })); |
|
|
| vi.mock('../../utils/agentUtils.js', () => ({ |
| renderAgentActionFeedback: vi.fn(), |
| })); |
|
|
| describe('agentsCommand', () => { |
| let mockContext: ReturnType<typeof createMockCommandContext>; |
| let mockConfig: { |
| getAgentRegistry: ReturnType<typeof vi.fn>; |
| config: Config; |
| }; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
|
|
| mockConfig = { |
| getAgentRegistry: vi.fn().mockReturnValue({ |
| getAllDefinitions: vi.fn().mockReturnValue([]), |
| getAllAgentNames: vi.fn().mockReturnValue([]), |
| reload: vi.fn(), |
| }), |
| get config() { |
| return this as unknown as Config; |
| }, |
| }; |
|
|
| mockContext = createMockCommandContext({ |
| services: { |
| agentContext: mockConfig as unknown as Config, |
| settings: { |
| workspace: { path: '/mock/path' }, |
| merged: { agents: { overrides: {} } }, |
| } as unknown as LoadedSettings, |
| }, |
| }); |
| }); |
|
|
| it('should show an error if config is not available', async () => { |
| const contextWithoutConfig = createMockCommandContext({ |
| services: { |
| agentContext: null, |
| }, |
| }); |
|
|
| const result = await agentsCommand.action!(contextWithoutConfig, ''); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Config not loaded.', |
| }); |
| }); |
|
|
| it('should show an error if agent registry is not available', async () => { |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue(undefined); |
|
|
| const result = await agentsCommand.action!(mockContext, ''); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Agent registry not found.', |
| }); |
| }); |
|
|
| it('should call addItem with correct agents list', async () => { |
| const mockAgents = [ |
| { |
| name: 'agent1', |
| displayName: 'Agent One', |
| description: 'desc1', |
| kind: 'local', |
| }, |
| { |
| name: 'agent2', |
| displayName: undefined, |
| description: 'desc2', |
| kind: 'remote', |
| }, |
| ]; |
| mockConfig.getAgentRegistry().getAllDefinitions.mockReturnValue(mockAgents); |
|
|
| await agentsCommand.action!(mockContext, ''); |
|
|
| expect(mockContext.ui.addItem).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| type: MessageType.AGENTS_LIST, |
| agents: mockAgents, |
| }), |
| ); |
| }); |
|
|
| it('should reload the agent registry when reload subcommand is called', async () => { |
| const reloadSpy = vi.fn().mockResolvedValue({ |
| totalLoaded: 3, |
| localCount: 2, |
| remoteCount: 1, |
| newAgents: ['new-agent'], |
| updatedAgents: ['updated-agent'], |
| deletedAgents: ['deleted-agent'], |
| errors: [], |
| }); |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ |
| reload: reloadSpy, |
| }); |
|
|
| const reloadCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'reload', |
| ); |
| expect(reloadCommand).toBeDefined(); |
|
|
| const result = (await reloadCommand!.action!(mockContext, '')) as { |
| type: 'message'; |
| content: string; |
| }; |
|
|
| expect(reloadSpy).toHaveBeenCalled(); |
| expect(mockContext.ui.addItem).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| type: MessageType.INFO, |
| text: 'Reloading agent registry...', |
| }), |
| ); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: expect.stringContaining('Agents reloaded successfully:'), |
| }); |
| expect(result.content).toContain('- Total: 3 (2 local, 1 remote)'); |
| expect(result.content).toContain('- New: new-agent'); |
| expect(result.content).toContain('- Updated: updated-agent'); |
| expect(result.content).toContain('- Deleted: deleted-agent'); |
| expect(result.content).toContain( |
| 'Run /agents list to see all available agents.', |
| ); |
| }); |
|
|
| it('should show "reloaded with errors" if errors occurred during reload', async () => { |
| const reloadSpy = vi.fn().mockResolvedValue({ |
| totalLoaded: 1, |
| localCount: 1, |
| remoteCount: 0, |
| newAgents: [], |
| updatedAgents: [], |
| deletedAgents: [], |
| errors: ['Some error'], |
| }); |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ |
| reload: reloadSpy, |
| }); |
|
|
| const reloadCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'reload', |
| ); |
|
|
| const result = (await reloadCommand!.action!(mockContext, '')) as { |
| type: 'message'; |
| content: string; |
| }; |
|
|
| expect(result.content).toContain('Agents reloaded with errors:'); |
| expect(result.content).toContain('- Errors: 1 encountered during reload'); |
| }); |
|
|
| it('should show an error if agent registry is not available during reload', async () => { |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue(undefined); |
|
|
| const reloadCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'reload', |
| ); |
| const result = await reloadCommand!.action!(mockContext, ''); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Agent registry not found.', |
| }); |
| }); |
|
|
| it('should enable an agent successfully', async () => { |
| const reloadSpy = vi.fn().mockResolvedValue(undefined); |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ |
| getAllAgentNames: vi.fn().mockReturnValue([]), |
| reload: reloadSpy, |
| }); |
| |
| mockContext.services.settings.merged.agents.overrides['test-agent'] = { |
| enabled: false, |
| }; |
|
|
| vi.mocked(enableAgent).mockReturnValue({ |
| status: 'success', |
| agentName: 'test-agent', |
| action: 'enable', |
| modifiedScopes: [], |
| alreadyInStateScopes: [], |
| }); |
| vi.mocked(renderAgentActionFeedback).mockReturnValue('Enabled test-agent.'); |
|
|
| const enableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'enable', |
| ); |
| expect(enableCommand).toBeDefined(); |
|
|
| const result = await enableCommand!.action!(mockContext, 'test-agent'); |
|
|
| expect(enableAgent).toHaveBeenCalledWith( |
| mockContext.services.settings, |
| 'test-agent', |
| ); |
| expect(reloadSpy).toHaveBeenCalled(); |
| expect(mockContext.ui.addItem).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| type: MessageType.INFO, |
| text: 'Enabling test-agent...', |
| }), |
| ); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: 'Enabled test-agent.', |
| }); |
| }); |
|
|
| it('should handle no-op when enabling an agent', async () => { |
| mockConfig |
| .getAgentRegistry() |
| .getAllAgentNames.mockReturnValue(['test-agent']); |
|
|
| const enableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'enable', |
| ); |
| const result = await enableCommand!.action!(mockContext, 'test-agent'); |
|
|
| expect(enableAgent).not.toHaveBeenCalled(); |
| expect(mockContext.ui.addItem).not.toHaveBeenCalled(); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: "Agent 'test-agent' is already enabled.", |
| }); |
| }); |
|
|
| it('should show usage error if no agent name provided for enable', async () => { |
| const enableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'enable', |
| ); |
| const result = await enableCommand!.action!(mockContext, ' '); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Usage: /agents enable <agent-name>', |
| }); |
| }); |
|
|
| it('should show an error if config is not available for enable', async () => { |
| const contextWithoutConfig = createMockCommandContext({ |
| services: { agentContext: null }, |
| }); |
| const enableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'enable', |
| ); |
| const result = await enableCommand!.action!(contextWithoutConfig, 'test'); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Config not loaded.', |
| }); |
| }); |
|
|
| it('should disable an agent successfully', async () => { |
| const reloadSpy = vi.fn().mockResolvedValue(undefined); |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ |
| getAllAgentNames: vi.fn().mockReturnValue(['test-agent']), |
| reload: reloadSpy, |
| }); |
| vi.mocked(disableAgent).mockReturnValue({ |
| status: 'success', |
| agentName: 'test-agent', |
| action: 'disable', |
| modifiedScopes: [], |
| alreadyInStateScopes: [], |
| }); |
| vi.mocked(renderAgentActionFeedback).mockReturnValue( |
| 'Disabled test-agent.', |
| ); |
|
|
| const disableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'disable', |
| ); |
| expect(disableCommand).toBeDefined(); |
|
|
| const result = await disableCommand!.action!(mockContext, 'test-agent'); |
|
|
| expect(disableAgent).toHaveBeenCalledWith( |
| mockContext.services.settings, |
| 'test-agent', |
| expect.anything(), |
| ); |
| expect(reloadSpy).toHaveBeenCalled(); |
| expect(mockContext.ui.addItem).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| type: MessageType.INFO, |
| text: 'Disabling test-agent...', |
| }), |
| ); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: 'Disabled test-agent.', |
| }); |
| }); |
|
|
| it('should show info message if agent is already disabled', async () => { |
| mockConfig.getAgentRegistry().getAllAgentNames.mockReturnValue([]); |
| mockContext.services.settings.merged.agents.overrides['test-agent'] = { |
| enabled: false, |
| }; |
|
|
| const disableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'disable', |
| ); |
| const result = await disableCommand!.action!(mockContext, 'test-agent'); |
|
|
| expect(disableAgent).not.toHaveBeenCalled(); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: "Agent 'test-agent' is already disabled.", |
| }); |
| }); |
|
|
| it('should show error if agent is not found when disabling', async () => { |
| mockConfig.getAgentRegistry().getAllAgentNames.mockReturnValue([]); |
|
|
| const disableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'disable', |
| ); |
| const result = await disableCommand!.action!(mockContext, 'test-agent'); |
|
|
| expect(disableAgent).not.toHaveBeenCalled(); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: "Agent 'test-agent' not found.", |
| }); |
| }); |
|
|
| it('should show usage error if no agent name provided for disable', async () => { |
| const disableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'disable', |
| ); |
| const result = await disableCommand!.action!(mockContext, ''); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Usage: /agents disable <agent-name>', |
| }); |
| }); |
|
|
| it('should show an error if config is not available for disable', async () => { |
| const contextWithoutConfig = createMockCommandContext({ |
| services: { agentContext: null }, |
| }); |
| const disableCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'disable', |
| ); |
| const result = await disableCommand!.action!(contextWithoutConfig, 'test'); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Config not loaded.', |
| }); |
| }); |
|
|
| describe('config sub-command', () => { |
| it('should return dialog action for a valid agent', async () => { |
| const mockDefinition = { |
| name: 'test-agent', |
| displayName: 'Test Agent', |
| description: 'test desc', |
| kind: 'local', |
| }; |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ |
| getDiscoveredDefinition: vi.fn().mockReturnValue(mockDefinition), |
| }); |
|
|
| const configCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'config', |
| ); |
| expect(configCommand).toBeDefined(); |
|
|
| const result = await configCommand!.action!(mockContext, 'test-agent'); |
|
|
| expect(result).toEqual({ |
| type: 'dialog', |
| dialog: 'agentConfig', |
| props: { |
| name: 'test-agent', |
| displayName: 'Test Agent', |
| definition: mockDefinition, |
| }, |
| }); |
| }); |
|
|
| it('should use name as displayName if displayName is missing', async () => { |
| const mockDefinition = { |
| name: 'test-agent', |
| description: 'test desc', |
| kind: 'local', |
| }; |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ |
| getDiscoveredDefinition: vi.fn().mockReturnValue(mockDefinition), |
| }); |
|
|
| const configCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'config', |
| ); |
| const result = await configCommand!.action!(mockContext, 'test-agent'); |
|
|
| expect(result).toEqual({ |
| type: 'dialog', |
| dialog: 'agentConfig', |
| props: { |
| name: 'test-agent', |
| displayName: 'test-agent', |
| definition: mockDefinition, |
| }, |
| }); |
| }); |
|
|
| it('should show error if agent is not found', async () => { |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ |
| getDiscoveredDefinition: vi.fn().mockReturnValue(undefined), |
| }); |
|
|
| const configCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'config', |
| ); |
| const result = await configCommand!.action!(mockContext, 'non-existent'); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: "Agent 'non-existent' not found.", |
| }); |
| }); |
|
|
| it('should show usage error if no agent name provided', async () => { |
| const configCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'config', |
| ); |
| const result = await configCommand!.action!(mockContext, ' '); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Usage: /agents config <agent-name>', |
| }); |
| }); |
|
|
| it('should show an error if config is not available', async () => { |
| const contextWithoutConfig = createMockCommandContext({ |
| services: { agentContext: null }, |
| }); |
| const configCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'config', |
| ); |
| const result = await configCommand!.action!(contextWithoutConfig, 'test'); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Config not loaded.', |
| }); |
| }); |
|
|
| it('should provide completions for discovered agents', async () => { |
| mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ |
| getAllDiscoveredAgentNames: vi |
| .fn() |
| .mockReturnValue(['agent1', 'agent2', 'other']), |
| }); |
|
|
| const configCommand = agentsCommand.subCommands?.find( |
| (cmd) => cmd.name === 'config', |
| ); |
| expect(configCommand?.completion).toBeDefined(); |
|
|
| const completions = await configCommand!.completion!(mockContext, 'age'); |
| expect(completions).toEqual(['agent1', 'agent2']); |
| }); |
| }); |
| }); |
|
|