MOPC-App/tests/unit/stage-assignment.test.ts

171 lines
5.4 KiB
TypeScript
Raw Normal View History

/**
* 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<string, number>()
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)
})
})