feat(solo): previewEnqueueAllAction met blocker-detectie

Voeg previewEnqueueAllAction toe aan actions/claude-jobs.ts: haalt taken op in PBI-volgorde, filtert actieve jobs, detecteert eerste blocker (REVIEW taak of BLOCKED PBI). Retourneert tasks[], blockerIndex en blockerReason. Tests: 7 nieuwe cases voor alle blocker-scenario's en demo-blokkering.
This commit is contained in:
Scrum4Me Agent 2026-05-03 13:29:48 +02:00
commit 8018920cae
2 changed files with 194 additions and 0 deletions

View file

@ -49,6 +49,7 @@ import {
enqueueClaudeJobAction,
enqueueAllTodoJobsAction,
cancelClaudeJobAction,
previewEnqueueAllAction,
} from '@/actions/claude-jobs'
const SESSION_USER = { userId: 'user-1', isDemo: false }
@ -193,6 +194,107 @@ describe('enqueueAllTodoJobsAction', () => {
})
})
const makePbiTask = (id: string, status: string, pbiStatus = 'READY') => ({
id,
title: `Task ${id}`,
status,
story: { id: 'story-1', title: 'Story 1', code: 'ST-1', pbi: { id: 'pbi-1', status: pbiStatus, priority: 1, sort_order: 1.0 } },
})
describe('previewEnqueueAllAction', () => {
it('blocks demo user', async () => {
mockGetSession.mockResolvedValue(SESSION_DEMO)
const result = await previewEnqueueAllAction(PRODUCT_ID)
expect(result).toMatchObject({ error: 'Niet beschikbaar in demo-modus' })
expect(mockFindManyTask).not.toHaveBeenCalled()
})
it('returns error when product not accessible', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstProduct.mockResolvedValue(null)
const result = await previewEnqueueAllAction(PRODUCT_ID)
expect(result).toMatchObject({ error: 'Geen toegang tot dit product' })
expect(mockFindManyTask).not.toHaveBeenCalled()
})
it('returns empty tasks when no active sprint', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
mockFindFirstSprint.mockResolvedValue(null)
const result = await previewEnqueueAllAction(PRODUCT_ID)
expect(result).toEqual({ tasks: [], blockerIndex: null, blockerReason: null })
expect(mockFindManyTask).not.toHaveBeenCalled()
})
it('returns all tasks with no blocker when only TO_DO tasks', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
mockFindManyTask.mockResolvedValue([
makePbiTask('t1', 'TO_DO'),
makePbiTask('t2', 'TO_DO'),
])
const result = await previewEnqueueAllAction(PRODUCT_ID)
expect(result).toMatchObject({ blockerIndex: null, blockerReason: null })
if (!('error' in result)) expect(result.tasks).toHaveLength(2)
})
it('detects REVIEW task as blocker at correct index', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
mockFindManyTask.mockResolvedValue([
makePbiTask('t1', 'TO_DO'),
makePbiTask('t2', 'TO_DO'),
makePbiTask('t3', 'REVIEW'),
makePbiTask('t4', 'TO_DO'),
])
const result = await previewEnqueueAllAction(PRODUCT_ID)
expect(result).toMatchObject({ blockerIndex: 2, blockerReason: 'task-review' })
if (!('error' in result)) expect(result.tasks).toHaveLength(3)
})
it('detects BLOCKED PBI as blocker at first task of that PBI', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
mockFindManyTask.mockResolvedValue([
makePbiTask('t1', 'TO_DO', 'BLOCKED'),
makePbiTask('t2', 'TO_DO', 'BLOCKED'),
])
const result = await previewEnqueueAllAction(PRODUCT_ID)
expect(result).toMatchObject({ blockerIndex: 0, blockerReason: 'pbi-blocked' })
if (!('error' in result)) expect(result.tasks).toHaveLength(1)
})
it('queries without TO_DO filter to expose REVIEW tasks', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)
mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
mockFindManyTask.mockResolvedValue([])
await previewEnqueueAllAction(PRODUCT_ID)
expect(mockFindManyTask).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.not.objectContaining({ status: 'TO_DO' }),
})
)
})
})
describe('cancelClaudeJobAction', () => {
it('happy path: cancels QUEUED job', async () => {
mockGetSession.mockResolvedValue(SESSION_USER)