import { beforeEach, describe, expect, it, vi } from 'vitest' import { useSoloStore } from '@/stores/solo-store' import type { RealtimeEvent } from '@/stores/solo-store' import type { SoloTask, SoloWorkspaceSnapshot } from '@/stores/solo-workspace/types' function baseTask(id: string, overrides: Partial = {}): SoloTask { return { id, title: `Task ${id}`, description: null, implementation_plan: null, priority: 1, sort_order: 1, status: 'TO_DO', verify_only: false, verify_required: 'ALIGNED_OR_PARTIAL', story_id: 'story-1', story_code: 'ST-1', story_title: 'Story 1', task_code: `ST-1.${id}`, pbi_code: null, pbi_title: null, pbi_description: null, ...overrides, } } function snapshot(tasks: SoloTask[]): SoloWorkspaceSnapshot { return { product: { id: 'prod-1', name: 'Product 1' }, sprint: { id: 'sprint-1', sprint_goal: 'Goal' }, activeUserId: 'user-1', tasks, unassignedStories: [ { id: 'story-b', code: 'ST-2', title: 'Story B', tasks: [] }, { id: 'story-a', code: 'ST-1', title: 'Story A', tasks: [] }, ], } } function taskEvent(overrides: Partial): RealtimeEvent { return { op: 'U', entity: 'task', id: 'task-1', story_id: 'story-1', product_id: 'prod-1', sprint_id: 'sprint-1', assignee_id: 'user-1', ...overrides, } } beforeEach(() => { useSoloStore.setState({ context: { activeProduct: null, activeSprint: null, activeUserId: null }, entities: { tasksById: {}, unassignedStoriesById: {}, jobsByTaskId: {} }, relations: { taskIdsByColumn: { TO_DO: [], IN_PROGRESS: [], DONE: [] }, unassignedStoryIds: [], }, loading: { loadedProductId: null, loadedSprintId: null, loadingSprintId: null, activeRequestId: null, }, sync: { realtimeStatus: 'connecting', showConnectingIndicator: false, lastEventAt: null, lastResyncAt: null, resyncReason: null, }, pendingOps: new Set(), tasks: {}, unassignedStoriesById: {}, claudeJobsByTaskId: {}, }) vi.restoreAllMocks() }) describe('solo workspace store', () => { it('hydrateert genormaliseerde taken, kolomrelaties en unassigned stories', () => { useSoloStore.getState().hydrateSnapshot( snapshot([ baseTask('task-2', { status: 'DONE', sort_order: 2 }), baseTask('task-1', { status: 'TO_DO', sort_order: 1 }), baseTask('task-3', { status: 'REVIEW', sort_order: 3 }), ]), ) const s = useSoloStore.getState() expect(s.context.activeSprint?.id).toBe('sprint-1') expect(s.relations.taskIdsByColumn.TO_DO).toEqual(['task-1']) expect(s.relations.taskIdsByColumn.IN_PROGRESS).toEqual(['task-3']) expect(s.relations.taskIdsByColumn.DONE).toEqual(['task-2']) expect(s.relations.unassignedStoryIds).toEqual(['story-a', 'story-b']) }) it('past realtime task updates toe en herbouwt kolomrelaties', () => { useSoloStore.getState().hydrateSnapshot(snapshot([baseTask('task-1')])) useSoloStore.getState().handleRealtimeEvent( taskEvent({ status: 'DONE', sort_order: 5, title: 'Done task' }), ) const s = useSoloStore.getState() expect(s.tasks['task-1'].status).toBe('DONE') expect(s.tasks['task-1'].title).toBe('Done task') expect(s.relations.taskIdsByColumn.TO_DO).toEqual([]) expect(s.relations.taskIdsByColumn.DONE).toEqual(['task-1']) }) it('resynct actieve scopes via de solo-workspace route', async () => { useSoloStore.getState().hydrateSnapshot(snapshot([baseTask('task-1')])) const next = snapshot([baseTask('task-1', { status: 'IN_PROGRESS' })]) const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response(JSON.stringify(next), { status: 200 }), ) await useSoloStore.getState().resyncActiveScopes('manual') expect(fetchSpy).toHaveBeenCalledWith( '/api/products/prod-1/solo-workspace?sprint_id=sprint-1', expect.objectContaining({ cache: 'no-store' }), ) const s = useSoloStore.getState() expect(s.tasks['task-1'].status).toBe('IN_PROGRESS') expect(s.sync.resyncReason).toBe('manual') }) })