cc-haha/desktop/src/__tests__/skillsSettings.test.tsx
程序员阿江(Relakkes) 48da5ccbfa Reduce Settings complexity by removing the embedded shell surface
The Settings terminal added a full xterm plus Tauri PTY stack for a job
that is better handled by dedicated install and configuration flows. This
change removes the Settings tab, frontend terminal wiring, Tauri terminal
commands, and the terminal-only dependencies so the desktop settings
surface stays narrower and less fragile.

Constraint: The worktree already contains unrelated desktop icon and UI changes, so this commit stages only the terminal-removal slice
Rejected: Keep a hidden or runtime-only terminal stub | it would still preserve the heavy cross-layer maintenance surface
Rejected: Remove only the Settings tab and leave the Tauri PTY backend | that would leave dead code and unused dependencies behind
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If future install workflows need more power, prefer Settings-native actions and runtime refresh over reintroducing a general shell tab
Tested: bun x vitest run src/__tests__/generalSettings.test.tsx src/__tests__/skillsSettings.test.tsx src/__tests__/pluginsSettings.test.tsx src/__tests__/agentsSettings.test.tsx src/__tests__/mcpSettings.test.tsx src/components/settings/InstallCenter.test.tsx; bun run lint; bun run build; cargo check --manifest-path desktop/src-tauri/Cargo.toml
Not-tested: Manual packaged desktop app click-through after removing the Settings terminal tab
2026-04-23 11:06:32 +08:00

260 lines
7.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import { Settings } from '../pages/Settings'
import { useSkillStore } from '../stores/skillStore'
import { useSettingsStore } from '../stores/settingsStore'
import { useSessionStore } from '../stores/sessionStore'
import { useTabStore, SETTINGS_TAB_ID } from '../stores/tabStore'
import { useUIStore } from '../stores/uiStore'
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: () => <div>Adapter Settings Mock</div>,
}))
vi.mock('../stores/agentStore', () => ({
useAgentStore: () => ({
activeAgents: [],
allAgents: [],
isLoading: false,
error: null,
selectedAgent: null,
fetchAgents: vi.fn(),
selectAgent: vi.fn(),
}),
}))
vi.mock('../components/chat/CodeViewer', () => ({
CodeViewer: ({ code }: { code: string }) => <pre data-testid="code-viewer">{code}</pre>,
}))
const MOCK_FETCH_SKILLS = vi.fn()
const MOCK_FETCH_SKILL_DETAIL = vi.fn()
const MOCK_CLEAR_SELECTION = vi.fn()
function switchToSkillsTab() {
fireEvent.click(screen.getByText('Skills'))
}
describe('Settings > Skills tab', () => {
beforeEach(() => {
vi.clearAllMocks()
useSettingsStore.setState({ locale: 'en' })
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,
selectedProjects: [],
availableProjects: ['/workspace/project'],
})
useTabStore.setState({ tabs: [], activeTabId: null })
useUIStore.setState({ pendingSettingsTab: null })
useSkillStore.setState({
skills: [],
selectedSkill: null,
selectedSkillReturnTab: 'skills',
isLoading: false,
isDetailLoading: false,
error: null,
fetchSkills: MOCK_FETCH_SKILLS,
fetchSkillDetail: MOCK_FETCH_SKILL_DETAIL,
clearSelection: MOCK_CLEAR_SELECTION,
})
})
it('renders browser summary and grouped skill cards', () => {
useSkillStore.setState({
skills: [
{
name: 'alpha',
displayName: 'Alpha Skill',
description: 'First skill description',
source: 'user',
userInvocable: true,
version: '1.0.0',
contentLength: 400,
hasDirectory: true,
},
{
name: 'beta',
description: 'Second skill description',
source: 'project',
userInvocable: false,
contentLength: 200,
hasDirectory: true,
},
{
name: 'telegram:access',
displayName: 'Telegram Access',
description: 'Plugin-provided access workflow',
source: 'plugin',
pluginName: 'telegram',
userInvocable: true,
contentLength: 280,
hasDirectory: true,
},
],
})
render(<Settings />)
switchToSkillsTab()
expect(screen.getByText('Browse installed skills')).toBeInTheDocument()
expect(screen.getByText('Skill Browser')).toBeInTheDocument()
expect(screen.getByText('Total skills')).toBeInTheDocument()
expect(screen.getByText('Alpha Skill')).toBeInTheDocument()
expect(screen.getByText('Second skill description')).toBeInTheDocument()
expect(screen.getAllByText('Plugin').length).toBeGreaterThan(0)
expect(screen.getByText('Telegram Access')).toBeInTheDocument()
})
it('uses the active session workDir even when settings tab is focused', () => {
const fetchSkills = vi.fn()
useSkillStore.setState({
skills: [],
selectedSkill: null,
isLoading: false,
isDetailLoading: false,
error: null,
fetchSkills,
fetchSkillDetail: MOCK_FETCH_SKILL_DETAIL,
clearSelection: MOCK_CLEAR_SELECTION,
})
useTabStore.setState({
activeTabId: SETTINGS_TAB_ID,
tabs: [{ sessionId: SETTINGS_TAB_ID, title: 'Settings', type: 'settings', status: 'idle' }],
})
render(<Settings />)
switchToSkillsTab()
expect(fetchSkills).toHaveBeenCalledWith('/workspace/project')
})
it('opens skill detail with metadata cards and parsed markdown body', () => {
useSkillStore.setState({
selectedSkill: {
meta: {
name: 'alpha',
displayName: 'Alpha Skill',
description: 'First skill description',
source: 'user',
userInvocable: true,
version: '1.0.0',
contentLength: 400,
hasDirectory: true,
},
tree: [
{ name: 'SKILL.md', path: 'SKILL.md', type: 'file' },
{ name: 'run.ts', path: 'run.ts', type: 'file' },
],
files: [
{
path: 'SKILL.md',
content: '# Hello\n\nBody content',
body: '# Hello\n\nBody content',
language: 'markdown',
isEntry: true,
frontmatter: {
description: 'Frontmatter description',
'allowed-tools': ['Read', 'Edit'],
model: 'sonnet',
},
},
{
path: 'run.ts',
content: 'console.log("hello")',
language: 'typescript',
isEntry: false,
},
],
skillRoot: '/tmp/alpha',
},
selectedSkillReturnTab: 'skills',
})
render(<Settings />)
switchToSkillsTab()
expect(screen.getByText('Skill metadata')).toBeInTheDocument()
expect(screen.getByText('/slash')).toBeInTheDocument()
expect(screen.getByText('Frontmatter description')).toBeInTheDocument()
expect(screen.getByText('Read, Edit')).toBeInTheDocument()
expect(screen.getByText('Hello')).toBeInTheDocument()
expect(screen.queryByText(/^---$/)).not.toBeInTheDocument()
})
it('returns to plugins tab when skill detail was opened from plugins', () => {
useSkillStore.setState({
selectedSkill: {
meta: {
name: 'telegram:access',
displayName: 'Access',
description: 'Plugin skill',
source: 'plugin',
userInvocable: true,
contentLength: 200,
hasDirectory: true,
},
tree: [{ name: 'SKILL.md', path: 'SKILL.md', type: 'file' }],
files: [
{
path: 'SKILL.md',
content: '# Access',
body: '# Access',
language: 'markdown',
isEntry: true,
},
],
skillRoot: '/tmp/telegram-access',
},
selectedSkillReturnTab: 'plugins',
})
render(<Settings />)
switchToSkillsTab()
fireEvent.click(screen.getByText('Back to list'))
expect(screen.getByText('Installed Plugins')).toBeInTheDocument()
})
})