mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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
396 lines
12 KiB
TypeScript
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()
|
|
})
|
|
})
|