userTasksSlice.test.ts5.7 KBView on GitHub /**
* Tests for the UserTasksSlice additions that make the task board render solely from the slice:
* - hydrateTodoTasks — authoritative hydration of the open working set (upsert + reconcile removals)
* - task board bulk selection (x / shift+x): toggle / set / clear / anchor
*
* See TASK_SLICE_REFACTOR_DESIGN.md.
*/
import { act } from '@testing-library/react';
import { useCedarStore } from '@/modules/store';
import type { HydratedUserTask } from '@/modules/userTasks/slice/userTasksSlice';
/** Minimal HydratedUserTask for slice-logic tests — only the fields the reconciler reads matter. */
function task(id: string, status: HydratedUserTask['status'] = 'todo'): HydratedUserTask {
return {
id,
userId: 'u1',
conversationId: `conv-${id}`,
taskGroupId: null,
taskChannel: 'email',
taskType: 'response',
taskCreatedBy: 'agent',
taskActionData: null,
agentExecutionEnabled: false,
executionRunId: null,
creationRunId: null,
notes: null,
chatThreadId: null,
description: `task ${id}`,
status,
isRead: false,
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
completedAt: null,
dueDate: new Date('2026-01-02'),
taskOutput: null,
sourceThreadId: null,
sourceDocumentId: null,
sourceMarkerId: null,
tags: [],
sortOrder: 0,
sortOrderPinned: false,
};
}
const reset = () =>
useCedarStore.setState((s) => ({
...s,
tasks: {},
taskSelection: [],
taskSelectionAnchorId: null,
}));
beforeEach(reset);
afterEach(reset);
// ---------------------------------------------------------------------------
// hydrateTodoTasks
// ---------------------------------------------------------------------------
describe('hydrateTodoTasks', () => {
it('upserts all incoming tasks into the slice', () => {
act(() => useCedarStore.getState().hydrateTodoTasks([task('a'), task('b')]));
const { tasks } = useCedarStore.getState();
expect(Object.keys(tasks).sort()).toEqual(['a', 'b']);
});
it('removes a todo task absent from the authoritative list (server-completed elsewhere)', () => {
act(() => useCedarStore.getState().hydrateTodoTasks([task('a'), task('b')]));
// Next fetch no longer includes 'a' — it was completed/deleted server-side.
act(() => useCedarStore.getState().hydrateTodoTasks([task('b')]));
const { tasks } = useCedarStore.getState();
expect(Object.keys(tasks)).toEqual(['b']);
});
it('does NOT remove non-todo tasks that are absent from the list', () => {
// A 'done' task held for another surface must survive a todo-scoped hydration.
act(() => useCedarStore.setState((s) => ({ ...s, tasks: { d: task('d', 'done') } })));
act(() => useCedarStore.getState().hydrateTodoTasks([task('a')]));
const { tasks } = useCedarStore.getState();
expect(Object.keys(tasks).sort()).toEqual(['a', 'd']);
expect(tasks.d.status).toBe('done');
});
it('reflects updated fields on re-hydration (upsert, not ignore)', () => {
act(() => useCedarStore.getState().hydrateTodoTasks([task('a')]));
const updated = { ...task('a'), description: 'changed' };
act(() => useCedarStore.getState().hydrateTodoTasks([updated]));
expect(useCedarStore.getState().tasks.a.description).toBe('changed');
});
it('empty list clears the todo working set', () => {
act(() => useCedarStore.getState().hydrateTodoTasks([task('a'), task('b')]));
act(() => useCedarStore.getState().hydrateTodoTasks([]));
expect(Object.keys(useCedarStore.getState().tasks)).toEqual([]);
});
});
// ---------------------------------------------------------------------------
// Task board bulk selection
// ---------------------------------------------------------------------------
describe('toggleTaskSelection', () => {
it('adds a task when not selected', () => {
act(() => useCedarStore.getState().toggleTaskSelection('a'));
expect(useCedarStore.getState().taskSelection).toEqual(['a']);
});
it('removes a task when already selected', () => {
act(() => useCedarStore.getState().toggleTaskSelection('a'));
act(() => useCedarStore.getState().toggleTaskSelection('a'));
expect(useCedarStore.getState().taskSelection).toEqual([]);
});
it('preserves insertion order across independent toggles', () => {
act(() => useCedarStore.getState().toggleTaskSelection('a'));
act(() => useCedarStore.getState().toggleTaskSelection('b'));
expect(useCedarStore.getState().taskSelection).toEqual(['a', 'b']);
});
});
describe('setTaskSelection', () => {
it('replaces the selection and dedupes', () => {
act(() => useCedarStore.getState().setTaskSelection(['a', 'b', 'a', 'c']));
expect(useCedarStore.getState().taskSelection).toEqual(['a', 'b', 'c']);
});
});
describe('clearTaskSelection', () => {
it('clears selection and the range anchor', () => {
act(() => {
useCedarStore.getState().setTaskSelection(['a', 'b']);
useCedarStore.getState().setTaskSelectionAnchorId('a');
});
act(() => useCedarStore.getState().clearTaskSelection());
expect(useCedarStore.getState().taskSelection).toEqual([]);
expect(useCedarStore.getState().taskSelectionAnchorId).toBeNull();
});
});
describe('setTaskSelectionAnchorId / isTaskSelected', () => {
it('persists the anchor for range-select', () => {
act(() => useCedarStore.getState().setTaskSelectionAnchorId('a'));
expect(useCedarStore.getState().taskSelectionAnchorId).toBe('a');
});
it('isTaskSelected reflects membership', () => {
act(() => useCedarStore.getState().setTaskSelection(['a']));
expect(useCedarStore.getState().isTaskSelected('a')).toBe(true);
expect(useCedarStore.getState().isTaskSelected('z')).toBe(false);
});
});