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>
82 lines
2.5 KiB
TypeScript
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)
|
|
})
|
|
})
|