/** * U-006: Assignment — COI Conflict Excluded * U-007: Assignment — Insufficient Capacity / Overflow */ import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { prisma } from '../setup' import { createTestUser, createTestProgram, createTestPipeline, createTestTrack, createTestStage, createTestProject, createTestPSS, cleanupTestData, } from '../helpers' import { previewStageAssignment } from '@/server/services/stage-assignment' let programId: string let userIds: string[] = [] beforeAll(async () => { const program = await createTestProgram({ name: 'Assignment Test' }) programId = program.id }) afterAll(async () => { await cleanupTestData(programId, userIds) }) describe('U-006: COI Conflict Excluded', () => { it('excludes jurors with declared COI from the assignment pool', async () => { const pipeline = await createTestPipeline(programId) const track = await createTestTrack(pipeline.id) const stage = await createTestStage(track.id, { name: 'Eval Stage', stageType: 'EVALUATION', status: 'STAGE_ACTIVE', }) // Create 3 jurors const juror1 = await createTestUser('JURY_MEMBER', { name: 'Juror 1' }) const juror2 = await createTestUser('JURY_MEMBER', { name: 'Juror 2' }) const juror3 = await createTestUser('JURY_MEMBER', { name: 'Juror 3' }) userIds.push(juror1.id, juror2.id, juror3.id) // Create a project const project = await createTestProject(programId) await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' }) // Juror 1 has a COI with this project await prisma.conflictOfInterest.create({ data: { // Need an assignment first for the FK constraint assignmentId: (await prisma.assignment.create({ data: { userId: juror1.id, projectId: project.id, stageId: stage.id, method: 'MANUAL', }, })).id, userId: juror1.id, projectId: project.id, hasConflict: true, conflictType: 'personal', description: 'Test COI', }, }) const preview = await previewStageAssignment( stage.id, { requiredReviews: 2, respectCOI: true }, prisma ) // Juror 1 should NOT appear in any assignment (already assigned + COI) // The remaining assignments should come from juror2 and juror3 const assignedUserIds = preview.assignments.map(a => a.userId) expect(assignedUserIds).not.toContain(juror1.id) // Should have 2 assignments (requiredReviews=2, juror1 excluded via existing assignment + COI) // juror1 already has an existing assignment, but COI would exclude them from new ones too expect(preview.assignments.length).toBeGreaterThanOrEqual(0) }) }) describe('U-007: Insufficient Capacity / Overflow', () => { it('flags overflow when more projects than juror capacity', async () => { const pipeline = await createTestPipeline(programId) const track = await createTestTrack(pipeline.id) const stage = await createTestStage(track.id, { name: 'Overflow Stage', stageType: 'EVALUATION', status: 'STAGE_ACTIVE', }) // Create 2 jurors with maxAssignments=3 const jurorA = await createTestUser('JURY_MEMBER', { name: 'Juror A', maxAssignments: 3, }) const jurorB = await createTestUser('JURY_MEMBER', { name: 'Juror B', maxAssignments: 3, }) userIds.push(jurorA.id, jurorB.id) // Deactivate all other jury members so only our 2 test jurors are in the pool. // The service queries ALL active JURY_MEMBER users globally. await prisma.user.updateMany({ where: { role: 'JURY_MEMBER', id: { notIn: [jurorA.id, jurorB.id] }, }, data: { status: 'SUSPENDED' }, }) // Create 10 projects — total capacity is 6 assignments (2 jurors * 3 max) // With requiredReviews=2, we need 20 assignment slots for 10 projects, // but only 6 are available, so many will be unassigned const projectIds: string[] = [] for (let i = 0; i < 10; i++) { const project = await createTestProject(programId, { title: `Overflow Project ${i}`, }) await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' }) projectIds.push(project.id) } const preview = await previewStageAssignment( stage.id, { requiredReviews: 2, maxAssignmentsPerJuror: 3, }, prisma ) // Reactivate jury members for subsequent tests await prisma.user.updateMany({ where: { role: 'JURY_MEMBER', status: 'SUSPENDED', }, data: { status: 'ACTIVE' }, }) // Total capacity = 6 slots, need 20 → many projects unassigned expect(preview.stats.totalProjects).toBe(10) expect(preview.stats.totalJurors).toBe(2) // No juror should exceed their max assignments const jurorAssignmentCounts = new Map() for (const a of preview.assignments) { const current = jurorAssignmentCounts.get(a.userId) ?? 0 jurorAssignmentCounts.set(a.userId, current + 1) } for (const [, count] of jurorAssignmentCounts) { expect(count).toBeLessThanOrEqual(3) } // There should be unassigned projects (capacity 6 < needed 20) expect(preview.unassignedProjects.length).toBeGreaterThan(0) // Coverage should be < 100% expect(preview.stats.coveragePercent).toBeLessThan(100) }) })