mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
The desktop workspace panel and chat transcript were mixing project-level file state with per-turn session history, which made rewind, file attachment, and changed-file previews brittle across multi-turn and historical sessions. This keeps turn checkpoints durable in the transcript, makes workspace refreshes happen at the right lifecycle points, and hardens long file previews without blocking the UI. Constraint: Right-side workspace changes are project working-tree state, while chat turn cards are session checkpoint state. Rejected: Treat every changed-file panel entry as session-local | new sessions must still reveal existing dirty project files. Confidence: high Scope-risk: moderate Directive: Do not couple global workspace status to session checkpoint cards without preserving both product meanings. Tested: cd desktop && bun run test src/components/workspace/WorkspacePanel.test.tsx Tested: cd desktop && bun run lint Tested: git diff --check Tested: cd desktop && bun run test Not-tested: Packaged Tauri runtime smoke test
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import '@testing-library/jest-dom'
|
|
import { FileSearchMenu } from './FileSearchMenu'
|
|
import { ApiError } from '../../api/client'
|
|
import { filesystemApi } from '../../api/filesystem'
|
|
import { useSettingsStore } from '../../stores/settingsStore'
|
|
|
|
vi.mock('../../api/filesystem', () => ({
|
|
filesystemApi: {
|
|
browse: vi.fn(),
|
|
search: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
describe('FileSearchMenu', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
useSettingsStore.setState({ locale: 'en' })
|
|
})
|
|
|
|
it('shows an explicit error when directory browsing is denied', async () => {
|
|
vi.mocked(filesystemApi.browse).mockRejectedValueOnce(
|
|
new ApiError(403, { error: 'Access denied: path outside allowed directory' }),
|
|
)
|
|
|
|
render(
|
|
<FileSearchMenu
|
|
cwd="/private/tmp"
|
|
onSelect={() => {}}
|
|
/>,
|
|
)
|
|
|
|
expect(await screen.findByText('Cannot access this directory')).toBeInTheDocument()
|
|
expect(screen.queryByText('No files in this directory')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('renders returned files when browsing succeeds', async () => {
|
|
vi.mocked(filesystemApi.browse).mockResolvedValueOnce({
|
|
currentPath: '/tmp',
|
|
parentPath: '/',
|
|
entries: [
|
|
{ name: 'preview.png', path: '/tmp/preview.png', isDirectory: false },
|
|
],
|
|
})
|
|
|
|
render(
|
|
<FileSearchMenu
|
|
cwd="/tmp"
|
|
onSelect={() => {}}
|
|
/>,
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('preview.png')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('navigates directories without selecting them as attachments', async () => {
|
|
const onSelect = vi.fn()
|
|
const onNavigate = vi.fn()
|
|
vi.mocked(filesystemApi.search).mockResolvedValueOnce({
|
|
currentPath: '/repo',
|
|
parentPath: '/',
|
|
query: 'backend',
|
|
entries: [
|
|
{ name: 'backend', path: '/repo/backend', isDirectory: true },
|
|
],
|
|
})
|
|
vi.mocked(filesystemApi.browse).mockResolvedValueOnce({
|
|
currentPath: '/repo/backend',
|
|
parentPath: '/repo',
|
|
entries: [
|
|
{ name: 'src', path: '/repo/backend/src', isDirectory: true },
|
|
],
|
|
})
|
|
|
|
render(
|
|
<FileSearchMenu
|
|
cwd="/repo"
|
|
filter="backend"
|
|
onSelect={onSelect}
|
|
onNavigate={onNavigate}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(await screen.findByText('backend'))
|
|
|
|
expect(onSelect).not.toHaveBeenCalled()
|
|
expect(onNavigate).toHaveBeenCalledWith('backend/')
|
|
await waitFor(() => {
|
|
expect(filesystemApi.browse).toHaveBeenCalledWith('/repo/backend', { includeFiles: true })
|
|
})
|
|
})
|
|
|
|
it('passes nested relative file paths when selecting a file', async () => {
|
|
const onSelect = vi.fn()
|
|
vi.mocked(filesystemApi.search).mockResolvedValueOnce({
|
|
currentPath: '/repo/backend/src',
|
|
parentPath: '/repo/backend',
|
|
query: 'pictactic',
|
|
entries: [
|
|
{ name: 'pictactic', path: '/repo/backend/src/pictactic', isDirectory: false },
|
|
],
|
|
})
|
|
|
|
render(
|
|
<FileSearchMenu
|
|
cwd="/repo"
|
|
filter="backend/src/pictactic"
|
|
onSelect={onSelect}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(await screen.findByText('pictactic'))
|
|
|
|
expect(onSelect).toHaveBeenCalledWith('/repo/backend/src/pictactic', 'backend/src/pictactic')
|
|
})
|
|
})
|