Scrum4Me/__tests__/lib/sprint-conflicts.test.ts
Madhura68 b4a515e86e feat(PBI-79/ST-1343): sprint-conflicts helper-library
- lib/sprint-conflicts.ts: drie pure/server-side helpers voor eligibility
  + cross-sprint detectie.
  - isEligibleForSprint(story): sprint_id IS NULL en status != DONE
  - partitionByEligibility(prisma, storyIds, excludeSprintId): split in
    eligible / notEligible / crossSprint met reden per story
  - getBlockingSprintMap(prisma, productId, storyIds, excludeSprintId):
    map storyId → { sprintId, sprintName } voor stories in andere OPEN sprint
- Tests: __tests__/lib/sprint-conflicts.test.ts (16 cases) — alle eligibility
  paden + cross-sprint scoping + CLOSED-sprint filtering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:45:59 +02:00

195 lines
5.9 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import type { StoryStatus } from '@prisma/client'
import {
getBlockingSprintMap,
isEligibleForSprint,
partitionByEligibility,
} from '@/lib/sprint-conflicts'
function mockPrisma(stories: Array<Record<string, unknown>>) {
return {
story: {
findMany: vi.fn().mockResolvedValue(stories),
},
} as unknown as Parameters<typeof partitionByEligibility>[0]
}
describe('isEligibleForSprint', () => {
it('returns true for OPEN story without sprint', () => {
expect(
isEligibleForSprint({ sprint_id: null, status: 'OPEN' as StoryStatus }),
).toBe(true)
})
it('returns true for IN_SPRINT story without sprint_id (edge: restoration)', () => {
expect(
isEligibleForSprint({
sprint_id: null,
status: 'IN_SPRINT' as StoryStatus,
}),
).toBe(true)
})
it('returns false for DONE story without sprint', () => {
expect(
isEligibleForSprint({ sprint_id: null, status: 'DONE' as StoryStatus }),
).toBe(false)
})
it('returns false when story is in any sprint (open status)', () => {
expect(
isEligibleForSprint({
sprint_id: 'abc',
status: 'OPEN' as StoryStatus,
}),
).toBe(false)
})
it('returns false when story is in any sprint (done status)', () => {
expect(
isEligibleForSprint({
sprint_id: 'abc',
status: 'DONE' as StoryStatus,
}),
).toBe(false)
})
})
describe('partitionByEligibility', () => {
it('returns empty partition for empty input', async () => {
const prisma = mockPrisma([])
const result = await partitionByEligibility(prisma, [])
expect(result).toEqual({ eligible: [], notEligible: [], crossSprint: [] })
})
it('classifies all eligible when stories are free + OPEN', async () => {
const prisma = mockPrisma([
{ id: 's1', sprint_id: null, status: 'OPEN', sprint: null },
{ id: 's2', sprint_id: null, status: 'IN_SPRINT', sprint: null },
])
const result = await partitionByEligibility(prisma, ['s1', 's2'])
expect(result.eligible).toEqual(['s1', 's2'])
expect(result.notEligible).toEqual([])
expect(result.crossSprint).toEqual([])
})
it('marks DONE stories as notEligible with reason=DONE', async () => {
const prisma = mockPrisma([
{ id: 's1', sprint_id: null, status: 'DONE', sprint: null },
])
const result = await partitionByEligibility(prisma, ['s1'])
expect(result.eligible).toEqual([])
expect(result.notEligible).toEqual([{ storyId: 's1', reason: 'DONE' }])
})
it('marks stories in other OPEN sprint as crossSprint + notEligible', async () => {
const prisma = mockPrisma([
{
id: 's1',
sprint_id: 'sprint-other',
status: 'IN_SPRINT',
sprint: { id: 'sprint-other', code: 'SP-2', status: 'OPEN' },
},
])
const result = await partitionByEligibility(prisma, ['s1'])
expect(result.crossSprint).toEqual([
{ storyId: 's1', sprintId: 'sprint-other', sprintName: 'SP-2' },
])
expect(result.notEligible).toEqual([
{ storyId: 's1', reason: 'IN_OTHER_SPRINT' },
])
expect(result.eligible).toEqual([])
})
it('classifies story in CLOSED sprint with status=OPEN as eligible (status reset already happened)', async () => {
const prisma = mockPrisma([
{
id: 's1',
sprint_id: null,
status: 'OPEN',
sprint: null,
},
])
const result = await partitionByEligibility(prisma, ['s1'])
expect(result.eligible).toEqual(['s1'])
})
it('does NOT mark crossSprint for stories in CLOSED other sprint', async () => {
const prisma = mockPrisma([
{
id: 's1',
sprint_id: 'sprint-closed',
status: 'DONE',
sprint: { id: 'sprint-closed', code: 'SP-C', status: 'CLOSED' },
},
])
const result = await partitionByEligibility(prisma, ['s1'])
expect(result.crossSprint).toEqual([])
expect(result.notEligible).toEqual([
{ storyId: 's1', reason: 'IN_OTHER_SPRINT' },
])
})
it('respects excludeSprintId — story in same sprint is eligible', async () => {
const prisma = mockPrisma([
{
id: 's1',
sprint_id: 'sprint-active',
status: 'IN_SPRINT',
sprint: { id: 'sprint-active', code: 'SP-A', status: 'OPEN' },
},
])
const result = await partitionByEligibility(prisma, ['s1'], 'sprint-active')
expect(result.eligible).toEqual(['s1'])
expect(result.crossSprint).toEqual([])
})
})
describe('getBlockingSprintMap', () => {
it('returns empty map for empty input', async () => {
const prisma = mockPrisma([])
const result = await getBlockingSprintMap(prisma, 'p1', [])
expect(result.size).toBe(0)
})
it('returns blocking sprint info for stories in OPEN sprints', async () => {
const prisma = mockPrisma([
{
id: 's1',
sprint_id: 'sprint-x',
sprint: { id: 'sprint-x', code: 'SP-X', status: 'OPEN' },
},
])
const result = await getBlockingSprintMap(prisma, 'p1', ['s1'])
expect(result.get('s1')).toEqual({
sprintId: 'sprint-x',
sprintName: 'SP-X',
})
})
it('excludes the active sprint from blocking', async () => {
const prisma = mockPrisma([
{
id: 's1',
sprint_id: 'sprint-active',
sprint: { id: 'sprint-active', code: 'SP-A', status: 'OPEN' },
},
])
const result = await getBlockingSprintMap(
prisma,
'p1',
['s1'],
'sprint-active',
)
expect(result.size).toBe(0)
})
it('does not include CLOSED sprints (filtered at DB query level)', async () => {
// The prisma mock receives WHERE sprint.status='OPEN' so CLOSED stories
// are already filtered out before reaching this function's mapping logic.
const prisma = mockPrisma([])
const result = await getBlockingSprintMap(prisma, 'p1', ['s1'])
expect(result.size).toBe(0)
})
})