cc-haha/desktop/src/stores/cliTaskStore.test.ts
程序员阿江(Relakkes) ef818f8d8c fix(desktop): keep dismissed task progress hidden
Dismissed completed task lists are a user visibility choice for that completed cycle, so polling the same persisted tasks during the next chat turn should not resurrect the bar. The store now keeps the dismissed completion key while the remote reset is in flight, and the markdown renderer gives default lists enough inset so bullets stay inside message cards.

Constraint: Preserve future task cycles when new TodoWrite or task updates arrive.
Rejected: Clear only the visible task array | polling can rehydrate the same completed list before remote reset finishes
Confidence: high
Scope-risk: narrow
Tested: cd desktop && bun run test -- run src/stores/cliTaskStore.test.ts src/components/markdown/MarkdownRenderer.test.tsx
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Live Tauri chat session task creation and dismissal
2026-05-31 17:42:30 +08:00

212 lines
6.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cliTasksApi } from '../api/cliTasks'
import type { CLITask, TaskStatus } from '../types/cliTask'
import { useCLITaskStore } from './cliTaskStore'
vi.mock('../api/cliTasks', () => ({
cliTasksApi: {
getTasksForList: vi.fn(),
resetTaskList: vi.fn(),
},
}))
function makeTask(taskListId: string, status: TaskStatus = 'in_progress'): CLITask {
return {
id: '1',
subject: 'Keep current session isolated',
description: '',
status,
blocks: [],
blockedBy: [],
taskListId,
}
}
describe('cliTaskStore', () => {
beforeEach(() => {
useCLITaskStore.getState().clearTasks()
vi.clearAllMocks()
})
afterEach(() => {
useCLITaskStore.getState().clearTasks()
})
it('clears stale tasks immediately when switching tracked sessions', async () => {
let resolveRequest: ((value: { tasks: ReturnType<typeof makeTask>[] }) => void) | null = null
vi.mocked(cliTasksApi.getTasksForList).mockImplementation(
(sessionId: string) =>
new Promise<{ tasks: ReturnType<typeof makeTask>[] }>((resolve) => {
if (sessionId === 'session-2') resolveRequest = resolve
}),
)
useCLITaskStore.setState({
sessionId: 'session-1',
tasks: [makeTask('session-1')],
expanded: true,
completedAndDismissed: true,
dismissedCompletionKey: 'session-1::done',
})
const fetchPromise = useCLITaskStore.getState().fetchSessionTasks('session-2')
expect(useCLITaskStore.getState()).toMatchObject({
sessionId: 'session-2',
tasks: [],
expanded: false,
completedAndDismissed: false,
dismissedCompletionKey: null,
})
expect(resolveRequest).not.toBeNull()
resolveRequest!({ tasks: [makeTask('session-2', 'completed')] })
await fetchPromise
expect(useCLITaskStore.getState().tasks).toMatchObject([
{ taskListId: 'session-2', status: 'completed' },
])
})
it('resets a completed task list locally before clearing it remotely', async () => {
let resolveReset: ((value: { ok: true }) => void) | null = null
vi.mocked(cliTasksApi.resetTaskList).mockImplementation(
() => new Promise<{ ok: true }>((resolve) => {
resolveReset = resolve
}),
)
useCLITaskStore.setState({
sessionId: 'session-1',
tasks: [
makeTask('session-1', 'completed'),
{ ...makeTask('session-1', 'completed'), id: '2', subject: 'Second completed task' },
],
expanded: true,
completedAndDismissed: true,
dismissedCompletionKey: 'session-1::done',
})
const resetPromise = useCLITaskStore.getState().resetCompletedTasks()
expect(vi.mocked(cliTasksApi.resetTaskList)).toHaveBeenCalledWith('session-1')
expect(useCLITaskStore.getState()).toMatchObject({
tasks: [],
resetting: true,
completedAndDismissed: true,
dismissedCompletionKey: 'session-1::1::Keep current session isolated::completed::::|session-1::2::Second completed task::completed::::',
expanded: false,
})
expect(resolveReset).not.toBeNull()
resolveReset!({ ok: true })
await resetPromise
expect(useCLITaskStore.getState().resetting).toBe(false)
})
it('keeps a dismissed completed list hidden if polling returns it again', async () => {
const completedTasks = [
makeTask('session-1', 'completed'),
{ ...makeTask('session-1', 'completed'), id: '2', subject: 'Second completed task' },
]
vi.mocked(cliTasksApi.resetTaskList).mockResolvedValue({ ok: true })
vi.mocked(cliTasksApi.getTasksForList).mockResolvedValue({ tasks: completedTasks })
useCLITaskStore.setState({
sessionId: 'session-1',
tasks: completedTasks,
expanded: true,
completedAndDismissed: false,
dismissedCompletionKey: null,
})
await useCLITaskStore.getState().resetCompletedTasks()
await useCLITaskStore.getState().fetchSessionTasks('session-1')
expect(useCLITaskStore.getState()).toMatchObject({
tasks: completedTasks,
completedAndDismissed: true,
dismissedCompletionKey: 'session-1::1::Keep current session isolated::completed::::|session-1::2::Second completed task::completed::::',
expanded: false,
})
})
it('refreshes tasks for the currently tracked session by default', async () => {
vi.mocked(cliTasksApi.getTasksForList).mockResolvedValue({
tasks: [makeTask('session-1', 'in_progress')],
})
useCLITaskStore.setState({
sessionId: 'session-1',
tasks: [],
expanded: false,
completedAndDismissed: false,
dismissedCompletionKey: null,
})
await useCLITaskStore.getState().refreshTasks()
expect(cliTasksApi.getTasksForList).toHaveBeenCalledWith('session-1')
expect(useCLITaskStore.getState().tasks).toMatchObject([
{ taskListId: 'session-1', status: 'in_progress' },
])
})
it('marks completed tasks dismissed for the currently tracked session by default', () => {
useCLITaskStore.setState({
sessionId: 'session-1',
tasks: [makeTask('session-1', 'completed')],
expanded: true,
completedAndDismissed: false,
dismissedCompletionKey: null,
})
useCLITaskStore.getState().markCompletedAndDismissed()
expect(useCLITaskStore.getState()).toMatchObject({
completedAndDismissed: true,
dismissedCompletionKey: 'session-1::1::Keep current session isolated::completed::::',
expanded: false,
})
})
it('ignores TodoWrite updates for a session that is not currently tracked', () => {
useCLITaskStore.setState({
sessionId: 'session-1',
tasks: [makeTask('session-1', 'in_progress')],
expanded: true,
completedAndDismissed: false,
dismissedCompletionKey: null,
})
useCLITaskStore.getState().setTasksFromTodos([
{ content: 'Session 2 task', status: 'completed' },
], 'session-2')
expect(useCLITaskStore.getState().tasks).toMatchObject([
{ taskListId: 'session-1', subject: 'Keep current session isolated' },
])
})
it('does not reset completed tasks for a different session', async () => {
useCLITaskStore.setState({
sessionId: 'session-1',
tasks: [makeTask('session-1', 'completed')],
expanded: true,
completedAndDismissed: false,
dismissedCompletionKey: null,
})
await useCLITaskStore.getState().resetCompletedTasks('session-2')
expect(vi.mocked(cliTasksApi.resetTaskList)).not.toHaveBeenCalled()
expect(useCLITaskStore.getState().tasks).toMatchObject([
{ taskListId: 'session-1', status: 'completed' },
])
})
})