Scrum4Me/__tests__/stores/solo-workspace/store.test.ts
Janpeter Visser 3b5cee823c
Load/render workspace alignment (#182)
* docs: plan load render workspace alignment

* fix: normalize workspace status hydration

* fix: avoid duplicate backlog hydration load

* refactor: use sprint store active story

* refactor: migrate solo to workspace store

* chore: stabilize verification ignores
2026-05-10 07:34:58 +02:00

131 lines
4.1 KiB
TypeScript

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> = {}): 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>): 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')
})
})