* 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
131 lines
4.1 KiB
TypeScript
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')
|
|
})
|
|
})
|