Scrum4Me/__tests__/lib/insights/backlog-health.test.ts
janpeter visser b23db68916 feat(insights): add getBacklogHealth helper — stuck tasks + missing AC/plan counts
Three read-only counters: stories without acceptance_criteria, tasks without
implementation_plan, and top-10 IN_PROGRESS tasks stuck >7 days. All scoped
via productAccessFilter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:15:46 +02:00

82 lines
2.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
const { mockStoryCount, mockTaskCount, mockTaskFindMany } = vi.hoisted(() => ({
mockStoryCount: vi.fn(),
mockTaskCount: vi.fn(),
mockTaskFindMany: vi.fn(),
}))
vi.mock('@/lib/prisma', () => ({
prisma: {
story: { count: mockStoryCount },
task: { count: mockTaskCount, findMany: mockTaskFindMany },
},
}))
vi.mock('@/lib/product-access', () => ({
productAccessFilter: () => ({ some: 'filter' }),
}))
import { getBacklogHealth } from '@/lib/insights/backlog-health'
function makeTask(id: string, daysAgo: number) {
const updatedAt = new Date(Date.now() - daysAgo * 86_400_000)
return {
id,
title: `Task ${id}`,
updated_at: updatedAt,
story: {
product: { id: 'prod-1', name: 'My Product' },
sprint: { sprint_goal: 'Sprint goal' },
},
}
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('getBacklogHealth', () => {
it('returns all zeros when backlog is healthy', async () => {
mockStoryCount.mockResolvedValue(0)
mockTaskCount.mockResolvedValue(0)
mockTaskFindMany.mockResolvedValue([])
const result = await getBacklogHealth('user-1')
expect(result.storiesWithoutAc).toBe(0)
expect(result.tasksWithoutPlan).toBe(0)
expect(result.stuckTasks).toEqual([])
})
it('returns counts and stuck tasks when everything is flagged', async () => {
mockStoryCount.mockResolvedValue(5)
mockTaskCount.mockResolvedValue(3)
mockTaskFindMany.mockResolvedValue([makeTask('t1', 10), makeTask('t2', 8)])
const result = await getBacklogHealth('user-1')
expect(result.storiesWithoutAc).toBe(5)
expect(result.tasksWithoutPlan).toBe(3)
expect(result.stuckTasks).toHaveLength(2)
expect(result.stuckTasks[0].taskId).toBe('t1')
expect(result.stuckTasks[0].daysStuck).toBeGreaterThanOrEqual(10)
expect(result.stuckTasks[0].productName).toBe('My Product')
expect(result.stuckTasks[0].sprintGoal).toBe('Sprint goal')
})
it('mixed: some counters non-zero, one stuck task, no sprint', async () => {
mockStoryCount.mockResolvedValue(2)
mockTaskCount.mockResolvedValue(0)
const task = makeTask('t3', 14)
task.story.sprint = null as unknown as { sprint_goal: string }
mockTaskFindMany.mockResolvedValue([task])
const result = await getBacklogHealth('user-1')
expect(result.storiesWithoutAc).toBe(2)
expect(result.tasksWithoutPlan).toBe(0)
expect(result.stuckTasks[0].sprintGoal).toBeNull()
expect(result.stuckTasks[0].daysStuck).toBeGreaterThanOrEqual(14)
})
})