cc-haha/desktop/src/__tests__/agentsSettings.test.tsx
程序员阿江(Relakkes) b156be8d8d feat: make PR quality verification self-enforcing
Contributors and coding agents need one local command that both reports and enforces the quality contract. This change turns the PR gate into the shared verification entrypoint, adds path-selected local lanes, tightens coverage accounting around changed lines, and documents the repair loop in contributor and agent-facing guidance.

Constraint: Ordinary PR verification must stay non-live and runnable without provider credentials
Constraint: Coverage policy updates in this commit require maintainer approval before push/merge
Rejected: Keep quality guidance only in docs | agents need executable scripts and AGENTS.md instructions to follow the loop consistently
Confidence: high
Scope-risk: broad
Directive: Do not bypass `bun run verify` for production changes; fix failed lanes and coverage reports instead of lowering thresholds
Tested: bun run check:policy
Tested: ALLOW_CLI_CORE_CHANGE=1 ALLOW_COVERAGE_BASELINE_CHANGE=1 bun run verify
Not-tested: live provider baseline; no provider credentials were required for this non-live PR gate
2026-05-06 22:33:43 +08:00

396 lines
12 KiB
TypeScript

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: () => <div>Adapter Settings Mock</div>,
}))
vi.mock('../components/chat/CodeViewer', () => ({
CodeViewer: ({ code }: { code: string }) => <pre data-testid="code-viewer">{code}</pre>,
}))
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(<Settings />)
expect(screen.getByText('Agents')).toBeInTheDocument()
})
it('shows loading spinner when fetching agents', () => {
useAgentStore.setState({ isLoading: true, allAgents: [], activeAgents: [], fetchAgents: noopFetch })
render(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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()
})
})