import { beforeEach, describe, expect, it, vi } from 'vitest' import { act, fireEvent, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import { Settings } from '../pages/Settings' import { usePluginStore } from '../stores/pluginStore' import { useSettingsStore } from '../stores/settingsStore' import { useSessionStore } from '../stores/sessionStore' import { useUIStore } from '../stores/uiStore' const MOCK_FETCH_SKILLS = vi.fn() const MOCK_FETCH_SKILL_DETAIL = vi.fn() const MOCK_FETCH_AGENTS = vi.fn() const MOCK_FETCH_SERVERS = vi.fn() vi.mock('../api/agents', () => ({ agentsApi: { list: vi.fn().mockResolvedValue({ activeAgents: [], allAgents: [] }), }, })) 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('../stores/agentStore', () => ({ useAgentStore: Object.assign((selector?: (state: any) => unknown) => { const state = { activeAgents: [], allAgents: [], isLoading: false, error: null, selectedAgent: null, fetchAgents: MOCK_FETCH_AGENTS, selectAgent: vi.fn(), } return selector ? selector(state) : state }, { getState: () => ({ activeAgents: [], allAgents: [], isLoading: false, error: null, selectedAgent: null, fetchAgents: MOCK_FETCH_AGENTS, selectAgent: vi.fn(), }), }), })) vi.mock('../stores/skillStore', () => ({ useSkillStore: Object.assign((selector?: (state: any) => unknown) => { const state = { skills: [], selectedSkill: null, isLoading: false, isDetailLoading: false, error: null, fetchSkills: MOCK_FETCH_SKILLS, fetchSkillDetail: MOCK_FETCH_SKILL_DETAIL, clearSelection: vi.fn(), } return selector ? selector(state) : state }, { getState: () => ({ skills: [], selectedSkill: null, isLoading: false, isDetailLoading: false, error: null, fetchSkills: MOCK_FETCH_SKILLS, fetchSkillDetail: MOCK_FETCH_SKILL_DETAIL, clearSelection: vi.fn(), }), }), })) vi.mock('../stores/mcpStore', () => ({ useMcpStore: Object.assign((selector?: (state: any) => unknown) => { const state = { servers: [], selectedServer: null, isLoading: false, error: null, fetchServers: MOCK_FETCH_SERVERS, createServer: vi.fn(), updateServer: vi.fn(), deleteServer: vi.fn(), toggleServer: vi.fn(), reconnectServer: vi.fn(), selectServer: vi.fn(), } return selector ? selector(state) : state }, { getState: () => ({ servers: [], selectedServer: null, isLoading: false, error: null, fetchServers: MOCK_FETCH_SERVERS, createServer: vi.fn(), updateServer: vi.fn(), deleteServer: vi.fn(), toggleServer: vi.fn(), reconnectServer: vi.fn(), selectServer: vi.fn(), }), }), })) const noop = vi.fn() function switchToPluginsTab() { fireEvent.click(screen.getByText('Plugins')) } describe('Settings > Plugins tab', () => { beforeEach(() => { vi.clearAllMocks() useSettingsStore.setState({ locale: 'en' }) useUIStore.setState({ pendingSettingsTab: null }) useSessionStore.setState({ sessions: [ { id: 'session-1', title: 'Active session', createdAt: '2026-04-20T00:00:00.000Z', modifiedAt: '2026-04-20T00:00:00.000Z', messageCount: 1, projectPath: '/workspace/project', workDir: '/workspace/project', workDirExists: true, }, ], activeSessionId: 'session-1', isLoading: false, error: null, }) usePluginStore.setState({ plugins: [], marketplaces: [], summary: { total: 0, enabled: 0, errorCount: 0, marketplaceCount: 0 }, selectedPlugin: null, lastReloadSummary: null, isLoading: false, isDetailLoading: false, isApplying: false, error: null, fetchPlugins: noop, fetchPluginDetail: noop, reloadPlugins: vi.fn().mockResolvedValue({ enabled: 1, disabled: 0, skills: 2, agents: 1, hooks: 0, mcpServers: 1, lspServers: 0, errors: 0, }), enablePlugin: vi.fn().mockResolvedValue('enabled'), disablePlugin: vi.fn().mockResolvedValue('disabled'), updatePlugin: vi.fn().mockResolvedValue('updated'), uninstallPlugin: vi.fn().mockResolvedValue('uninstalled'), clearSelection: vi.fn(), }) }) it('renders plugin browser summary and grouped cards', () => { usePluginStore.setState({ plugins: [ { id: 'github@claude-plugins-official', name: 'github', marketplace: 'claude-plugins-official', scope: 'user', enabled: true, hasErrors: false, isBuiltin: false, version: '1.2.3', description: 'GitHub integration', authorName: 'Anthropic', componentCounts: { commands: 1, agents: 1, skills: 2, hooks: 0, mcpServers: 1, lspServers: 0, }, errors: [], }, { id: 'pyright-lsp@claude-plugins-official', name: 'pyright-lsp', marketplace: 'claude-plugins-official', scope: 'project', enabled: false, hasErrors: true, isBuiltin: false, description: 'Python language tooling', componentCounts: { commands: 0, agents: 0, skills: 0, hooks: 0, mcpServers: 0, lspServers: 1, }, errors: ['Executable not found in $PATH'], }, ], marketplaces: [ { name: 'claude-plugins-official', source: 'github:anthropics/claude-plugins-official', autoUpdate: true, installedCount: 2, }, ], summary: { total: 2, enabled: 1, errorCount: 1, marketplaceCount: 1 }, }) render() switchToPluginsTab() expect(screen.getByText('Browse installed plugins')).toBeInTheDocument() expect(screen.getByText('Plugin Manager')).toBeInTheDocument() expect(screen.getAllByText('Needs attention').length).toBeGreaterThan(0) expect(screen.getByText('github')).toBeInTheDocument() expect(screen.getByText('Python language tooling')).toBeInTheDocument() expect(screen.getByText('Known marketplaces')).toBeInTheDocument() }) it('renders plugin detail with bundled capability sections', () => { usePluginStore.setState({ selectedPlugin: { id: 'github@claude-plugins-official', name: 'github', marketplace: 'claude-plugins-official', scope: 'user', enabled: true, hasErrors: false, isBuiltin: false, version: '1.2.3', description: 'GitHub integration', authorName: 'Anthropic', installPath: '/Users/test/.claude/plugins/cache/github', componentCounts: { commands: 1, agents: 1, skills: 2, hooks: 1, mcpServers: 1, lspServers: 0, }, capabilities: { commands: ['review-pr'], agents: ['pr-reviewer'], skills: ['commit', 'create-pr'], hooks: ['SessionStart'], mcpServers: ['github-api'], lspServers: [], }, commandEntries: [ { name: 'review-pr', description: 'Review the current pull request.', }, ], agentEntries: [ { name: 'pr-reviewer', description: 'Review pull request quality and risk.', }, ], hookEntries: [ { event: 'SessionStart', matcher: 'Write', actions: ['echo preparing plugin runtime'], }, ], skillEntries: [ { name: 'create-pr', description: 'Create a pull request from the current branch.', }, { name: 'commit', description: 'Commit the current staged changes.', version: '1.0.0', }, ], mcpServerEntries: [ { name: 'plugin:github:github-api', displayName: 'github-api', transport: 'http', summary: 'https://api.github.com/mcp', }, ], errors: [], }, }) render() switchToPluginsTab() expect(screen.getByText('Plugin Detail')).toBeInTheDocument() expect(screen.getByText('GitHub integration')).toBeInTheDocument() expect(screen.getByText('Bundled capabilities')).toBeInTheDocument() expect(screen.getByText('/review-pr')).toBeInTheDocument() expect(screen.getByText('Review pull request quality and risk.')).toBeInTheDocument() expect(screen.getByText('echo preparing plugin runtime')).toBeInTheDocument() expect(screen.getByText('Create a pull request from the current branch.')).toBeInTheDocument() expect(screen.getByText('https://api.github.com/mcp')).toBeInTheDocument() expect(screen.getByText('Apply changes')).toBeInTheDocument() expect(screen.getByText('Uninstall')).toBeInTheDocument() }) it('keeps plugin detail hook order stable while the selected plugin reloads', () => { usePluginStore.setState({ selectedPlugin: { id: 'github@claude-plugins-official', name: 'github', marketplace: 'claude-plugins-official', scope: 'user', enabled: false, hasErrors: false, isBuiltin: false, description: 'GitHub integration', componentCounts: { commands: 1, agents: 0, skills: 0, hooks: 0, mcpServers: 0, lspServers: 0, }, capabilities: { commands: ['review-pr'], agents: [], skills: [], hooks: [], mcpServers: [], lspServers: [], }, commandEntries: [ { name: 'review-pr', description: 'Review the current pull request.', }, ], agentEntries: [], hookEntries: [], skillEntries: [], mcpServerEntries: [], errors: [], }, }) const { container } = render() switchToPluginsTab() expect(screen.getByText('GitHub integration')).toBeInTheDocument() act(() => { usePluginStore.setState({ isDetailLoading: true }) }) expect(container.querySelector('.animate-spin')).toBeInTheDocument() }) it('navigates plugin skills into the shared Skills page flow', () => { usePluginStore.setState({ selectedPlugin: { id: 'telegram@claude-plugins-official', name: 'telegram', marketplace: 'claude-plugins-official', scope: 'user', enabled: true, hasErrors: false, isBuiltin: false, description: 'Telegram integration', componentCounts: { commands: 0, agents: 0, skills: 1, hooks: 0, mcpServers: 0, lspServers: 0, }, capabilities: { commands: [], agents: [], skills: ['telegram:access'], hooks: [], mcpServers: [], lspServers: [], }, commandEntries: [], agentEntries: [], hookEntries: [], skillEntries: [ { name: 'telegram:access', displayName: 'access', description: 'Manage Telegram access.', pluginName: 'telegram', }, ], mcpServerEntries: [], errors: [], }, }) render() switchToPluginsTab() fireEvent.click(screen.getByText('access')) expect(MOCK_FETCH_SKILL_DETAIL).toHaveBeenCalledWith('plugin', 'telegram:access', '/workspace/project', 'plugins') }) it('disables shared navigation cards for disabled plugins', () => { usePluginStore.setState({ selectedPlugin: { id: 'codex@openai-codex', name: 'codex', marketplace: 'openai-codex', scope: 'user', enabled: false, hasErrors: false, isBuiltin: false, description: 'Use Codex from Claude Code', componentCounts: { commands: 0, agents: 1, skills: 1, hooks: 0, mcpServers: 0, lspServers: 0, }, capabilities: { commands: [], agents: ['codex:codex-rescue'], skills: ['codex:gpt-5-4-prompting'], hooks: [], mcpServers: [], lspServers: [], }, commandEntries: [], agentEntries: [ { name: 'codex:codex-rescue', displayName: 'codex-rescue', description: 'Delegate to Codex.', }, ], hookEntries: [], skillEntries: [ { name: 'codex:gpt-5-4-prompting', displayName: 'gpt-5-4-prompting', description: 'Prompting guide.', }, ], mcpServerEntries: [], errors: [], }, }) render() switchToPluginsTab() expect(screen.getAllByText('Enable this plugin and apply changes before opening its skills, agents, or MCP entries in the shared management pages.').length).toBeGreaterThan(0) expect(screen.getByRole('button', { name: /codex-rescue/i })).toBeDisabled() expect(screen.getByRole('button', { name: /gpt-5-4-prompting/i })).toBeDisabled() }) })