cc-haha/desktop/src/api/projects.test.ts
小橙子 d2ef9ad330
feat(sidebar): delete all sessions for a project (#74)
* feat(sidebar): delete all sessions for a project

Add a "Delete all sessions" option to the project context menu so a
project can be removed from the sidebar entirely (not just hidden).
Hide-from-sidebar is a UI-only soft toggle; this is a destructive
operation that wipes the on-disk artefacts.

Backend
- POST /api/projects/sessions/clear accepts { projectId } or
  { workDir } (the desktop client passes workDir; backend sanitizes).
- Deletes every .jsonl transcript and matching .summary.json sidecar
  under ~/.claude/projects/<sanitized-id>/. Sub-directories and other
  non-session files (memory/, user notes) are preserved.
- If the directory becomes empty, removes it so the project drops off
  the sidebar listing entirely.
- Defence in depth: rejects path-separator/null-byte/`.`/`..` ids and
  re-checks the resolved path stays under projectsDir.
- 11 backend tests covering traversal/null-byte/dot rejection, missing
  dir 200, full-clear+rmdir, non-jsonl preservation, summary sidecar
  deletion, workDir absolute-path acceptance, relative-path rejection,
  and 405 on GET.

Frontend
- Sidebar project context menu: red "Delete all sessions" entry
  (Trash2 icon, danger) below the hide entry.
- ConfirmDialog with project title + session count + irreversibility
  warning. Disconnects open sessions and closes their tabs before the
  call so the websocket layer doesn't keep stale handles.
- After success, fetchSessions() refreshes the list — the project
  disappears if its dir was removed, or shows up empty if memory/
  remained.
- 5 new i18n keys across en/zh/jp/kr/zh-TW.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(sidebar): cover projectsApi.clearSessions client behaviour

Wire-level guard for the desktop API helper introduced in this PR — the
sidebar's "Delete all sessions" flow depends on it sending the right
endpoint and body, and surfacing server errors back to the caller for
toast handling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: 你的姓名 <you@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 13:12:19 +08:00

36 lines
1.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import '@testing-library/jest-dom'
const apiPostMock = vi.fn()
vi.mock('./client', () => ({
api: {
post: (url: string, body?: unknown) => apiPostMock(url, body),
get: vi.fn(),
},
}))
import { projectsApi } from './projects'
beforeEach(() => {
apiPostMock.mockReset()
})
describe('projectsApi.clearSessions', () => {
it('posts to /api/projects/sessions/clear with workDir in body', async () => {
apiPostMock.mockResolvedValue({ ok: true, deletedSessions: 3, projectDirRemoved: true })
const result = await projectsApi.clearSessions('/work/alpha')
expect(apiPostMock).toHaveBeenCalledTimes(1)
expect(apiPostMock).toHaveBeenCalledWith('/api/projects/sessions/clear', { workDir: '/work/alpha' })
expect(result.deletedSessions).toBe(3)
expect(result.projectDirRemoved).toBe(true)
})
it('propagates server errors so the caller can show a toast', async () => {
apiPostMock.mockRejectedValue(new Error('Forbidden'))
await expect(projectsApi.clearSessions('/work/beta')).rejects.toThrow('Forbidden')
})
})