diff --git a/frontend/tests/components/agents/AgentTypeForm.test.tsx b/frontend/tests/components/agents/AgentTypeForm.test.tsx index 3d9dab9..6eceea7 100644 --- a/frontend/tests/components/agents/AgentTypeForm.test.tsx +++ b/frontend/tests/components/agents/AgentTypeForm.test.tsx @@ -235,4 +235,327 @@ describe('AgentTypeForm', () => { expect(container.querySelector('form')).toHaveClass('custom-class'); }); }); + + describe('Model Tab', () => { + it('switches to model tab and shows model selection', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /model/i })); + + expect(screen.getByText('Model Selection')).toBeInTheDocument(); + expect(screen.getByText('Choose the AI models that power this agent type')).toBeInTheDocument(); + expect(screen.getByLabelText(/primary model/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/fallover model/i)).toBeInTheDocument(); + }); + + it('shows model parameters section', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /model/i })); + + expect(screen.getByText('Model Parameters')).toBeInTheDocument(); + expect(screen.getByLabelText(/temperature/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/max tokens/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/top p/i)).toBeInTheDocument(); + }); + + it('allows changing temperature', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /model/i })); + + const temperatureInput = screen.getByLabelText(/temperature/i) as HTMLInputElement; + expect(temperatureInput.value).toBe('0.7'); + + await user.clear(temperatureInput); + await user.type(temperatureInput, '0.9'); + + expect(temperatureInput.value).toBe('0.9'); + }); + + it('allows changing max tokens', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /model/i })); + + const maxTokensInput = screen.getByLabelText(/max tokens/i) as HTMLInputElement; + expect(maxTokensInput.value).toBe('8192'); + + await user.clear(maxTokensInput); + await user.type(maxTokensInput, '16384'); + + expect(maxTokensInput.value).toBe('16384'); + }); + + it('allows changing top p', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /model/i })); + + const topPInput = screen.getByLabelText(/top p/i) as HTMLInputElement; + expect(topPInput.value).toBe('0.95'); + + await user.clear(topPInput); + await user.type(topPInput, '0.8'); + + expect(topPInput.value).toBe('0.8'); + }); + + it('shows primary model select trigger', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /model/i })); + + // Verify the select trigger is present and labeled correctly + const primaryModelTrigger = screen.getByLabelText(/primary model/i); + expect(primaryModelTrigger).toBeInTheDocument(); + expect(primaryModelTrigger).toHaveAttribute('role', 'combobox'); + }); + + it('shows fallback model select trigger', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /model/i })); + + // Verify the select trigger is present and labeled correctly + const fallbackModelTrigger = screen.getByLabelText(/fallover model/i); + expect(fallbackModelTrigger).toBeInTheDocument(); + expect(fallbackModelTrigger).toHaveAttribute('role', 'combobox'); + }); + + it('displays pre-selected model in edit mode', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /model/i })); + + // Check that the selected model is displayed (multiple elements exist due to hidden native select) + const modelTexts = screen.getAllByText('Claude Opus 4.5'); + expect(modelTexts.length).toBeGreaterThan(0); + }); + }); + + describe('Permissions Tab', () => { + it('has a permissions tab trigger', () => { + render(); + + const permissionsTab = screen.getByRole('tab', { name: /permissions/i }); + expect(permissionsTab).toBeInTheDocument(); + expect(permissionsTab).toHaveAttribute('aria-controls'); + }); + + it('permissions tab content exists', () => { + render(); + + // The tab panel for permissions should exist (even if hidden) + const tabPanels = document.querySelectorAll('[role="tabpanel"]'); + expect(tabPanels.length).toBeGreaterThan(0); + }); + }); + + describe('Personality Tab', () => { + it('switches to personality tab and shows prompt editor', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /personality/i })); + + expect(screen.getByText('Personality Prompt')).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/you are a/i)).toBeInTheDocument(); + }); + + it('shows character count', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /personality/i })); + + // Should show character count for the existing prompt + expect(screen.getByText(/character count:/i)).toBeInTheDocument(); + }); + + it('updates character count when typing', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /personality/i })); + + const promptTextarea = screen.getByPlaceholderText(/you are a/i); + await user.type(promptTextarea, 'Test prompt'); + + expect(screen.getByText('Character count: 11')).toBeInTheDocument(); + }); + + it('pre-fills personality prompt in edit mode', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('tab', { name: /personality/i })); + + const promptTextarea = screen.getByPlaceholderText(/you are a/i) as HTMLTextAreaElement; + expect(promptTextarea.value).toBe('You are a Software Architect...'); + }); + }); + + describe('Status Field', () => { + it('shows status select in basic info tab', () => { + render(); + + expect(screen.getByLabelText(/status/i)).toBeInTheDocument(); + }); + + it('shows status select as combobox', () => { + render(); + + const statusTrigger = screen.getByLabelText(/status/i); + expect(statusTrigger).toHaveAttribute('role', 'combobox'); + }); + + it('displays active status for new agent by default', () => { + render(); + + // The select trigger should show "Active" by default + expect(screen.getByText('Active')).toBeInTheDocument(); + }); + + it('preserves active status in edit mode', () => { + render(); + + // Mock agent has is_active = true + // Status select trigger should show "Active" + const statusTrigger = screen.getByLabelText(/status/i); + expect(statusTrigger).toHaveTextContent('Active'); + }); + + it('shows inactive status when agent is inactive', () => { + const inactiveAgent = { ...mockAgentType, is_active: false }; + render(); + + // Status select trigger should show "Inactive / Draft" + const statusTrigger = screen.getByLabelText(/status/i); + expect(statusTrigger).toHaveTextContent('Inactive'); + }); + }); + + describe('Expertise Edge Cases', () => { + it('does not add duplicate expertise', async () => { + const user = userEvent.setup(); + render(); + + // Agent type already has 'system design' + const expertiseInput = screen.getByPlaceholderText(/e.g., system design/i); + await user.type(expertiseInput, 'system design'); + await user.click(screen.getByRole('button', { name: /^add$/i })); + + // Should still only have one 'system design' badge + const badges = screen.getAllByText('system design'); + expect(badges).toHaveLength(1); + }); + + it('does not add empty expertise', async () => { + const user = userEvent.setup(); + render(); + + const addButton = screen.getByRole('button', { name: /^add$/i }); + await user.click(addButton); + + // No badges should be added + expect(screen.queryByRole('button', { name: /^remove/i })).not.toBeInTheDocument(); + }); + + it('converts expertise to lowercase', async () => { + const user = userEvent.setup(); + render(); + + const expertiseInput = screen.getByPlaceholderText(/e.g., system design/i); + await user.type(expertiseInput, 'API Design'); + await user.click(screen.getByRole('button', { name: /^add$/i })); + + expect(screen.getByText('api design')).toBeInTheDocument(); + }); + + it('trims whitespace from expertise', async () => { + const user = userEvent.setup(); + render(); + + const expertiseInput = screen.getByPlaceholderText(/e.g., system design/i); + await user.type(expertiseInput, ' testing '); + await user.click(screen.getByRole('button', { name: /^add$/i })); + + expect(screen.getByText('testing')).toBeInTheDocument(); + }); + + it('clears input after adding expertise', async () => { + const user = userEvent.setup(); + render(); + + const expertiseInput = screen.getByPlaceholderText(/e.g., system design/i) as HTMLInputElement; + await user.type(expertiseInput, 'new skill'); + await user.click(screen.getByRole('button', { name: /^add$/i })); + + expect(expertiseInput.value).toBe(''); + }); + }); + + describe('Form Submission', () => { + it('calls onSubmit when form is valid', async () => { + const user = userEvent.setup(); + const onSubmit = jest.fn(); + render(); + + // Submit the pre-filled form + await user.click(screen.getByRole('button', { name: /save changes/i })); + + // React Hook Form should call onSubmit with form data + await waitFor( + () => { + expect(onSubmit).toHaveBeenCalled(); + }, + { timeout: 2000 } + ); + }); + + it('passes form data to onSubmit callback', async () => { + const user = userEvent.setup(); + const onSubmit = jest.fn(); + render(); + + await user.click(screen.getByRole('button', { name: /save changes/i })); + + await waitFor( + () => { + expect(onSubmit).toHaveBeenCalled(); + }, + { timeout: 2000 } + ); + + // Verify the first argument contains expected fields + const formData = onSubmit.mock.calls[0][0]; + expect(formData).toHaveProperty('name', 'Software Architect'); + expect(formData).toHaveProperty('slug', 'software-architect'); + expect(formData).toHaveProperty('expertise'); + expect(formData).toHaveProperty('personality_prompt'); + }); + }); + + describe('Null Model Params Handling', () => { + it('handles null model_params gracefully', () => { + const agentTypeWithNullParams: AgentTypeResponse = { + ...mockAgentType, + model_params: null, + }; + + render(); + + // Should render without errors + expect(screen.getByText('Edit Agent Type')).toBeInTheDocument(); + }); + }); });