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[] }) => void) | null = null vi.mocked(cliTasksApi.getTasksForList).mockImplementation( (sessionId: string) => new Promise<{ tasks: ReturnType[] }>((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' }, ]) }) })