import { describe, it, expect, vi, beforeEach } from 'vitest' import { act, fireEvent, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import { Settings } from '../pages/Settings' import { useAgentStore } from '../stores/agentStore' import { useSkillStore } from '../stores/skillStore' import { useSettingsStore } from '../stores/settingsStore' import { useSessionStore } from '../stores/sessionStore' import { SETTINGS_TAB_ID, useTabStore } from '../stores/tabStore' import { useUIStore } from '../stores/uiStore' vi.mock('../api/agents', () => ({ agentsApi: { list: vi.fn().mockResolvedValue({ activeAgents: [], allAgents: [] }), }, })) const noopFetch = vi.fn() vi.mock('../stores/providerStore', () => ({ useProviderStore: () => ({ providers: [], activeId: null, presets: [], isLoading: false, isPresetsLoading: false, fetchProviders: vi.fn(), fetchPresets: vi.fn(), deleteProvider: vi.fn(), activateProvider: vi.fn(), activateOfficial: vi.fn(), testProvider: vi.fn(), createProvider: vi.fn(), updateProvider: vi.fn(), testConfig: vi.fn(), }), })) vi.mock('../pages/AdapterSettings', () => ({ AdapterSettings: () =>
Adapter Settings Mock
, })) vi.mock('../components/chat/CodeViewer', () => ({ CodeViewer: ({ code }: { code: string }) =>
{code}
, })) const MOCK_AGENTS = [ { agentType: 'code-reviewer', description: 'Reviews code for quality and security', model: 'claude-sonnet-4-6', modelDisplay: 'claude-sonnet-4-6', tools: ['Read', 'Grep', 'Glob'], systemPrompt: '# Code Reviewer\n\nYou are an expert code reviewer.', color: 'blue', source: 'userSettings' as const, baseDir: '~/.claude/agents', isActive: true, }, { agentType: 'doc-writer', description: 'Writes technical documentation', model: 'claude-haiku-4-5', modelDisplay: 'claude-haiku-4-5', tools: ['Read'], systemPrompt: 'You write clear and concise docs.', color: 'green', source: 'built-in' as const, baseDir: 'built-in', isActive: true, }, { agentType: 'plain-agent', description: undefined, model: undefined, modelDisplay: 'inherit', tools: undefined, systemPrompt: undefined, color: undefined, source: 'projectSettings' as const, baseDir: '/workspace/project/.claude/agents', isActive: false, overriddenBy: 'userSettings' as const, }, { agentType: 'telegram:pairing', description: 'Plugin agent for Telegram pairing flows', model: 'inherit', modelDisplay: 'inherit', tools: ['Read'], systemPrompt: 'Pair Telegram access for the current workspace.', color: 'cyan', source: 'plugin' as const, baseDir: '/Users/test/.claude/plugins/cache/telegram', isActive: true, }, ] const MOCK_SKILL_DETAIL = { meta: { name: 'skill-docs', displayName: 'Skill Docs', description: 'A rich skill readme', source: 'user' as const, userInvocable: true, contentLength: 200, hasDirectory: true, }, tree: [ { name: 'SKILL.md', path: 'SKILL.md', type: 'file' as const }, { name: 'helper.ts', path: 'helper.ts', type: 'file' as const }, ], files: [ { path: 'SKILL.md', language: 'markdown', content: '# Heading\n\nParagraph with `inline code`.\n\n## Section\n\n- First item\n- Second item\n\n> Helpful quote', body: '# Heading\n\nParagraph with `inline code`.\n\n## Section\n\n- First item\n- Second item\n\n> Helpful quote', isEntry: true, frontmatter: { description: 'A rich skill readme', model: 'sonnet', }, }, { path: 'helper.ts', language: 'typescript', content: 'export const helper = true', isEntry: false, }, ], skillRoot: '/tmp/skill-docs', } function switchToAgentsTab() { fireEvent.click(screen.getByText('Agents')) } function switchToSkillsTab() { fireEvent.click(screen.getByText('Skills')) } describe('Settings > Agents tab', () => { beforeEach(() => { useSettingsStore.setState({ locale: 'en' }) useTabStore.setState({ activeTabId: 'session-1', tabs: [{ sessionId: 'session-1', title: 'Test', type: 'session', status: 'idle' }], }) useUIStore.setState({ pendingSettingsTab: null }) useSessionStore.setState({ sessions: [ { id: 'session-1', title: 'Test Session', createdAt: '', modifiedAt: '', messageCount: 0, projectPath: '/workspace/project', workDir: '/workspace/project', workDirExists: true, }, ], activeSessionId: 'session-1', isLoading: false, error: null, selectedProjects: [], availableProjects: [], fetchSessions: noopFetch, createSession: vi.fn(), deleteSession: vi.fn(), renameSession: vi.fn(), updateSessionTitle: vi.fn(), setActiveSession: vi.fn(), setSelectedProjects: vi.fn(), }) useAgentStore.setState({ activeAgents: [], allAgents: [], isLoading: false, error: null, selectedAgent: null, selectedAgentReturnTab: 'agents', fetchAgents: noopFetch, selectAgent: (agent) => useAgentStore.setState({ selectedAgent: agent }), }) useSkillStore.setState({ skills: [], selectedSkill: null, isLoading: false, isDetailLoading: false, error: null, fetchSkills: noopFetch, fetchSkillDetail: noopFetch, clearSelection: () => useSkillStore.setState({ selectedSkill: null }), }) }) it('renders the Agents tab button in sidebar', () => { render() expect(screen.getByText('Agents')).toBeInTheDocument() }) it('shows loading spinner when fetching agents', () => { useAgentStore.setState({ isLoading: true, allAgents: [], activeAgents: [], fetchAgents: noopFetch }) render() switchToAgentsTab() const spinner = document.querySelector('.animate-spin') expect(spinner).toBeInTheDocument() }) it('uses the active session workDir even when settings tab is focused', async () => { const fetchAgents = vi.fn() useAgentStore.setState({ allAgents: [], activeAgents: [], isLoading: false, fetchAgents, }) useTabStore.setState({ activeTabId: SETTINGS_TAB_ID, tabs: [{ sessionId: SETTINGS_TAB_ID, title: 'Settings', type: 'settings', status: 'idle' }], }) render() switchToAgentsTab() expect(fetchAgents).toHaveBeenCalledWith('/workspace/project') }) it('shows error state with retry button when API fails', () => { useAgentStore.setState({ allAgents: [], activeAgents: [], isLoading: false, error: 'Network error', fetchAgents: noopFetch }) render() switchToAgentsTab() expect(screen.getByText('Network error')).toBeInTheDocument() expect(screen.getByText('Retry')).toBeInTheDocument() }) it('renders grouped agent browser with source sections', () => { useAgentStore.setState({ allAgents: MOCK_AGENTS, activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), isLoading: false, fetchAgents: noopFetch, }) render() switchToAgentsTab() expect(screen.getByText('Browse installed agents')).toBeInTheDocument() expect(screen.getByText('Agent Browser')).toBeInTheDocument() expect(screen.getAllByText('User').length).toBeGreaterThan(0) expect(screen.getAllByText('Built-in').length).toBeGreaterThan(0) expect(screen.getAllByText('Project').length).toBeGreaterThan(0) expect(screen.getAllByText('Plugin').length).toBeGreaterThan(0) expect(screen.getByText('code-reviewer')).toBeInTheDocument() expect(screen.getByText('Writes technical documentation')).toBeInTheDocument() expect(screen.getByText('telegram:pairing')).toBeInTheDocument() expect(screen.getByText('Overridden by User')).toBeInTheDocument() }) it('opens agent detail with metadata cards and document prompt', () => { useAgentStore.setState({ allAgents: MOCK_AGENTS, activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), isLoading: false, fetchAgents: noopFetch, }) render() switchToAgentsTab() fireEvent.click(screen.getByText('code-reviewer')) expect(screen.getByText('Back to list')).toBeInTheDocument() expect(screen.getByText('Agent Profile')).toBeInTheDocument() expect(screen.getAllByText('claude-sonnet-4-6')[0]).toBeInTheDocument() expect(screen.getByText('Read')).toBeInTheDocument() expect(screen.getByRole('heading', { name: 'Code Reviewer' })).toBeInTheDocument() const rendererRoot = screen.getByRole('heading', { name: 'Code Reviewer' }).closest('div[class*="prose"]') expect(rendererRoot?.className).toContain('max-w-[72ch]') }) it('shows no system prompt state when agent has no prompt', () => { useAgentStore.setState({ allAgents: MOCK_AGENTS, activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), isLoading: false, fetchAgents: noopFetch, }) render() switchToAgentsTab() fireEvent.click(screen.getByText('plain-agent')) expect(screen.getByText('No system prompt defined.')).toBeInTheDocument() expect(screen.getByText('shadowed by User')).toBeInTheDocument() }) it('navigates back to list from detail view', () => { useAgentStore.setState({ allAgents: MOCK_AGENTS, activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), isLoading: false, fetchAgents: noopFetch, }) render() switchToAgentsTab() fireEvent.click(screen.getByText('code-reviewer')) fireEvent.click(screen.getByText('Back to list')) expect(screen.getByText('code-reviewer')).toBeInTheDocument() expect(screen.getByText('doc-writer')).toBeInTheDocument() expect(screen.getByText('plain-agent')).toBeInTheDocument() }) it('returns to plugins tab when agent detail was opened from plugins', async () => { useAgentStore.setState({ allAgents: MOCK_AGENTS, activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), isLoading: false, selectedAgent: MOCK_AGENTS[0], selectedAgentReturnTab: 'plugins', fetchAgents: noopFetch, selectAgent: (agent) => useAgentStore.setState({ selectedAgent: agent, selectedAgentReturnTab: agent ? 'plugins' : 'agents', }), }) render() switchToAgentsTab() await act(async () => { fireEvent.click(screen.getByText('Back to list')) await Promise.resolve() }) expect(screen.getByText('Installed Plugins')).toBeInTheDocument() }) }) describe('Settings > Skills tab', () => { beforeEach(() => { useSettingsStore.setState({ locale: 'en' }) useSkillStore.setState({ skills: [], selectedSkill: null, isLoading: false, isDetailLoading: false, error: null, fetchSkills: noopFetch, fetchSkillDetail: noopFetch, clearSelection: () => useSkillStore.setState({ selectedSkill: null }), }) }) it('renders markdown skills with document styling in detail view', () => { useSkillStore.setState({ selectedSkill: MOCK_SKILL_DETAIL, clearSelection: () => useSkillStore.setState({ selectedSkill: null }), }) render() switchToSkillsTab() expect(screen.getByText('Skill metadata')).toBeInTheDocument() expect(screen.getByRole('heading', { name: 'Heading' })).toBeInTheDocument() const rendererRoot = screen.getByRole('heading', { name: 'Heading' }).closest('div[class*="prose"]') expect(rendererRoot?.className).toContain('max-w-[72ch]') expect(rendererRoot?.className).toContain('prose-h2:border-b') expect(rendererRoot?.className).toContain('prose-p:text-[15px]') expect(screen.getByText('Helpful quote')).toBeInTheDocument() }) it('keeps code files rendered in CodeViewer instead of markdown prose', () => { useSkillStore.setState({ selectedSkill: MOCK_SKILL_DETAIL, clearSelection: () => useSkillStore.setState({ selectedSkill: null }), }) render() switchToSkillsTab() fireEvent.click(screen.getAllByText('helper.ts')[0]!) expect(screen.getByTestId('code-viewer')).toHaveTextContent('export const helper = true') expect(screen.queryByRole('heading', { name: 'Heading' })).not.toBeInTheDocument() }) })