mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
The sidebar now groups conversations by project, so the embedded project picker in the search box duplicated the main navigation model and hid state inside a compact icon. Remove that picker and its store state, leaving search as plain text filtering and keeping the project header menu focused on organization and sorting. The archive-all entry was also hidden because a broad destructive action does not belong in the lightweight project menu. Constraint: Project grouping is now the primary project navigation surface. Rejected: Keep the embedded project picker hidden in place | it would leave dead filtering state and a stale recovery path. Rejected: Keep archive-all in the header menu | broad destructive actions are too risky for this surface. Confidence: high Scope-risk: moderate Directive: Do not reintroduce broad session deletion or hidden project filters into the sidebar header without a dedicated reviewed management flow. Tested: cd desktop && bun run test src/components/layout/Sidebar.test.tsx src/stores/sessionStore.test.ts src/components/layout/TabBar.test.tsx Tested: cd desktop && bun run lint Tested: cd desktop && bun run build
275 lines
8.0 KiB
TypeScript
275 lines
8.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import '@testing-library/jest-dom'
|
|
|
|
import { McpSettings } from '../pages/McpSettings'
|
|
import { useMcpStore } from '../stores/mcpStore'
|
|
import { useSessionStore } from '../stores/sessionStore'
|
|
import { useSettingsStore } from '../stores/settingsStore'
|
|
|
|
describe('McpSettings', () => {
|
|
beforeEach(() => {
|
|
useSettingsStore.setState({ locale: 'en' })
|
|
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,
|
|
fetchSessions: vi.fn(),
|
|
createSession: vi.fn(),
|
|
deleteSession: vi.fn(),
|
|
renameSession: vi.fn(),
|
|
updateSessionTitle: vi.fn(),
|
|
setActiveSession: vi.fn(),
|
|
})
|
|
useMcpStore.setState({
|
|
servers: [],
|
|
selectedServer: null,
|
|
isLoading: false,
|
|
error: null,
|
|
fetchServers: vi.fn(),
|
|
createServer: vi.fn(),
|
|
updateServer: vi.fn(),
|
|
deleteServer: vi.fn(),
|
|
toggleServer: vi.fn(),
|
|
reconnectServer: vi.fn(),
|
|
refreshServerStatus: vi.fn(),
|
|
selectServer: vi.fn(),
|
|
})
|
|
})
|
|
|
|
it('loads only global MCP servers on mount', () => {
|
|
const fetchServers = vi.fn()
|
|
useMcpStore.setState({ fetchServers })
|
|
|
|
render(<McpSettings />)
|
|
|
|
expect(fetchServers).toHaveBeenCalledWith(undefined, '/workspace/project')
|
|
})
|
|
|
|
it('renders the empty state and add button', () => {
|
|
render(<McpSettings />)
|
|
|
|
expect(screen.getByText('MCP servers')).toBeInTheDocument()
|
|
expect(screen.getByText('No MCP servers configured yet')).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: /add server/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows plugin and user MCP servers in grouped sections', () => {
|
|
useMcpStore.setState({
|
|
servers: [
|
|
{
|
|
name: 'plugin:telegram:telegram',
|
|
scope: 'dynamic',
|
|
transport: 'stdio',
|
|
enabled: true,
|
|
status: 'connected',
|
|
statusLabel: 'Connected',
|
|
configLocation: '/tmp/config',
|
|
summary: 'npx @telegram/mcp',
|
|
canEdit: false,
|
|
canRemove: false,
|
|
canReconnect: true,
|
|
canToggle: true,
|
|
config: { type: 'stdio', command: 'npx', args: ['@telegram/mcp'], env: {} },
|
|
},
|
|
{
|
|
name: 'global-user',
|
|
scope: 'user',
|
|
transport: 'http',
|
|
enabled: true,
|
|
status: 'connected',
|
|
statusLabel: 'Connected',
|
|
configLocation: '/tmp/config',
|
|
summary: 'https://example.com/mcp',
|
|
canEdit: true,
|
|
canRemove: true,
|
|
canReconnect: true,
|
|
canToggle: true,
|
|
config: { type: 'http', url: 'https://example.com/mcp', headers: {} },
|
|
},
|
|
],
|
|
})
|
|
|
|
render(<McpSettings />)
|
|
|
|
expect(screen.getAllByText('Plugin').length).toBeGreaterThan(0)
|
|
expect(screen.getAllByText('User').length).toBeGreaterThan(0)
|
|
expect(screen.getByText('plugin:telegram:telegram')).toBeInTheDocument()
|
|
expect(screen.getByText('global-user')).toBeInTheDocument()
|
|
})
|
|
|
|
it('starts background status refresh after the fast list render', async () => {
|
|
const server = {
|
|
name: 'deepwiki',
|
|
scope: 'user',
|
|
transport: 'http',
|
|
enabled: true,
|
|
status: 'checking' as const,
|
|
statusLabel: 'Checking',
|
|
configLocation: '/tmp/config',
|
|
summary: 'https://example.com/mcp',
|
|
canEdit: true,
|
|
canRemove: true,
|
|
canReconnect: true,
|
|
canToggle: true,
|
|
config: { type: 'http' as const, url: 'https://example.com/mcp', headers: {} },
|
|
}
|
|
const refreshServerStatus = vi.fn().mockResolvedValue({
|
|
...server,
|
|
status: 'connected' as const,
|
|
statusLabel: 'Connected',
|
|
})
|
|
|
|
useMcpStore.setState({
|
|
servers: [server],
|
|
refreshServerStatus,
|
|
})
|
|
|
|
render(<McpSettings />)
|
|
|
|
expect(screen.getByText('Checking')).toBeInTheDocument()
|
|
|
|
await waitFor(() => {
|
|
expect(refreshServerStatus).toHaveBeenCalledWith(server, '/workspace/project')
|
|
})
|
|
})
|
|
|
|
it('opens the delete confirmation modal from the edit view and deletes with the active cwd', async () => {
|
|
const deleteServer = vi.fn().mockResolvedValue(undefined)
|
|
const server = {
|
|
name: 'global-user',
|
|
scope: 'user',
|
|
transport: 'http',
|
|
enabled: true,
|
|
status: 'connected',
|
|
statusLabel: 'Connected',
|
|
configLocation: '/tmp/config',
|
|
summary: 'https://example.com/mcp',
|
|
canEdit: true,
|
|
canRemove: true,
|
|
canReconnect: true,
|
|
canToggle: true,
|
|
config: { type: 'http', url: 'https://example.com/mcp', headers: {} },
|
|
} as const
|
|
|
|
useMcpStore.setState({
|
|
servers: [server],
|
|
deleteServer,
|
|
})
|
|
|
|
render(<McpSettings />)
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open global-user' }))
|
|
})
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: /uninstall/i }))
|
|
})
|
|
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
expect(screen.getByText('Delete MCP server')).toBeInTheDocument()
|
|
expect(screen.getByText('Delete MCP server "global-user"? This action cannot be undone.')).toBeInTheDocument()
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
|
|
})
|
|
|
|
expect(deleteServer).toHaveBeenCalledWith(server, '/workspace/project')
|
|
})
|
|
|
|
it('uses the active cwd when toggling a server', async () => {
|
|
const toggleServer = vi.fn().mockResolvedValue(undefined)
|
|
const server = {
|
|
name: 'global-user',
|
|
scope: 'user',
|
|
transport: 'http',
|
|
enabled: true,
|
|
status: 'connected',
|
|
statusLabel: 'Connected',
|
|
configLocation: '/tmp/config',
|
|
summary: 'https://example.com/mcp',
|
|
canEdit: true,
|
|
canRemove: true,
|
|
canReconnect: true,
|
|
canToggle: true,
|
|
config: { type: 'http', url: 'https://example.com/mcp', headers: {} },
|
|
} as const
|
|
|
|
useMcpStore.setState({
|
|
servers: [server],
|
|
toggleServer,
|
|
})
|
|
|
|
render(<McpSettings />)
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('switch'))
|
|
})
|
|
|
|
expect(toggleServer).toHaveBeenCalledWith(server, '/workspace/project')
|
|
})
|
|
|
|
it('shows reconnecting status immediately in the detail view', async () => {
|
|
let resolveReconnect: ((value: typeof server) => void) | null = null
|
|
const server = {
|
|
name: 'plugin:telegram:telegram',
|
|
scope: 'dynamic',
|
|
transport: 'stdio',
|
|
enabled: true,
|
|
status: 'failed' as 'connected' | 'needs-auth' | 'failed' | 'disabled' | 'checking',
|
|
statusLabel: 'Unavailable',
|
|
statusDetail: 'Timed out' as string | undefined,
|
|
configLocation: '/tmp/config',
|
|
summary: 'bun run start',
|
|
canEdit: false,
|
|
canRemove: false,
|
|
canReconnect: true,
|
|
canToggle: true,
|
|
config: { type: 'stdio' as const, command: 'bun', args: ['run', 'start'], env: {} },
|
|
}
|
|
const reconnectServer = vi.fn().mockImplementation(() => new Promise<typeof server>((resolve) => {
|
|
resolveReconnect = resolve
|
|
}))
|
|
|
|
useMcpStore.setState({
|
|
servers: [server],
|
|
reconnectServer,
|
|
})
|
|
|
|
render(<McpSettings />)
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open plugin:telegram:telegram' }))
|
|
})
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: /reconnect/i }))
|
|
})
|
|
|
|
expect(screen.getAllByText('Reconnecting...').length).toBeGreaterThan(0)
|
|
expect(reconnectServer).toHaveBeenCalledWith(server, '/workspace/project')
|
|
|
|
await act(async () => {
|
|
resolveReconnect?.({
|
|
...server,
|
|
status: 'connected',
|
|
statusLabel: 'Connected',
|
|
statusDetail: undefined,
|
|
})
|
|
})
|
|
})
|
|
})
|