import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import { ChatInput } from '@/pages/Chat/ChatInput'; const { agentsState, chatState, gatewayState } = vi.hoisted(() => ({ agentsState: { agents: [] as Array>, }, chatState: { currentAgentId: 'main', }, gatewayState: { status: { state: 'running', port: 18789 }, }, })); vi.mock('@/stores/agents', () => ({ useAgentsStore: (selector: (state: typeof agentsState) => unknown) => selector(agentsState), })); vi.mock('@/stores/chat', () => ({ useChatStore: (selector: (state: typeof chatState) => unknown) => selector(chatState), })); vi.mock('@/stores/gateway', () => ({ useGatewayStore: (selector: (state: typeof gatewayState) => unknown) => selector(gatewayState), })); vi.mock('@/lib/host-api', () => ({ hostApiFetch: vi.fn(), })); vi.mock('@/lib/api-client', () => ({ invokeIpc: vi.fn(), })); function translate(key: string, vars?: Record): string { switch (key) { case 'composer.attachFiles': return 'Attach files'; case 'composer.pickAgent': return 'Choose agent'; case 'composer.clearTarget': return 'Clear target agent'; case 'composer.targetChip': return `@${String(vars?.agent ?? '')}`; case 'composer.agentPickerTitle': return 'Route the next message to another agent'; case 'composer.gatewayDisconnectedPlaceholder': return 'Gateway not connected...'; case 'composer.send': return 'Send'; case 'composer.stop': return 'Stop'; case 'composer.gatewayConnected': return 'connected'; case 'composer.gatewayStatus': return `gateway ${String(vars?.state ?? '')} | port: ${String(vars?.port ?? '')} ${String(vars?.pid ?? '')}`.trim(); case 'composer.retryFailedAttachments': return 'Retry failed attachments'; default: return key; } } vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: translate, }), })); describe('ChatInput agent targeting', () => { beforeEach(() => { agentsState.agents = []; chatState.currentAgentId = 'main'; gatewayState.status = { state: 'running', port: 18789 }; }); it('hides the @agent picker when only one agent is configured', () => { agentsState.agents = [ { id: 'main', name: 'Main', isDefault: true, modelDisplay: 'MiniMax', inheritedModel: true, workspace: '~/.openclaw/workspace', agentDir: '~/.openclaw/agents/main/agent', mainSessionKey: 'agent:main:main', channelTypes: [], }, ]; render(); expect(screen.queryByTitle('Choose agent')).not.toBeInTheDocument(); }); it('lets the user select an agent target and sends it with the message', () => { const onSend = vi.fn(); agentsState.agents = [ { id: 'main', name: 'Main', isDefault: true, modelDisplay: 'MiniMax', inheritedModel: true, workspace: '~/.openclaw/workspace', agentDir: '~/.openclaw/agents/main/agent', mainSessionKey: 'agent:main:main', channelTypes: [], }, { id: 'research', name: 'Research', isDefault: false, modelDisplay: 'Claude', inheritedModel: false, workspace: '~/.openclaw/workspace-research', agentDir: '~/.openclaw/agents/research/agent', mainSessionKey: 'agent:research:desk', channelTypes: [], }, ]; render(); fireEvent.click(screen.getByTitle('Choose agent')); fireEvent.click(screen.getByText('Research')); expect(screen.getByText('@Research')).toBeInTheDocument(); fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Hello direct agent' } }); fireEvent.click(screen.getByTitle('Send')); expect(onSend).toHaveBeenCalledWith('Hello direct agent', undefined, 'research'); }); });