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
This commit is contained in:
parent
98ee05d458
commit
3b5cee823c
28 changed files with 1845 additions and 577 deletions
131
__tests__/stores/solo-workspace/store.test.ts
Normal file
131
__tests__/stores/solo-workspace/store.test.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
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')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue