Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -1,417 +1,417 @@
/**
* Test Data Factories
*
* Provides helper functions to create test data in the database.
* Each factory returns the created record and uses unique identifiers
* to avoid collisions between test files.
*/
import { randomUUID } from 'crypto'
import { prisma } from './setup'
import type {
UserRole,
StageType,
StageStatus,
TrackKind,
ProjectStageStateValue,
AssignmentMethod,
} from '@prisma/client'
export function uid(prefix = 'test'): string {
return `${prefix}-${randomUUID().slice(0, 12)}`
}
// ─── User Factory ──────────────────────────────────────────────────────────
export async function createTestUser(
role: UserRole = 'JURY_MEMBER',
overrides: Partial<{
email: string
name: string
status: string
expertiseTags: string[]
maxAssignments: number
country: string
}> = {},
) {
const id = uid('user')
return prisma.user.create({
data: {
id,
email: overrides.email ?? `${id}@test.local`,
name: overrides.name ?? `Test ${role}`,
role,
status: (overrides.status as any) ?? 'ACTIVE',
expertiseTags: overrides.expertiseTags ?? [],
maxAssignments: overrides.maxAssignments ?? null,
country: overrides.country ?? null,
},
})
}
// ─── Program Factory ───────────────────────────────────────────────────────
export async function createTestProgram(
overrides: Partial<{ name: string; year: number }> = {},
) {
const id = uid('prog')
return prisma.program.create({
data: {
id,
name: overrides.name ?? `Test Program ${id}`,
year: overrides.year ?? 2026,
status: 'ACTIVE',
},
})
}
// ─── Pipeline Factory ──────────────────────────────────────────────────────
export async function createTestPipeline(
programId: string,
overrides: Partial<{ name: string; slug: string; status: string }> = {},
) {
const id = uid('pipe')
return prisma.pipeline.create({
data: {
id,
programId,
name: overrides.name ?? `Pipeline ${id}`,
slug: overrides.slug ?? id,
status: overrides.status ?? 'DRAFT',
},
})
}
// ─── Track Factory ─────────────────────────────────────────────────────────
export async function createTestTrack(
pipelineId: string,
overrides: Partial<{
name: string
slug: string
kind: TrackKind
sortOrder: number
routingMode: string
decisionMode: string
}> = {},
) {
const id = uid('track')
return prisma.track.create({
data: {
id,
pipelineId,
name: overrides.name ?? `Track ${id}`,
slug: overrides.slug ?? id,
kind: overrides.kind ?? 'MAIN',
sortOrder: overrides.sortOrder ?? 0,
routingMode: (overrides.routingMode as any) ?? null,
decisionMode: (overrides.decisionMode as any) ?? null,
},
})
}
// ─── Stage Factory ─────────────────────────────────────────────────────────
export async function createTestStage(
trackId: string,
overrides: Partial<{
name: string
slug: string
stageType: StageType
status: StageStatus
sortOrder: number
configJson: Record<string, unknown>
windowOpenAt: Date
windowCloseAt: Date
}> = {},
) {
const id = uid('stage')
return prisma.stage.create({
data: {
id,
trackId,
name: overrides.name ?? `Stage ${id}`,
slug: overrides.slug ?? id,
stageType: overrides.stageType ?? 'EVALUATION',
status: overrides.status ?? 'STAGE_ACTIVE',
sortOrder: overrides.sortOrder ?? 0,
configJson: (overrides.configJson as any) ?? undefined,
windowOpenAt: overrides.windowOpenAt ?? null,
windowCloseAt: overrides.windowCloseAt ?? null,
},
})
}
// ─── Stage Transition Factory ──────────────────────────────────────────────
export async function createTestTransition(
fromStageId: string,
toStageId: string,
overrides: Partial<{ isDefault: boolean; guardJson: Record<string, unknown> }> = {},
) {
return prisma.stageTransition.create({
data: {
fromStageId,
toStageId,
isDefault: overrides.isDefault ?? true,
guardJson: (overrides.guardJson as any) ?? undefined,
},
})
}
// ─── Project Factory ───────────────────────────────────────────────────────
export async function createTestProject(
programId: string,
overrides: Partial<{
title: string
description: string
country: string
tags: string[]
competitionCategory: string
oceanIssue: string
}> = {},
) {
const id = uid('proj')
return prisma.project.create({
data: {
id,
programId,
title: overrides.title ?? `Test Project ${id}`,
description: overrides.description ?? 'Test project description',
country: overrides.country ?? 'France',
tags: overrides.tags ?? [],
competitionCategory: (overrides.competitionCategory as any) ?? null,
oceanIssue: (overrides.oceanIssue as any) ?? null,
},
})
}
// ─── ProjectStageState Factory ─────────────────────────────────────────────
export async function createTestPSS(
projectId: string,
trackId: string,
stageId: string,
overrides: Partial<{
state: ProjectStageStateValue
exitedAt: Date | null
}> = {},
) {
return prisma.projectStageState.create({
data: {
projectId,
trackId,
stageId,
state: overrides.state ?? 'PENDING',
exitedAt: overrides.exitedAt ?? null,
},
})
}
// ─── Assignment Factory ────────────────────────────────────────────────────
export async function createTestAssignment(
userId: string,
projectId: string,
stageId: string,
overrides: Partial<{
method: AssignmentMethod
isCompleted: boolean
}> = {},
) {
return prisma.assignment.create({
data: {
userId,
projectId,
stageId,
method: overrides.method ?? 'MANUAL',
isCompleted: overrides.isCompleted ?? false,
},
})
}
// ─── Evaluation Form Factory ───────────────────────────────────────────────
export async function createTestEvaluationForm(
stageId: string,
criteria: Array<{
id: string
label: string
scale: string
weight: number
}> = [],
) {
return prisma.evaluationForm.create({
data: {
stageId,
criteriaJson: criteria.length > 0
? criteria
: [
{ id: 'c1', label: 'Innovation', scale: '1-10', weight: 1 },
{ id: 'c2', label: 'Impact', scale: '1-10', weight: 1 },
],
isActive: true,
},
})
}
// ─── Routing Rule Factory ──────────────────────────────────────────────────
export async function createTestRoutingRule(
pipelineId: string,
destinationTrackId: string,
overrides: Partial<{
name: string
priority: number
predicateJson: Record<string, unknown>
sourceTrackId: string
destinationStageId: string
isActive: boolean
}> = {},
) {
const id = uid('rule')
return prisma.routingRule.create({
data: {
id,
pipelineId,
name: overrides.name ?? `Rule ${id}`,
destinationTrackId,
sourceTrackId: overrides.sourceTrackId ?? null,
destinationStageId: overrides.destinationStageId ?? null,
predicateJson: (overrides.predicateJson ?? { field: 'project.status', operator: 'eq', value: 'SUBMITTED' }) as any,
priority: overrides.priority ?? 0,
isActive: overrides.isActive ?? true,
},
})
}
// ─── Filtering Rule Factory ────────────────────────────────────────────────
export async function createTestFilteringRule(
stageId: string,
overrides: Partial<{
name: string
ruleType: string
configJson: Record<string, unknown>
priority: number
}> = {},
) {
return prisma.filteringRule.create({
data: {
stageId,
name: overrides.name ?? 'Test Filter Rule',
ruleType: (overrides.ruleType as any) ?? 'DOCUMENT_CHECK',
configJson: (overrides.configJson ?? { requiredFileTypes: ['EXEC_SUMMARY'], action: 'REJECT' }) as any,
priority: overrides.priority ?? 0,
isActive: true,
},
})
}
// ─── COI Factory ───────────────────────────────────────────────────────────
export async function createTestCOI(
assignmentId: string,
userId: string,
projectId: string,
hasConflict = true,
) {
return prisma.conflictOfInterest.create({
data: {
assignmentId,
userId,
projectId,
hasConflict,
conflictType: hasConflict ? 'personal' : null,
description: hasConflict ? 'Test conflict' : null,
},
})
}
// ─── Cohort + CohortProject Factory ────────────────────────────────────────
export async function createTestCohort(
stageId: string,
overrides: Partial<{
name: string
isOpen: boolean
votingMode: string
}> = {},
) {
const id = uid('cohort')
return prisma.cohort.create({
data: {
id,
stageId,
name: overrides.name ?? `Cohort ${id}`,
isOpen: overrides.isOpen ?? false,
votingMode: overrides.votingMode ?? 'simple',
},
})
}
export async function createTestCohortProject(
cohortId: string,
projectId: string,
sortOrder = 0,
) {
return prisma.cohortProject.create({
data: {
cohortId,
projectId,
sortOrder,
},
})
}
// ─── Cleanup Utility ───────────────────────────────────────────────────────
/**
* Cleanup all test data created by a specific test run.
* Pass the programId to cascade-delete most related data.
*/
export async function cleanupTestData(programId: string, userIds: string[] = []) {
// Delete in reverse dependency order — scoped by programId or userIds
if (userIds.length > 0) {
await prisma.decisionAuditLog.deleteMany({ where: { actorId: { in: userIds } } })
await prisma.overrideAction.deleteMany({ where: { actorId: { in: userIds } } })
await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } })
}
await prisma.cohortProject.deleteMany({ where: { cohort: { stage: { track: { pipeline: { programId } } } } } })
await prisma.cohort.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.liveProgressCursor.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringResult.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringRule.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringJob.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.assignmentJob.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.conflictOfInterest.deleteMany({ where: { assignment: { stage: { track: { pipeline: { programId } } } } } })
await prisma.evaluation.deleteMany({ where: { assignment: { stage: { track: { pipeline: { programId } } } } } })
await prisma.assignment.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationForm.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.fileRequirement.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.gracePeriod.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.reminderLog.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationSummary.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationDiscussion.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.projectStageState.deleteMany({ where: { track: { pipeline: { programId } } } })
await prisma.stageTransition.deleteMany({ where: { fromStage: { track: { pipeline: { programId } } } } })
await prisma.routingRule.deleteMany({ where: { pipeline: { programId } } })
await prisma.awardEligibility.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.awardVote.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.awardJuror.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.specialAward.deleteMany({ where: { programId } })
await prisma.stage.deleteMany({ where: { track: { pipeline: { programId } } } })
await prisma.track.deleteMany({ where: { pipeline: { programId } } })
await prisma.pipeline.deleteMany({ where: { programId } })
await prisma.projectStatusHistory.deleteMany({ where: { project: { programId } } })
await prisma.projectFile.deleteMany({ where: { project: { programId } } })
await prisma.projectTag.deleteMany({ where: { project: { programId } } })
await prisma.project.deleteMany({ where: { programId } })
await prisma.program.deleteMany({ where: { id: programId } })
if (userIds.length > 0) {
await prisma.user.deleteMany({ where: { id: { in: userIds } } })
}
}
/**
* Test Data Factories
*
* Provides helper functions to create test data in the database.
* Each factory returns the created record and uses unique identifiers
* to avoid collisions between test files.
*/
import { randomUUID } from 'crypto'
import { prisma } from './setup'
import type {
UserRole,
StageType,
StageStatus,
TrackKind,
ProjectStageStateValue,
AssignmentMethod,
} from '@prisma/client'
export function uid(prefix = 'test'): string {
return `${prefix}-${randomUUID().slice(0, 12)}`
}
// ─── User Factory ──────────────────────────────────────────────────────────
export async function createTestUser(
role: UserRole = 'JURY_MEMBER',
overrides: Partial<{
email: string
name: string
status: string
expertiseTags: string[]
maxAssignments: number
country: string
}> = {},
) {
const id = uid('user')
return prisma.user.create({
data: {
id,
email: overrides.email ?? `${id}@test.local`,
name: overrides.name ?? `Test ${role}`,
role,
status: (overrides.status as any) ?? 'ACTIVE',
expertiseTags: overrides.expertiseTags ?? [],
maxAssignments: overrides.maxAssignments ?? null,
country: overrides.country ?? null,
},
})
}
// ─── Program Factory ───────────────────────────────────────────────────────
export async function createTestProgram(
overrides: Partial<{ name: string; year: number }> = {},
) {
const id = uid('prog')
return prisma.program.create({
data: {
id,
name: overrides.name ?? `Test Program ${id}`,
year: overrides.year ?? 2026,
status: 'ACTIVE',
},
})
}
// ─── Pipeline Factory ──────────────────────────────────────────────────────
export async function createTestPipeline(
programId: string,
overrides: Partial<{ name: string; slug: string; status: string }> = {},
) {
const id = uid('pipe')
return prisma.pipeline.create({
data: {
id,
programId,
name: overrides.name ?? `Pipeline ${id}`,
slug: overrides.slug ?? id,
status: overrides.status ?? 'DRAFT',
},
})
}
// ─── Track Factory ─────────────────────────────────────────────────────────
export async function createTestTrack(
pipelineId: string,
overrides: Partial<{
name: string
slug: string
kind: TrackKind
sortOrder: number
routingMode: string
decisionMode: string
}> = {},
) {
const id = uid('track')
return prisma.track.create({
data: {
id,
pipelineId,
name: overrides.name ?? `Track ${id}`,
slug: overrides.slug ?? id,
kind: overrides.kind ?? 'MAIN',
sortOrder: overrides.sortOrder ?? 0,
routingMode: (overrides.routingMode as any) ?? null,
decisionMode: (overrides.decisionMode as any) ?? null,
},
})
}
// ─── Stage Factory ─────────────────────────────────────────────────────────
export async function createTestStage(
trackId: string,
overrides: Partial<{
name: string
slug: string
stageType: StageType
status: StageStatus
sortOrder: number
configJson: Record<string, unknown>
windowOpenAt: Date
windowCloseAt: Date
}> = {},
) {
const id = uid('stage')
return prisma.stage.create({
data: {
id,
trackId,
name: overrides.name ?? `Stage ${id}`,
slug: overrides.slug ?? id,
stageType: overrides.stageType ?? 'EVALUATION',
status: overrides.status ?? 'STAGE_ACTIVE',
sortOrder: overrides.sortOrder ?? 0,
configJson: (overrides.configJson as any) ?? undefined,
windowOpenAt: overrides.windowOpenAt ?? null,
windowCloseAt: overrides.windowCloseAt ?? null,
},
})
}
// ─── Stage Transition Factory ──────────────────────────────────────────────
export async function createTestTransition(
fromStageId: string,
toStageId: string,
overrides: Partial<{ isDefault: boolean; guardJson: Record<string, unknown> }> = {},
) {
return prisma.stageTransition.create({
data: {
fromStageId,
toStageId,
isDefault: overrides.isDefault ?? true,
guardJson: (overrides.guardJson as any) ?? undefined,
},
})
}
// ─── Project Factory ───────────────────────────────────────────────────────
export async function createTestProject(
programId: string,
overrides: Partial<{
title: string
description: string
country: string
tags: string[]
competitionCategory: string
oceanIssue: string
}> = {},
) {
const id = uid('proj')
return prisma.project.create({
data: {
id,
programId,
title: overrides.title ?? `Test Project ${id}`,
description: overrides.description ?? 'Test project description',
country: overrides.country ?? 'France',
tags: overrides.tags ?? [],
competitionCategory: (overrides.competitionCategory as any) ?? null,
oceanIssue: (overrides.oceanIssue as any) ?? null,
},
})
}
// ─── ProjectStageState Factory ─────────────────────────────────────────────
export async function createTestPSS(
projectId: string,
trackId: string,
stageId: string,
overrides: Partial<{
state: ProjectStageStateValue
exitedAt: Date | null
}> = {},
) {
return prisma.projectStageState.create({
data: {
projectId,
trackId,
stageId,
state: overrides.state ?? 'PENDING',
exitedAt: overrides.exitedAt ?? null,
},
})
}
// ─── Assignment Factory ────────────────────────────────────────────────────
export async function createTestAssignment(
userId: string,
projectId: string,
stageId: string,
overrides: Partial<{
method: AssignmentMethod
isCompleted: boolean
}> = {},
) {
return prisma.assignment.create({
data: {
userId,
projectId,
stageId,
method: overrides.method ?? 'MANUAL',
isCompleted: overrides.isCompleted ?? false,
},
})
}
// ─── Evaluation Form Factory ───────────────────────────────────────────────
export async function createTestEvaluationForm(
stageId: string,
criteria: Array<{
id: string
label: string
scale: string
weight: number
}> = [],
) {
return prisma.evaluationForm.create({
data: {
stageId,
criteriaJson: criteria.length > 0
? criteria
: [
{ id: 'c1', label: 'Innovation', scale: '1-10', weight: 1 },
{ id: 'c2', label: 'Impact', scale: '1-10', weight: 1 },
],
isActive: true,
},
})
}
// ─── Routing Rule Factory ──────────────────────────────────────────────────
export async function createTestRoutingRule(
pipelineId: string,
destinationTrackId: string,
overrides: Partial<{
name: string
priority: number
predicateJson: Record<string, unknown>
sourceTrackId: string
destinationStageId: string
isActive: boolean
}> = {},
) {
const id = uid('rule')
return prisma.routingRule.create({
data: {
id,
pipelineId,
name: overrides.name ?? `Rule ${id}`,
destinationTrackId,
sourceTrackId: overrides.sourceTrackId ?? null,
destinationStageId: overrides.destinationStageId ?? null,
predicateJson: (overrides.predicateJson ?? { field: 'project.status', operator: 'eq', value: 'SUBMITTED' }) as any,
priority: overrides.priority ?? 0,
isActive: overrides.isActive ?? true,
},
})
}
// ─── Filtering Rule Factory ────────────────────────────────────────────────
export async function createTestFilteringRule(
stageId: string,
overrides: Partial<{
name: string
ruleType: string
configJson: Record<string, unknown>
priority: number
}> = {},
) {
return prisma.filteringRule.create({
data: {
stageId,
name: overrides.name ?? 'Test Filter Rule',
ruleType: (overrides.ruleType as any) ?? 'DOCUMENT_CHECK',
configJson: (overrides.configJson ?? { requiredFileTypes: ['EXEC_SUMMARY'], action: 'REJECT' }) as any,
priority: overrides.priority ?? 0,
isActive: true,
},
})
}
// ─── COI Factory ───────────────────────────────────────────────────────────
export async function createTestCOI(
assignmentId: string,
userId: string,
projectId: string,
hasConflict = true,
) {
return prisma.conflictOfInterest.create({
data: {
assignmentId,
userId,
projectId,
hasConflict,
conflictType: hasConflict ? 'personal' : null,
description: hasConflict ? 'Test conflict' : null,
},
})
}
// ─── Cohort + CohortProject Factory ────────────────────────────────────────
export async function createTestCohort(
stageId: string,
overrides: Partial<{
name: string
isOpen: boolean
votingMode: string
}> = {},
) {
const id = uid('cohort')
return prisma.cohort.create({
data: {
id,
stageId,
name: overrides.name ?? `Cohort ${id}`,
isOpen: overrides.isOpen ?? false,
votingMode: overrides.votingMode ?? 'simple',
},
})
}
export async function createTestCohortProject(
cohortId: string,
projectId: string,
sortOrder = 0,
) {
return prisma.cohortProject.create({
data: {
cohortId,
projectId,
sortOrder,
},
})
}
// ─── Cleanup Utility ───────────────────────────────────────────────────────
/**
* Cleanup all test data created by a specific test run.
* Pass the programId to cascade-delete most related data.
*/
export async function cleanupTestData(programId: string, userIds: string[] = []) {
// Delete in reverse dependency order — scoped by programId or userIds
if (userIds.length > 0) {
await prisma.decisionAuditLog.deleteMany({ where: { actorId: { in: userIds } } })
await prisma.overrideAction.deleteMany({ where: { actorId: { in: userIds } } })
await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } })
}
await prisma.cohortProject.deleteMany({ where: { cohort: { stage: { track: { pipeline: { programId } } } } } })
await prisma.cohort.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.liveProgressCursor.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringResult.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringRule.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringJob.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.assignmentJob.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.conflictOfInterest.deleteMany({ where: { assignment: { stage: { track: { pipeline: { programId } } } } } })
await prisma.evaluation.deleteMany({ where: { assignment: { stage: { track: { pipeline: { programId } } } } } })
await prisma.assignment.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationForm.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.fileRequirement.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.gracePeriod.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.reminderLog.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationSummary.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationDiscussion.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.projectStageState.deleteMany({ where: { track: { pipeline: { programId } } } })
await prisma.stageTransition.deleteMany({ where: { fromStage: { track: { pipeline: { programId } } } } })
await prisma.routingRule.deleteMany({ where: { pipeline: { programId } } })
await prisma.awardEligibility.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.awardVote.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.awardJuror.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.specialAward.deleteMany({ where: { programId } })
await prisma.stage.deleteMany({ where: { track: { pipeline: { programId } } } })
await prisma.track.deleteMany({ where: { pipeline: { programId } } })
await prisma.pipeline.deleteMany({ where: { programId } })
await prisma.projectStatusHistory.deleteMany({ where: { project: { programId } } })
await prisma.projectFile.deleteMany({ where: { project: { programId } } })
await prisma.projectTag.deleteMany({ where: { project: { programId } } })
await prisma.project.deleteMany({ where: { programId } })
await prisma.program.deleteMany({ where: { id: programId } })
if (userIds.length > 0) {
await prisma.user.deleteMany({ where: { id: { in: userIds } } })
}
}

View File

@@ -1,92 +1,92 @@
/**
* I-005: Assignment API — Preview vs Execute Parity
*/
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, executeStageAssignment } from '@/server/services/stage-assignment'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Assignment Preview Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-005: Assignment Preview vs Execute Parity', () => {
it('preview and execute produce matching assignment pairs', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Assignment Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Create 3 jurors
const juror1 = await createTestUser('JURY_MEMBER', { name: 'Juror Preview 1' })
const juror2 = await createTestUser('JURY_MEMBER', { name: 'Juror Preview 2' })
const juror3 = await createTestUser('JURY_MEMBER', { name: 'Juror Preview 3' })
userIds.push(juror1.id, juror2.id, juror3.id)
// Create 2 projects
const proj1 = await createTestProject(programId, { title: 'Preview P1' })
const proj2 = await createTestProject(programId, { title: 'Preview P2' })
await createTestPSS(proj1.id, track.id, stage.id, { state: 'PENDING' })
await createTestPSS(proj2.id, track.id, stage.id, { state: 'PENDING' })
const config = { requiredReviews: 2 }
// Step 1: Preview
const preview = await previewStageAssignment(stage.id, config, prisma)
expect(preview.assignments.length).toBeGreaterThan(0)
expect(preview.stats.totalProjects).toBe(2)
// Step 2: Execute with the same pairs from preview
const assignmentInputs = preview.assignments.map(a => ({
userId: a.userId,
projectId: a.projectId,
}))
const execResult = await executeStageAssignment(
stage.id, assignmentInputs, admin.id, prisma
)
expect(execResult.created).toBe(assignmentInputs.length)
expect(execResult.errors).toHaveLength(0)
// Step 3: Verify all assignments exist in database
const dbAssignments = await prisma.assignment.findMany({
where: { stageId: stage.id },
})
expect(dbAssignments.length).toBe(assignmentInputs.length)
// Verify each preview pair has a matching DB record
for (const input of assignmentInputs) {
const match = dbAssignments.find(
a => a.userId === input.userId && a.projectId === input.projectId
)
expect(match).toBeDefined()
}
})
})
/**
* I-005: Assignment API — Preview vs Execute Parity
*/
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, executeStageAssignment } from '@/server/services/stage-assignment'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Assignment Preview Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-005: Assignment Preview vs Execute Parity', () => {
it('preview and execute produce matching assignment pairs', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Assignment Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Create 3 jurors
const juror1 = await createTestUser('JURY_MEMBER', { name: 'Juror Preview 1' })
const juror2 = await createTestUser('JURY_MEMBER', { name: 'Juror Preview 2' })
const juror3 = await createTestUser('JURY_MEMBER', { name: 'Juror Preview 3' })
userIds.push(juror1.id, juror2.id, juror3.id)
// Create 2 projects
const proj1 = await createTestProject(programId, { title: 'Preview P1' })
const proj2 = await createTestProject(programId, { title: 'Preview P2' })
await createTestPSS(proj1.id, track.id, stage.id, { state: 'PENDING' })
await createTestPSS(proj2.id, track.id, stage.id, { state: 'PENDING' })
const config = { requiredReviews: 2 }
// Step 1: Preview
const preview = await previewStageAssignment(stage.id, config, prisma)
expect(preview.assignments.length).toBeGreaterThan(0)
expect(preview.stats.totalProjects).toBe(2)
// Step 2: Execute with the same pairs from preview
const assignmentInputs = preview.assignments.map(a => ({
userId: a.userId,
projectId: a.projectId,
}))
const execResult = await executeStageAssignment(
stage.id, assignmentInputs, admin.id, prisma
)
expect(execResult.created).toBe(assignmentInputs.length)
expect(execResult.errors).toHaveLength(0)
// Step 3: Verify all assignments exist in database
const dbAssignments = await prisma.assignment.findMany({
where: { stageId: stage.id },
})
expect(dbAssignments.length).toBe(assignmentInputs.length)
// Verify each preview pair has a matching DB record
for (const input of assignmentInputs) {
const match = dbAssignments.find(
a => a.userId === input.userId && a.projectId === input.projectId
)
expect(match).toBeDefined()
}
})
})

View File

@@ -1,99 +1,99 @@
/**
* I-004: Award Exclusive Routing — Exclusive Route Removes from Main
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
createTestRoutingRule,
cleanupTestData,
} from '../helpers'
import { evaluateRoutingRules, executeRouting } from '@/server/services/routing-engine'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Award Exclusive Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-004: Award Exclusive Routing', () => {
it('exclusive routing exits all active PSS and creates new PSS in destination', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const mainTrack = await createTestTrack(pipeline.id, {
name: 'Main Track',
kind: 'MAIN',
sortOrder: 0,
})
const mainStage = await createTestStage(mainTrack.id, {
name: 'Main Eval',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Exclusive award track
const exclusiveTrack = await createTestTrack(pipeline.id, {
name: 'Exclusive Award',
kind: 'AWARD',
sortOrder: 1,
routingMode: 'EXCLUSIVE',
})
const exclusiveStage = await createTestStage(exclusiveTrack.id, {
name: 'Exclusive Eval',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Routing rule with always-matching predicate
await createTestRoutingRule(pipeline.id, exclusiveTrack.id, {
name: 'Exclusive Route',
priority: 0,
predicateJson: { field: 'project.country', operator: 'eq', value: 'Monaco' },
})
// Create project with active PSS in main track
const project = await createTestProject(programId, { country: 'Monaco' })
await createTestPSS(project.id, mainTrack.id, mainStage.id, { state: 'IN_PROGRESS' })
// Evaluate routing
const matchedRule = await evaluateRoutingRules(
project.id, mainStage.id, pipeline.id, prisma
)
expect(matchedRule).not.toBeNull()
expect(matchedRule!.routingMode).toBe('EXCLUSIVE')
// Execute exclusive routing
const routeResult = await executeRouting(project.id, matchedRule!, admin.id, prisma)
expect(routeResult.success).toBe(true)
// Verify: Main track PSS should be exited with state ROUTED
const mainPSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, trackId: mainTrack.id },
})
expect(mainPSS!.exitedAt).not.toBeNull()
expect(mainPSS!.state).toBe('ROUTED')
// Verify: Exclusive track PSS should be active
const exclusivePSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, trackId: exclusiveTrack.id, exitedAt: null },
})
expect(exclusivePSS).not.toBeNull()
expect(exclusivePSS!.state).toBe('PENDING')
})
})
/**
* I-004: Award Exclusive Routing — Exclusive Route Removes from Main
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
createTestRoutingRule,
cleanupTestData,
} from '../helpers'
import { evaluateRoutingRules, executeRouting } from '@/server/services/routing-engine'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Award Exclusive Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-004: Award Exclusive Routing', () => {
it('exclusive routing exits all active PSS and creates new PSS in destination', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const mainTrack = await createTestTrack(pipeline.id, {
name: 'Main Track',
kind: 'MAIN',
sortOrder: 0,
})
const mainStage = await createTestStage(mainTrack.id, {
name: 'Main Eval',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Exclusive award track
const exclusiveTrack = await createTestTrack(pipeline.id, {
name: 'Exclusive Award',
kind: 'AWARD',
sortOrder: 1,
routingMode: 'EXCLUSIVE',
})
const exclusiveStage = await createTestStage(exclusiveTrack.id, {
name: 'Exclusive Eval',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Routing rule with always-matching predicate
await createTestRoutingRule(pipeline.id, exclusiveTrack.id, {
name: 'Exclusive Route',
priority: 0,
predicateJson: { field: 'project.country', operator: 'eq', value: 'Monaco' },
})
// Create project with active PSS in main track
const project = await createTestProject(programId, { country: 'Monaco' })
await createTestPSS(project.id, mainTrack.id, mainStage.id, { state: 'IN_PROGRESS' })
// Evaluate routing
const matchedRule = await evaluateRoutingRules(
project.id, mainStage.id, pipeline.id, prisma
)
expect(matchedRule).not.toBeNull()
expect(matchedRule!.routingMode).toBe('EXCLUSIVE')
// Execute exclusive routing
const routeResult = await executeRouting(project.id, matchedRule!, admin.id, prisma)
expect(routeResult.success).toBe(true)
// Verify: Main track PSS should be exited with state ROUTED
const mainPSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, trackId: mainTrack.id },
})
expect(mainPSS!.exitedAt).not.toBeNull()
expect(mainPSS!.state).toBe('ROUTED')
// Verify: Exclusive track PSS should be active
const exclusivePSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, trackId: exclusiveTrack.id, exitedAt: null },
})
expect(exclusivePSS).not.toBeNull()
expect(exclusivePSS!.state).toBe('PENDING')
})
})

View File

@@ -1,156 +1,156 @@
/**
* I-007: Cohort Voting — Closed Window Submit
*
* Tests that audience votes are rejected when the cohort voting window is closed.
* The castVote procedure checks for an open cohort before accepting votes.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestCohort,
createTestCohortProject,
cleanupTestData,
} from '../helpers'
import { closeCohortWindow, openCohortWindow } from '@/server/services/live-control'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Cohort Voting Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-007: Cohort Voting — Closed Window Submit', () => {
it('cohort starts closed, opens, then closes correctly', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Live Final Voting',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const project = await createTestProject(programId, { title: 'Vote Project' })
const cohort = await createTestCohort(stage.id, {
name: 'Voting Cohort',
isOpen: false,
})
await createTestCohortProject(cohort.id, project.id, 0)
// Verify cohort starts closed
const initialCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(initialCohort!.isOpen).toBe(false)
// Open the cohort window
const openResult = await openCohortWindow(cohort.id, admin.id, prisma)
expect(openResult.success).toBe(true)
const openedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(openedCohort!.isOpen).toBe(true)
expect(openedCohort!.windowOpenAt).not.toBeNull()
// Close the cohort window
const closeResult = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(closeResult.success).toBe(true)
const closedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(closedCohort!.isOpen).toBe(false)
expect(closedCohort!.windowCloseAt).not.toBeNull()
})
it('rejects opening an already-open cohort', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Double Open Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const cohort = await createTestCohort(stage.id, {
name: 'Already Open Cohort',
isOpen: true, // Already open
})
const result = await openCohortWindow(cohort.id, admin.id, prisma)
expect(result.success).toBe(false)
expect(result.errors!.some(e => e.includes('already open'))).toBe(true)
})
it('rejects closing an already-closed cohort', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Double Close Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const cohort = await createTestCohort(stage.id, {
name: 'Already Closed Cohort',
isOpen: false,
})
const result = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(result.success).toBe(false)
expect(result.errors!.some(e => e.includes('already closed'))).toBe(true)
})
it('creates audit trail for cohort window operations', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Audit Trail Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const cohort = await createTestCohort(stage.id, {
name: 'Audit Cohort',
isOpen: false,
})
// Open then close — verify both succeed
const openRes = await openCohortWindow(cohort.id, admin.id, prisma)
expect(openRes.success).toBe(true)
const closeRes = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(closeRes.success).toBe(true)
// Verify audit trail
const auditLogs = await prisma.decisionAuditLog.findMany({
where: {
entityType: 'Cohort',
entityId: cohort.id,
},
orderBy: { createdAt: 'asc' },
})
expect(auditLogs.length).toBe(2)
expect(auditLogs[0].eventType).toBe('live.cohort_opened')
expect(auditLogs[1].eventType).toBe('live.cohort_closed')
})
})
/**
* I-007: Cohort Voting — Closed Window Submit
*
* Tests that audience votes are rejected when the cohort voting window is closed.
* The castVote procedure checks for an open cohort before accepting votes.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestCohort,
createTestCohortProject,
cleanupTestData,
} from '../helpers'
import { closeCohortWindow, openCohortWindow } from '@/server/services/live-control'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Cohort Voting Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-007: Cohort Voting — Closed Window Submit', () => {
it('cohort starts closed, opens, then closes correctly', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Live Final Voting',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const project = await createTestProject(programId, { title: 'Vote Project' })
const cohort = await createTestCohort(stage.id, {
name: 'Voting Cohort',
isOpen: false,
})
await createTestCohortProject(cohort.id, project.id, 0)
// Verify cohort starts closed
const initialCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(initialCohort!.isOpen).toBe(false)
// Open the cohort window
const openResult = await openCohortWindow(cohort.id, admin.id, prisma)
expect(openResult.success).toBe(true)
const openedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(openedCohort!.isOpen).toBe(true)
expect(openedCohort!.windowOpenAt).not.toBeNull()
// Close the cohort window
const closeResult = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(closeResult.success).toBe(true)
const closedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(closedCohort!.isOpen).toBe(false)
expect(closedCohort!.windowCloseAt).not.toBeNull()
})
it('rejects opening an already-open cohort', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Double Open Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const cohort = await createTestCohort(stage.id, {
name: 'Already Open Cohort',
isOpen: true, // Already open
})
const result = await openCohortWindow(cohort.id, admin.id, prisma)
expect(result.success).toBe(false)
expect(result.errors!.some(e => e.includes('already open'))).toBe(true)
})
it('rejects closing an already-closed cohort', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Double Close Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const cohort = await createTestCohort(stage.id, {
name: 'Already Closed Cohort',
isOpen: false,
})
const result = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(result.success).toBe(false)
expect(result.errors!.some(e => e.includes('already closed'))).toBe(true)
})
it('creates audit trail for cohort window operations', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Audit Trail Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const cohort = await createTestCohort(stage.id, {
name: 'Audit Cohort',
isOpen: false,
})
// Open then close — verify both succeed
const openRes = await openCohortWindow(cohort.id, admin.id, prisma)
expect(openRes.success).toBe(true)
const closeRes = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(closeRes.success).toBe(true)
// Verify audit trail
const auditLogs = await prisma.decisionAuditLog.findMany({
where: {
entityType: 'Cohort',
entityId: cohort.id,
},
orderBy: { createdAt: 'asc' },
})
expect(auditLogs.length).toBe(2)
expect(auditLogs[0].eventType).toBe('live.cohort_opened')
expect(auditLogs[1].eventType).toBe('live.cohort_closed')
})
})

View File

@@ -1,115 +1,115 @@
/**
* I-008: Decision Audit — Override Applied with Immutable Timeline
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
cleanupTestData,
} from '../helpers'
import { decisionRouter } from '@/server/routers/decision'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Decision Audit Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-008: Decision Audit — Override with Immutable Timeline', () => {
it('creates OverrideAction and DecisionAuditLog preserving original state', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, { status: 'STAGE_ACTIVE' })
const project = await createTestProject(programId, { title: 'Audit Project' })
const pss = await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const ctx = createTestContext(admin)
const caller = decisionRouter.createCaller(ctx)
// Apply override: PENDING → PASSED
await caller.override({
entityType: 'ProjectStageState',
entityId: pss.id,
newValue: { state: 'PASSED' },
reasonCode: 'POLICY_EXCEPTION',
reasonText: 'Special committee decision',
})
// 1. Verify OverrideAction preserves the original state
const overrideAction = await prisma.overrideAction.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id },
})
expect(overrideAction).not.toBeNull()
const prevValue = overrideAction!.previousValue as Record<string, unknown>
expect(prevValue.state).toBe('PENDING')
expect(overrideAction!.reasonCode).toBe('POLICY_EXCEPTION')
expect(overrideAction!.reasonText).toBe('Special committee decision')
expect(overrideAction!.actorId).toBe(admin.id)
// 2. Verify DecisionAuditLog was created
const auditLog = await prisma.decisionAuditLog.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id, eventType: 'override.applied' },
})
expect(auditLog).not.toBeNull()
expect(auditLog!.actorId).toBe(admin.id)
const details = auditLog!.detailsJson as Record<string, unknown>
expect(details.reasonCode).toBe('POLICY_EXCEPTION')
// 3. Verify the actual state was updated
const updatedPSS = await prisma.projectStageState.findUnique({ where: { id: pss.id } })
expect(updatedPSS!.state).toBe('PASSED')
// 4. Verify immutable timeline via the auditTimeline procedure
const timeline = await caller.auditTimeline({
entityType: 'ProjectStageState',
entityId: pss.id,
})
expect(timeline.timeline.length).toBeGreaterThanOrEqual(1)
const overrideEntry = timeline.timeline.find(t => t.type === 'override')
expect(overrideEntry).toBeDefined()
expect((overrideEntry!.details as any).reasonCode).toBe('POLICY_EXCEPTION')
// 5. Apply a second override: PASSED → REJECTED
await caller.override({
entityType: 'ProjectStageState',
entityId: pss.id,
newValue: { state: 'REJECTED' },
reasonCode: 'DATA_CORRECTION',
reasonText: 'Correcting previous override',
})
// 6. Verify both overrides exist in the timeline
const fullTimeline = await caller.auditTimeline({
entityType: 'ProjectStageState',
entityId: pss.id,
})
const overrides = fullTimeline.timeline.filter(t => t.type === 'override')
expect(overrides.length).toBe(2)
// 7. Verify second override preserved PASSED as previous state
const secondOverride = await prisma.overrideAction.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id, reasonCode: 'DATA_CORRECTION' },
})
expect(secondOverride).not.toBeNull()
const secondPrevValue = secondOverride!.previousValue as Record<string, unknown>
expect(secondPrevValue.state).toBe('PASSED')
})
})
/**
* I-008: Decision Audit — Override Applied with Immutable Timeline
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
cleanupTestData,
} from '../helpers'
import { decisionRouter } from '@/server/routers/decision'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Decision Audit Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-008: Decision Audit — Override with Immutable Timeline', () => {
it('creates OverrideAction and DecisionAuditLog preserving original state', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, { status: 'STAGE_ACTIVE' })
const project = await createTestProject(programId, { title: 'Audit Project' })
const pss = await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const ctx = createTestContext(admin)
const caller = decisionRouter.createCaller(ctx)
// Apply override: PENDING → PASSED
await caller.override({
entityType: 'ProjectStageState',
entityId: pss.id,
newValue: { state: 'PASSED' },
reasonCode: 'POLICY_EXCEPTION',
reasonText: 'Special committee decision',
})
// 1. Verify OverrideAction preserves the original state
const overrideAction = await prisma.overrideAction.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id },
})
expect(overrideAction).not.toBeNull()
const prevValue = overrideAction!.previousValue as Record<string, unknown>
expect(prevValue.state).toBe('PENDING')
expect(overrideAction!.reasonCode).toBe('POLICY_EXCEPTION')
expect(overrideAction!.reasonText).toBe('Special committee decision')
expect(overrideAction!.actorId).toBe(admin.id)
// 2. Verify DecisionAuditLog was created
const auditLog = await prisma.decisionAuditLog.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id, eventType: 'override.applied' },
})
expect(auditLog).not.toBeNull()
expect(auditLog!.actorId).toBe(admin.id)
const details = auditLog!.detailsJson as Record<string, unknown>
expect(details.reasonCode).toBe('POLICY_EXCEPTION')
// 3. Verify the actual state was updated
const updatedPSS = await prisma.projectStageState.findUnique({ where: { id: pss.id } })
expect(updatedPSS!.state).toBe('PASSED')
// 4. Verify immutable timeline via the auditTimeline procedure
const timeline = await caller.auditTimeline({
entityType: 'ProjectStageState',
entityId: pss.id,
})
expect(timeline.timeline.length).toBeGreaterThanOrEqual(1)
const overrideEntry = timeline.timeline.find(t => t.type === 'override')
expect(overrideEntry).toBeDefined()
expect((overrideEntry!.details as any).reasonCode).toBe('POLICY_EXCEPTION')
// 5. Apply a second override: PASSED → REJECTED
await caller.override({
entityType: 'ProjectStageState',
entityId: pss.id,
newValue: { state: 'REJECTED' },
reasonCode: 'DATA_CORRECTION',
reasonText: 'Correcting previous override',
})
// 6. Verify both overrides exist in the timeline
const fullTimeline = await caller.auditTimeline({
entityType: 'ProjectStageState',
entityId: pss.id,
})
const overrides = fullTimeline.timeline.filter(t => t.type === 'override')
expect(overrides.length).toBe(2)
// 7. Verify second override preserved PASSED as previous state
const secondOverride = await prisma.overrideAction.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id, reasonCode: 'DATA_CORRECTION' },
})
expect(secondOverride).not.toBeNull()
const secondPrevValue = secondOverride!.previousValue as Record<string, unknown>
expect(secondPrevValue.state).toBe('PASSED')
})
})

View File

@@ -1,139 +1,139 @@
/**
* I-006: Live Runtime — Jump / Reorder / Open-Close Windows
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestCohort,
createTestCohortProject,
cleanupTestData,
} from '../helpers'
import {
startSession,
jumpToProject,
reorderQueue,
pauseResume,
openCohortWindow,
closeCohortWindow,
} from '@/server/services/live-control'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Live Runtime Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-006: Live Runtime Operations', () => {
it('full live session lifecycle: start → jump → reorder → pause → resume → open/close window', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Live Final Session',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
// Create 4 projects
const projects = []
for (let i = 0; i < 4; i++) {
const p = await createTestProject(programId, { title: `Live Runtime P${i}` })
projects.push(p)
}
// Create cohort with projects
const cohort = await createTestCohort(stage.id, { name: 'Runtime Cohort', isOpen: false })
const cohortProjects = []
for (let i = 0; i < projects.length; i++) {
const cp = await createTestCohortProject(cohort.id, projects[i].id, i)
cohortProjects.push(cp)
}
// 1. Start session
const sessionResult = await startSession(stage.id, admin.id, prisma)
expect(sessionResult.success).toBe(true)
// Verify cursor starts at first project
let cursor = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursor!.activeProjectId).toBe(projects[0].id)
expect(cursor!.activeOrderIndex).toBe(0)
// 2. Jump to project at index 2
const jumpResult = await jumpToProject(stage.id, 2, admin.id, prisma)
expect(jumpResult.success).toBe(true)
expect(jumpResult.projectId).toBe(projects[2].id)
cursor = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursor!.activeProjectId).toBe(projects[2].id)
expect(cursor!.activeOrderIndex).toBe(2)
// 3. Reorder queue (reverse order)
const reorderedIds = [...cohortProjects].reverse().map(cp => cp.id)
const reorderResult = await reorderQueue(stage.id, reorderedIds, admin.id, prisma)
expect(reorderResult.success).toBe(true)
// Verify reorder updated sortOrder
const reorderedCPs = await prisma.cohortProject.findMany({
where: { cohortId: cohort.id },
orderBy: { sortOrder: 'asc' },
})
expect(reorderedCPs[0].projectId).toBe(projects[3].id) // Was last, now first
expect(reorderedCPs[3].projectId).toBe(projects[0].id) // Was first, now last
// 4. Pause
const pauseResult = await pauseResume(stage.id, true, admin.id, prisma)
expect(pauseResult.success).toBe(true)
cursor = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursor!.isPaused).toBe(true)
// 5. Resume
const resumeResult = await pauseResume(stage.id, false, admin.id, prisma)
expect(resumeResult.success).toBe(true)
cursor = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursor!.isPaused).toBe(false)
// 6. Open voting window
const openResult = await openCohortWindow(cohort.id, admin.id, prisma)
expect(openResult.success).toBe(true)
const openedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(openedCohort!.isOpen).toBe(true)
expect(openedCohort!.windowOpenAt).not.toBeNull()
// 7. Close voting window
const closeResult = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(closeResult.success).toBe(true)
const closedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(closedCohort!.isOpen).toBe(false)
expect(closedCohort!.windowCloseAt).not.toBeNull()
// Verify audit trail
const auditLogs = await prisma.decisionAuditLog.findMany({
where: {
OR: [
{ eventType: { startsWith: 'live.' } },
],
},
orderBy: { createdAt: 'asc' },
})
expect(auditLogs.length).toBeGreaterThanOrEqual(5) // session_started, cursor_updated, queue_reordered, paused, resumed, opened, closed
})
})
/**
* I-006: Live Runtime — Jump / Reorder / Open-Close Windows
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestCohort,
createTestCohortProject,
cleanupTestData,
} from '../helpers'
import {
startSession,
jumpToProject,
reorderQueue,
pauseResume,
openCohortWindow,
closeCohortWindow,
} from '@/server/services/live-control'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Live Runtime Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-006: Live Runtime Operations', () => {
it('full live session lifecycle: start → jump → reorder → pause → resume → open/close window', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Live Final Session',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
// Create 4 projects
const projects = []
for (let i = 0; i < 4; i++) {
const p = await createTestProject(programId, { title: `Live Runtime P${i}` })
projects.push(p)
}
// Create cohort with projects
const cohort = await createTestCohort(stage.id, { name: 'Runtime Cohort', isOpen: false })
const cohortProjects = []
for (let i = 0; i < projects.length; i++) {
const cp = await createTestCohortProject(cohort.id, projects[i].id, i)
cohortProjects.push(cp)
}
// 1. Start session
const sessionResult = await startSession(stage.id, admin.id, prisma)
expect(sessionResult.success).toBe(true)
// Verify cursor starts at first project
let cursor = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursor!.activeProjectId).toBe(projects[0].id)
expect(cursor!.activeOrderIndex).toBe(0)
// 2. Jump to project at index 2
const jumpResult = await jumpToProject(stage.id, 2, admin.id, prisma)
expect(jumpResult.success).toBe(true)
expect(jumpResult.projectId).toBe(projects[2].id)
cursor = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursor!.activeProjectId).toBe(projects[2].id)
expect(cursor!.activeOrderIndex).toBe(2)
// 3. Reorder queue (reverse order)
const reorderedIds = [...cohortProjects].reverse().map(cp => cp.id)
const reorderResult = await reorderQueue(stage.id, reorderedIds, admin.id, prisma)
expect(reorderResult.success).toBe(true)
// Verify reorder updated sortOrder
const reorderedCPs = await prisma.cohortProject.findMany({
where: { cohortId: cohort.id },
orderBy: { sortOrder: 'asc' },
})
expect(reorderedCPs[0].projectId).toBe(projects[3].id) // Was last, now first
expect(reorderedCPs[3].projectId).toBe(projects[0].id) // Was first, now last
// 4. Pause
const pauseResult = await pauseResume(stage.id, true, admin.id, prisma)
expect(pauseResult.success).toBe(true)
cursor = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursor!.isPaused).toBe(true)
// 5. Resume
const resumeResult = await pauseResume(stage.id, false, admin.id, prisma)
expect(resumeResult.success).toBe(true)
cursor = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursor!.isPaused).toBe(false)
// 6. Open voting window
const openResult = await openCohortWindow(cohort.id, admin.id, prisma)
expect(openResult.success).toBe(true)
const openedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(openedCohort!.isOpen).toBe(true)
expect(openedCohort!.windowOpenAt).not.toBeNull()
// 7. Close voting window
const closeResult = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(closeResult.success).toBe(true)
const closedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(closedCohort!.isOpen).toBe(false)
expect(closedCohort!.windowCloseAt).not.toBeNull()
// Verify audit trail
const auditLogs = await prisma.decisionAuditLog.findMany({
where: {
OR: [
{ eventType: { startsWith: 'live.' } },
],
},
orderBy: { createdAt: 'asc' },
})
expect(auditLogs.length).toBeGreaterThanOrEqual(5) // session_started, cursor_updated, queue_reordered, paused, resumed, opened, closed
})
})

View File

@@ -1,145 +1,145 @@
/**
* I-001: Pipeline CRUD — Create / Update / Publish
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
cleanupTestData,
} from '../helpers'
import { pipelineRouter } from '@/server/routers/pipeline'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Pipeline CRUD Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-001: Pipeline CRUD', () => {
it('creates a pipeline with tracks and stages via createStructure', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const ctx = createTestContext(admin)
const caller = pipelineRouter.createCaller(ctx)
const result = await caller.createStructure({
programId,
name: 'Test Pipeline',
slug: `test-pipe-${Date.now()}`,
tracks: [
{
name: 'Main Track',
slug: 'main',
kind: 'MAIN',
sortOrder: 0,
stages: [
{ name: 'Filtering', slug: 'filtering', stageType: 'FILTER', sortOrder: 0 },
{ name: 'Evaluation', slug: 'evaluation', stageType: 'EVALUATION', sortOrder: 1 },
],
},
],
})
expect(result.pipeline.id).toBeDefined()
// Verify pipeline was created
const pipeline = await prisma.pipeline.findUnique({
where: { id: result.pipeline.id },
include: {
tracks: {
include: { stages: { orderBy: { sortOrder: 'asc' } } },
},
},
})
expect(pipeline).not.toBeNull()
expect(pipeline!.name).toBe('Test Pipeline')
expect(pipeline!.tracks).toHaveLength(1)
expect(pipeline!.tracks[0].name).toBe('Main Track')
expect(pipeline!.tracks[0].stages).toHaveLength(2)
expect(pipeline!.tracks[0].stages[0].name).toBe('Filtering')
expect(pipeline!.tracks[0].stages[1].name).toBe('Evaluation')
// Verify auto-created StageTransition between stages
const transitions = await prisma.stageTransition.findMany({
where: {
fromStage: { trackId: pipeline!.tracks[0].id },
},
})
expect(transitions.length).toBeGreaterThanOrEqual(1)
})
it('updates a pipeline name and settings', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const ctx = createTestContext(admin)
const caller = pipelineRouter.createCaller(ctx)
const created = await caller.createStructure({
programId,
name: 'Original Name',
slug: `upd-pipe-${Date.now()}`,
tracks: [
{
name: 'Track 1',
slug: 'track-1',
kind: 'MAIN',
sortOrder: 0,
stages: [{ name: 'S1', slug: 's1', stageType: 'EVALUATION', sortOrder: 0 }],
},
],
})
const updated = await caller.update({
id: created.pipeline.id,
name: 'Updated Name',
settingsJson: { theme: 'blue' },
})
expect(updated.name).toBe('Updated Name')
const fetched = await prisma.pipeline.findUnique({ where: { id: created.pipeline.id } })
expect(fetched!.name).toBe('Updated Name')
expect((fetched!.settingsJson as any)?.theme).toBe('blue')
})
it('publishes a pipeline with valid structure', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const ctx = createTestContext(admin)
const caller = pipelineRouter.createCaller(ctx)
const created = await caller.createStructure({
programId,
name: 'Publish Test',
slug: `pub-pipe-${Date.now()}`,
tracks: [
{
name: 'Main',
slug: 'main',
kind: 'MAIN',
sortOrder: 0,
stages: [{ name: 'Eval', slug: 'eval', stageType: 'EVALUATION', sortOrder: 0 }],
},
],
})
const published = await caller.publish({ id: created.pipeline.id })
expect(published.status).toBe('ACTIVE')
const fetched = await prisma.pipeline.findUnique({ where: { id: created.pipeline.id } })
expect(fetched!.status).toBe('ACTIVE')
})
})
/**
* I-001: Pipeline CRUD — Create / Update / Publish
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
cleanupTestData,
} from '../helpers'
import { pipelineRouter } from '@/server/routers/pipeline'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Pipeline CRUD Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-001: Pipeline CRUD', () => {
it('creates a pipeline with tracks and stages via createStructure', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const ctx = createTestContext(admin)
const caller = pipelineRouter.createCaller(ctx)
const result = await caller.createStructure({
programId,
name: 'Test Pipeline',
slug: `test-pipe-${Date.now()}`,
tracks: [
{
name: 'Main Track',
slug: 'main',
kind: 'MAIN',
sortOrder: 0,
stages: [
{ name: 'Filtering', slug: 'filtering', stageType: 'FILTER', sortOrder: 0 },
{ name: 'Evaluation', slug: 'evaluation', stageType: 'EVALUATION', sortOrder: 1 },
],
},
],
})
expect(result.pipeline.id).toBeDefined()
// Verify pipeline was created
const pipeline = await prisma.pipeline.findUnique({
where: { id: result.pipeline.id },
include: {
tracks: {
include: { stages: { orderBy: { sortOrder: 'asc' } } },
},
},
})
expect(pipeline).not.toBeNull()
expect(pipeline!.name).toBe('Test Pipeline')
expect(pipeline!.tracks).toHaveLength(1)
expect(pipeline!.tracks[0].name).toBe('Main Track')
expect(pipeline!.tracks[0].stages).toHaveLength(2)
expect(pipeline!.tracks[0].stages[0].name).toBe('Filtering')
expect(pipeline!.tracks[0].stages[1].name).toBe('Evaluation')
// Verify auto-created StageTransition between stages
const transitions = await prisma.stageTransition.findMany({
where: {
fromStage: { trackId: pipeline!.tracks[0].id },
},
})
expect(transitions.length).toBeGreaterThanOrEqual(1)
})
it('updates a pipeline name and settings', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const ctx = createTestContext(admin)
const caller = pipelineRouter.createCaller(ctx)
const created = await caller.createStructure({
programId,
name: 'Original Name',
slug: `upd-pipe-${Date.now()}`,
tracks: [
{
name: 'Track 1',
slug: 'track-1',
kind: 'MAIN',
sortOrder: 0,
stages: [{ name: 'S1', slug: 's1', stageType: 'EVALUATION', sortOrder: 0 }],
},
],
})
const updated = await caller.update({
id: created.pipeline.id,
name: 'Updated Name',
settingsJson: { theme: 'blue' },
})
expect(updated.name).toBe('Updated Name')
const fetched = await prisma.pipeline.findUnique({ where: { id: created.pipeline.id } })
expect(fetched!.name).toBe('Updated Name')
expect((fetched!.settingsJson as any)?.theme).toBe('blue')
})
it('publishes a pipeline with valid structure', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const ctx = createTestContext(admin)
const caller = pipelineRouter.createCaller(ctx)
const created = await caller.createStructure({
programId,
name: 'Publish Test',
slug: `pub-pipe-${Date.now()}`,
tracks: [
{
name: 'Main',
slug: 'main',
kind: 'MAIN',
sortOrder: 0,
stages: [{ name: 'Eval', slug: 'eval', stageType: 'EVALUATION', sortOrder: 0 }],
},
],
})
const published = await caller.publish({ id: created.pipeline.id })
expect(published.status).toBe('ACTIVE')
const fetched = await prisma.pipeline.findUnique({ where: { id: created.pipeline.id } })
expect(fetched!.status).toBe('ACTIVE')
})
})

View File

@@ -1,166 +1,166 @@
/**
* I-002: Stage Config — Invalid Config Schema
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
cleanupTestData,
} from '../helpers'
import { stageRouter } from '@/server/routers/stage'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Stage Config Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-002: Stage Config — Valid and Invalid Updates', () => {
it('accepts valid stage config update', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Config Test Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
const updated = await caller.updateConfig({
id: stage.id,
configJson: {
requiredReviews: 3,
evaluationMode: 'CRITERIA_BASED',
},
})
expect(updated).toBeDefined()
// Verify the configJson was persisted
const fetched = await prisma.stage.findUnique({ where: { id: stage.id } })
const config = fetched!.configJson as Record<string, unknown>
expect(config.requiredReviews).toBe(3)
expect(config.evaluationMode).toBe('CRITERIA_BASED')
})
it('updates stage name via updateConfig', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Original Name',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
const updated = await caller.updateConfig({
id: stage.id,
name: 'New Name',
})
expect(updated.name).toBe('New Name')
const fetched = await prisma.stage.findUnique({ where: { id: stage.id } })
expect(fetched!.name).toBe('New Name')
})
it('updates stage window dates via updateConfig', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Window Test Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
const openAt = new Date('2026-03-01T00:00:00Z')
const closeAt = new Date('2026-04-01T00:00:00Z')
const updated = await caller.updateConfig({
id: stage.id,
windowOpenAt: openAt,
windowCloseAt: closeAt,
})
expect(updated).toBeDefined()
const fetched = await prisma.stage.findUnique({ where: { id: stage.id } })
expect(fetched!.windowOpenAt!.getTime()).toBe(openAt.getTime())
expect(fetched!.windowCloseAt!.getTime()).toBe(closeAt.getTime())
})
it('rejects invalid window dates (close before open)', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Invalid Window Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
await expect(
caller.updateConfig({
id: stage.id,
windowOpenAt: new Date('2026-04-01T00:00:00Z'),
windowCloseAt: new Date('2026-03-01T00:00:00Z'), // Before open
})
).rejects.toThrow()
})
it('opens and closes stage window via openWindow/closeWindow', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Open/Close Window Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
// Open the window
const opened = await caller.openWindow({ id: stage.id })
expect(opened.windowOpenAt).not.toBeNull()
// Close the window
const closed = await caller.closeWindow({ id: stage.id })
expect(closed.windowCloseAt).not.toBeNull()
})
})
/**
* I-002: Stage Config — Invalid Config Schema
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
cleanupTestData,
} from '../helpers'
import { stageRouter } from '@/server/routers/stage'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Stage Config Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-002: Stage Config — Valid and Invalid Updates', () => {
it('accepts valid stage config update', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Config Test Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
const updated = await caller.updateConfig({
id: stage.id,
configJson: {
requiredReviews: 3,
evaluationMode: 'CRITERIA_BASED',
},
})
expect(updated).toBeDefined()
// Verify the configJson was persisted
const fetched = await prisma.stage.findUnique({ where: { id: stage.id } })
const config = fetched!.configJson as Record<string, unknown>
expect(config.requiredReviews).toBe(3)
expect(config.evaluationMode).toBe('CRITERIA_BASED')
})
it('updates stage name via updateConfig', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Original Name',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
const updated = await caller.updateConfig({
id: stage.id,
name: 'New Name',
})
expect(updated.name).toBe('New Name')
const fetched = await prisma.stage.findUnique({ where: { id: stage.id } })
expect(fetched!.name).toBe('New Name')
})
it('updates stage window dates via updateConfig', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Window Test Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
const openAt = new Date('2026-03-01T00:00:00Z')
const closeAt = new Date('2026-04-01T00:00:00Z')
const updated = await caller.updateConfig({
id: stage.id,
windowOpenAt: openAt,
windowCloseAt: closeAt,
})
expect(updated).toBeDefined()
const fetched = await prisma.stage.findUnique({ where: { id: stage.id } })
expect(fetched!.windowOpenAt!.getTime()).toBe(openAt.getTime())
expect(fetched!.windowCloseAt!.getTime()).toBe(closeAt.getTime())
})
it('rejects invalid window dates (close before open)', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Invalid Window Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
await expect(
caller.updateConfig({
id: stage.id,
windowOpenAt: new Date('2026-04-01T00:00:00Z'),
windowCloseAt: new Date('2026-03-01T00:00:00Z'), // Before open
})
).rejects.toThrow()
})
it('opens and closes stage window via openWindow/closeWindow', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Open/Close Window Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const ctx = createTestContext(admin)
const caller = stageRouter.createCaller(ctx)
// Open the window
const opened = await caller.openWindow({ id: stage.id })
expect(opened.windowOpenAt).not.toBeNull()
// Close the window
const closed = await caller.closeWindow({ id: stage.id })
expect(closed.windowCloseAt).not.toBeNull()
})
})

View File

@@ -1,117 +1,117 @@
/**
* I-003: Transition + Routing — Filter Pass → Main + Award Parallel
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestTransition,
createTestProject,
createTestPSS,
createTestRoutingRule,
cleanupTestData,
} from '../helpers'
import { executeTransition } from '@/server/services/stage-engine'
import { evaluateRoutingRules, executeRouting } from '@/server/services/routing-engine'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Transition+Routing Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-003: Transition + Routing — Parallel Award Track', () => {
it('transitions a project then routes it to a parallel award track', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
// Main track: FILTER → EVALUATION
const mainTrack = await createTestTrack(pipeline.id, {
name: 'Main',
kind: 'MAIN',
sortOrder: 0,
})
const filterStage = await createTestStage(mainTrack.id, {
name: 'Filter',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const evalStage = await createTestStage(mainTrack.id, {
name: 'Evaluation',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
sortOrder: 1,
})
await createTestTransition(filterStage.id, evalStage.id)
// Award track (PARALLEL)
const awardTrack = await createTestTrack(pipeline.id, {
name: 'Innovation Award',
kind: 'AWARD',
sortOrder: 1,
routingMode: 'PARALLEL',
})
const awardEvalStage = await createTestStage(awardTrack.id, {
name: 'Award Eval',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
// Routing rule: projects from France → Award track (parallel)
await createTestRoutingRule(pipeline.id, awardTrack.id, {
name: 'France → Innovation Award',
priority: 0,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
// Create project in filter stage
const project = await createTestProject(programId, { country: 'France' })
await createTestPSS(project.id, mainTrack.id, filterStage.id, { state: 'PENDING' })
// Step 1: Transition from filter → evaluation
const transResult = await executeTransition(
project.id, mainTrack.id, filterStage.id, evalStage.id,
'PENDING', admin.id, prisma
)
expect(transResult.success).toBe(true)
// Step 2: Evaluate routing rules
const matchedRule = await evaluateRoutingRules(
project.id, evalStage.id, pipeline.id, prisma
)
expect(matchedRule).not.toBeNull()
expect(matchedRule!.destinationTrackId).toBe(awardTrack.id)
expect(matchedRule!.routingMode).toBe('PARALLEL')
// Step 3: Execute routing
const routeResult = await executeRouting(project.id, matchedRule!, admin.id, prisma)
expect(routeResult.success).toBe(true)
// Verify: Project should be in BOTH main eval stage AND award eval stage
const mainPSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, stageId: evalStage.id, exitedAt: null },
})
expect(mainPSS).not.toBeNull() // Still in main track
const awardPSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, stageId: awardEvalStage.id, exitedAt: null },
})
expect(awardPSS).not.toBeNull() // Also in award track
expect(awardPSS!.state).toBe('PENDING')
})
})
/**
* I-003: Transition + Routing — Filter Pass → Main + Award Parallel
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestTransition,
createTestProject,
createTestPSS,
createTestRoutingRule,
cleanupTestData,
} from '../helpers'
import { executeTransition } from '@/server/services/stage-engine'
import { evaluateRoutingRules, executeRouting } from '@/server/services/routing-engine'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Transition+Routing Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('I-003: Transition + Routing — Parallel Award Track', () => {
it('transitions a project then routes it to a parallel award track', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
// Main track: FILTER → EVALUATION
const mainTrack = await createTestTrack(pipeline.id, {
name: 'Main',
kind: 'MAIN',
sortOrder: 0,
})
const filterStage = await createTestStage(mainTrack.id, {
name: 'Filter',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const evalStage = await createTestStage(mainTrack.id, {
name: 'Evaluation',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
sortOrder: 1,
})
await createTestTransition(filterStage.id, evalStage.id)
// Award track (PARALLEL)
const awardTrack = await createTestTrack(pipeline.id, {
name: 'Innovation Award',
kind: 'AWARD',
sortOrder: 1,
routingMode: 'PARALLEL',
})
const awardEvalStage = await createTestStage(awardTrack.id, {
name: 'Award Eval',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
// Routing rule: projects from France → Award track (parallel)
await createTestRoutingRule(pipeline.id, awardTrack.id, {
name: 'France → Innovation Award',
priority: 0,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
// Create project in filter stage
const project = await createTestProject(programId, { country: 'France' })
await createTestPSS(project.id, mainTrack.id, filterStage.id, { state: 'PENDING' })
// Step 1: Transition from filter → evaluation
const transResult = await executeTransition(
project.id, mainTrack.id, filterStage.id, evalStage.id,
'PENDING', admin.id, prisma
)
expect(transResult.success).toBe(true)
// Step 2: Evaluate routing rules
const matchedRule = await evaluateRoutingRules(
project.id, evalStage.id, pipeline.id, prisma
)
expect(matchedRule).not.toBeNull()
expect(matchedRule!.destinationTrackId).toBe(awardTrack.id)
expect(matchedRule!.routingMode).toBe('PARALLEL')
// Step 3: Execute routing
const routeResult = await executeRouting(project.id, matchedRule!, admin.id, prisma)
expect(routeResult.success).toBe(true)
// Verify: Project should be in BOTH main eval stage AND award eval stage
const mainPSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, stageId: evalStage.id, exitedAt: null },
})
expect(mainPSS).not.toBeNull() // Still in main track
const awardPSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, stageId: awardEvalStage.id, exitedAt: null },
})
expect(awardPSS).not.toBeNull() // Also in award track
expect(awardPSS!.state).toBe('PENDING')
})
})

View File

@@ -1,66 +1,66 @@
/**
* Vitest global setup
*
* Provides:
* - `prisma` a PrismaClient connected to DATABASE_URL (or DATABASE_URL_TEST)
* - `createTestContext(user)` builds a tRPC-compatible context with an
* authenticated user so callers can invoke router procedures directly.
* - `createCaller(router, user)` shorthand for creating a type-safe caller
* from any tRPC router.
*
* Test isolation strategy: each test file should use `cleanupTestData()` in
* afterAll to remove data it created, keyed by unique program/user names.
*/
import { PrismaClient } from '@prisma/client'
import { afterAll } from 'vitest'
import type { UserRole } from '@prisma/client'
export const prisma = new PrismaClient({
datasourceUrl: process.env.DATABASE_URL_TEST ?? process.env.DATABASE_URL,
})
afterAll(async () => {
await prisma.$disconnect()
})
/**
* Build a fake tRPC context with an authenticated user.
* The returned object matches the shape expected by tRPC middleware
* (session.user, prisma, ip, userAgent).
*/
export function createTestContext(user: {
id: string
email: string
name?: string | null
role: UserRole
}) {
return {
session: {
user: {
id: user.id,
email: user.email,
name: user.name ?? user.email,
role: user.role,
},
expires: new Date(Date.now() + 86_400_000).toISOString(),
},
prisma,
ip: '127.0.0.1',
userAgent: 'vitest',
}
}
/**
* Create a tRPC caller for a given router, authenticated as `user`.
* Usage:
* const caller = createCaller(pipelineRouter, adminUser)
* const result = await caller.create({ ... })
*/
export function createCaller<TRouter extends { createCaller: (ctx: any) => any }>(
routerModule: { createCaller: (ctx: any) => any },
user: { id: string; email: string; name?: string | null; role: UserRole },
) {
const ctx = createTestContext(user)
return routerModule.createCaller(ctx)
}
/**
* Vitest global setup
*
* Provides:
* - `prisma` a PrismaClient connected to DATABASE_URL (or DATABASE_URL_TEST)
* - `createTestContext(user)` builds a tRPC-compatible context with an
* authenticated user so callers can invoke router procedures directly.
* - `createCaller(router, user)` shorthand for creating a type-safe caller
* from any tRPC router.
*
* Test isolation strategy: each test file should use `cleanupTestData()` in
* afterAll to remove data it created, keyed by unique program/user names.
*/
import { PrismaClient } from '@prisma/client'
import { afterAll } from 'vitest'
import type { UserRole } from '@prisma/client'
export const prisma = new PrismaClient({
datasourceUrl: process.env.DATABASE_URL_TEST ?? process.env.DATABASE_URL,
})
afterAll(async () => {
await prisma.$disconnect()
})
/**
* Build a fake tRPC context with an authenticated user.
* The returned object matches the shape expected by tRPC middleware
* (session.user, prisma, ip, userAgent).
*/
export function createTestContext(user: {
id: string
email: string
name?: string | null
role: UserRole
}) {
return {
session: {
user: {
id: user.id,
email: user.email,
name: user.name ?? user.email,
role: user.role,
},
expires: new Date(Date.now() + 86_400_000).toISOString(),
},
prisma,
ip: '127.0.0.1',
userAgent: 'vitest',
}
}
/**
* Create a tRPC caller for a given router, authenticated as `user`.
* Usage:
* const caller = createCaller(pipelineRouter, adminUser)
* const result = await caller.create({ ... })
*/
export function createCaller<TRouter extends { createCaller: (ctx: any) => any }>(
routerModule: { createCaller: (ctx: any) => any },
user: { id: string; email: string; name?: string | null; role: UserRole },
) {
const ctx = createTestContext(user)
return routerModule.createCaller(ctx)
}

View File

@@ -1,154 +1,154 @@
/**
* U-010: Award Governance — Unauthorized AWARD_MASTER
*
* Tests that non-AWARD_MASTER users cannot call finalizeWinners.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
cleanupTestData,
} from '../helpers'
import { awardRouter } from '@/server/routers/award'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Award Gov Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-010: Award Governance — Unauthorized AWARD_MASTER', () => {
it('rejects finalizeWinners when called by a JURY_MEMBER', async () => {
const jury = await createTestUser('JURY_MEMBER', { name: 'Unauthorized Jury' })
userIds.push(jury.id)
// Create award track infrastructure using admin
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await prisma.track.create({
data: {
pipelineId: pipeline.id,
name: 'Award Track',
slug: `award-${Date.now()}`,
kind: 'AWARD',
sortOrder: 1,
decisionMode: 'AWARD_MASTER_DECISION',
},
})
const stage = await createTestStage(track.id, {
name: 'Award Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Create a SpecialAward linked to the track
const award = await prisma.specialAward.create({
data: {
programId,
name: 'Best Innovation',
trackId: track.id,
status: 'VOTING_OPEN',
scoringMode: 'PICK_WINNER',
sortOrder: 0,
},
})
// Create an eligible project
const project = await createTestProject(programId, { title: 'Award Project' })
await prisma.awardEligibility.create({
data: {
awardId: award.id,
projectId: project.id,
eligible: true,
method: 'MANUAL',
},
})
// Attempt to call finalizeWinners as JURY_MEMBER — should be rejected
const juryCtx = createTestContext(jury)
const juryCaller = awardRouter.createCaller(juryCtx)
await expect(
juryCaller.finalizeWinners({
trackId: track.id,
winnerProjectId: project.id,
})
).rejects.toThrow() // Should throw FORBIDDEN or UNAUTHORIZED
// Verify the award still has no winner
const unchangedAward = await prisma.specialAward.findUnique({
where: { id: award.id },
})
expect(unchangedAward!.winnerProjectId).toBeNull()
})
it('allows SUPER_ADMIN to finalize winners (awardMasterProcedure permits)', async () => {
const admin = await createTestUser('SUPER_ADMIN', { name: 'Admin Award Master' })
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await prisma.track.create({
data: {
pipelineId: pipeline.id,
name: 'Admin Award Track',
slug: `admin-award-${Date.now()}`,
kind: 'AWARD',
sortOrder: 2,
decisionMode: 'AWARD_MASTER_DECISION',
},
})
await createTestStage(track.id, {
name: 'Admin Award Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const award = await prisma.specialAward.create({
data: {
programId,
name: 'Admin Award',
trackId: track.id,
status: 'VOTING_OPEN',
scoringMode: 'PICK_WINNER',
sortOrder: 0,
},
})
const project = await createTestProject(programId, { title: 'Winning Project' })
await prisma.awardEligibility.create({
data: {
awardId: award.id,
projectId: project.id,
eligible: true,
method: 'MANUAL',
},
})
const adminCtx = createTestContext(admin)
const adminCaller = awardRouter.createCaller(adminCtx)
const result = await adminCaller.finalizeWinners({
trackId: track.id,
winnerProjectId: project.id,
})
expect(result.winnerProjectId).toBe(project.id)
expect(result.status).toBe('CLOSED')
})
})
/**
* U-010: Award Governance — Unauthorized AWARD_MASTER
*
* Tests that non-AWARD_MASTER users cannot call finalizeWinners.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
cleanupTestData,
} from '../helpers'
import { awardRouter } from '@/server/routers/award'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Award Gov Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-010: Award Governance — Unauthorized AWARD_MASTER', () => {
it('rejects finalizeWinners when called by a JURY_MEMBER', async () => {
const jury = await createTestUser('JURY_MEMBER', { name: 'Unauthorized Jury' })
userIds.push(jury.id)
// Create award track infrastructure using admin
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await prisma.track.create({
data: {
pipelineId: pipeline.id,
name: 'Award Track',
slug: `award-${Date.now()}`,
kind: 'AWARD',
sortOrder: 1,
decisionMode: 'AWARD_MASTER_DECISION',
},
})
const stage = await createTestStage(track.id, {
name: 'Award Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Create a SpecialAward linked to the track
const award = await prisma.specialAward.create({
data: {
programId,
name: 'Best Innovation',
trackId: track.id,
status: 'VOTING_OPEN',
scoringMode: 'PICK_WINNER',
sortOrder: 0,
},
})
// Create an eligible project
const project = await createTestProject(programId, { title: 'Award Project' })
await prisma.awardEligibility.create({
data: {
awardId: award.id,
projectId: project.id,
eligible: true,
method: 'MANUAL',
},
})
// Attempt to call finalizeWinners as JURY_MEMBER — should be rejected
const juryCtx = createTestContext(jury)
const juryCaller = awardRouter.createCaller(juryCtx)
await expect(
juryCaller.finalizeWinners({
trackId: track.id,
winnerProjectId: project.id,
})
).rejects.toThrow() // Should throw FORBIDDEN or UNAUTHORIZED
// Verify the award still has no winner
const unchangedAward = await prisma.specialAward.findUnique({
where: { id: award.id },
})
expect(unchangedAward!.winnerProjectId).toBeNull()
})
it('allows SUPER_ADMIN to finalize winners (awardMasterProcedure permits)', async () => {
const admin = await createTestUser('SUPER_ADMIN', { name: 'Admin Award Master' })
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await prisma.track.create({
data: {
pipelineId: pipeline.id,
name: 'Admin Award Track',
slug: `admin-award-${Date.now()}`,
kind: 'AWARD',
sortOrder: 2,
decisionMode: 'AWARD_MASTER_DECISION',
},
})
await createTestStage(track.id, {
name: 'Admin Award Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const award = await prisma.specialAward.create({
data: {
programId,
name: 'Admin Award',
trackId: track.id,
status: 'VOTING_OPEN',
scoringMode: 'PICK_WINNER',
sortOrder: 0,
},
})
const project = await createTestProject(programId, { title: 'Winning Project' })
await prisma.awardEligibility.create({
data: {
awardId: award.id,
projectId: project.id,
eligible: true,
method: 'MANUAL',
},
})
const adminCtx = createTestContext(admin)
const adminCaller = awardRouter.createCaller(adminCtx)
const result = await adminCaller.finalizeWinners({
trackId: track.id,
winnerProjectId: project.id,
})
expect(result.winnerProjectId).toBe(project.id)
expect(result.status).toBe('CLOSED')
})
})

View File

@@ -1,188 +1,188 @@
/**
* U-009: Live Cursor — Concurrent Update Handling
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
createTestCohort,
createTestCohortProject,
cleanupTestData,
} from '../helpers'
import {
startSession,
setActiveProject,
jumpToProject,
pauseResume,
openCohortWindow,
closeCohortWindow,
} from '@/server/services/live-control'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Live Control Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-009: Live Cursor Operations', () => {
it('starts a session for a LIVE_FINAL stage', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Live Final',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
// Create cohort with projects
const project1 = await createTestProject(programId, { title: 'Live Project 1' })
const project2 = await createTestProject(programId, { title: 'Live Project 2' })
const cohort = await createTestCohort(stage.id, { name: 'Final Cohort' })
await createTestCohortProject(cohort.id, project1.id, 0)
await createTestCohortProject(cohort.id, project2.id, 1)
const result = await startSession(stage.id, admin.id, prisma)
expect(result.success).toBe(true)
expect(result.sessionId).not.toBeNull()
expect(result.cursorId).not.toBeNull()
// Verify cursor state
const cursor = await prisma.liveProgressCursor.findUnique({
where: { stageId: stage.id },
})
expect(cursor).not.toBeNull()
expect(cursor!.activeProjectId).toBe(project1.id) // First project
expect(cursor!.activeOrderIndex).toBe(0)
expect(cursor!.isPaused).toBe(false)
})
it('rejects starting a session on non-LIVE_FINAL stage', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Evaluation Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const result = await startSession(stage.id, admin.id, prisma)
expect(result.success).toBe(false)
expect(result.errors!.some(e => e.includes('LIVE_FINAL'))).toBe(true)
})
it('handles concurrent cursor updates without corruption', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Concurrent Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const project1 = await createTestProject(programId, { title: 'ConcP1' })
const project2 = await createTestProject(programId, { title: 'ConcP2' })
const project3 = await createTestProject(programId, { title: 'ConcP3' })
const cohort = await createTestCohort(stage.id, { name: 'Conc Cohort' })
await createTestCohortProject(cohort.id, project1.id, 0)
await createTestCohortProject(cohort.id, project2.id, 1)
await createTestCohortProject(cohort.id, project3.id, 2)
// Start session
await startSession(stage.id, admin.id, prisma)
// Fire 2 concurrent setActiveProject calls
const [result1, result2] = await Promise.all([
setActiveProject(stage.id, project2.id, admin.id, prisma),
setActiveProject(stage.id, project3.id, admin.id, prisma),
])
// Both should succeed (last-write-wins)
expect(result1.success).toBe(true)
expect(result2.success).toBe(true)
// Final cursor state should be consistent (one of the two writes wins)
const cursor = await prisma.liveProgressCursor.findUnique({
where: { stageId: stage.id },
})
expect(cursor).not.toBeNull()
// The active project should be either project2 or project3, not corrupted
expect([project2.id, project3.id]).toContain(cursor!.activeProjectId)
})
it('jump, pause/resume, and cohort window operations work correctly', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Full Live Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const p1 = await createTestProject(programId, { title: 'P1' })
const p2 = await createTestProject(programId, { title: 'P2' })
const cohort = await createTestCohort(stage.id, { name: 'Test Cohort', isOpen: false })
await createTestCohortProject(cohort.id, p1.id, 0)
await createTestCohortProject(cohort.id, p2.id, 1)
await startSession(stage.id, admin.id, prisma)
// Jump to index 1
const jumpResult = await jumpToProject(stage.id, 1, admin.id, prisma)
expect(jumpResult.success).toBe(true)
expect(jumpResult.projectId).toBe(p2.id)
// Pause
const pauseResult = await pauseResume(stage.id, true, admin.id, prisma)
expect(pauseResult.success).toBe(true)
const cursorPaused = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursorPaused!.isPaused).toBe(true)
// Resume
const resumeResult = await pauseResume(stage.id, false, admin.id, prisma)
expect(resumeResult.success).toBe(true)
// Open cohort window
const openResult = await openCohortWindow(cohort.id, admin.id, prisma)
expect(openResult.success).toBe(true)
const openCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(openCohort!.isOpen).toBe(true)
expect(openCohort!.windowOpenAt).not.toBeNull()
// Close cohort window
const closeResult = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(closeResult.success).toBe(true)
const closedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(closedCohort!.isOpen).toBe(false)
expect(closedCohort!.windowCloseAt).not.toBeNull()
})
})
/**
* U-009: Live Cursor — Concurrent Update Handling
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
createTestCohort,
createTestCohortProject,
cleanupTestData,
} from '../helpers'
import {
startSession,
setActiveProject,
jumpToProject,
pauseResume,
openCohortWindow,
closeCohortWindow,
} from '@/server/services/live-control'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Live Control Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-009: Live Cursor Operations', () => {
it('starts a session for a LIVE_FINAL stage', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Live Final',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
// Create cohort with projects
const project1 = await createTestProject(programId, { title: 'Live Project 1' })
const project2 = await createTestProject(programId, { title: 'Live Project 2' })
const cohort = await createTestCohort(stage.id, { name: 'Final Cohort' })
await createTestCohortProject(cohort.id, project1.id, 0)
await createTestCohortProject(cohort.id, project2.id, 1)
const result = await startSession(stage.id, admin.id, prisma)
expect(result.success).toBe(true)
expect(result.sessionId).not.toBeNull()
expect(result.cursorId).not.toBeNull()
// Verify cursor state
const cursor = await prisma.liveProgressCursor.findUnique({
where: { stageId: stage.id },
})
expect(cursor).not.toBeNull()
expect(cursor!.activeProjectId).toBe(project1.id) // First project
expect(cursor!.activeOrderIndex).toBe(0)
expect(cursor!.isPaused).toBe(false)
})
it('rejects starting a session on non-LIVE_FINAL stage', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Evaluation Stage',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
const result = await startSession(stage.id, admin.id, prisma)
expect(result.success).toBe(false)
expect(result.errors!.some(e => e.includes('LIVE_FINAL'))).toBe(true)
})
it('handles concurrent cursor updates without corruption', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Concurrent Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const project1 = await createTestProject(programId, { title: 'ConcP1' })
const project2 = await createTestProject(programId, { title: 'ConcP2' })
const project3 = await createTestProject(programId, { title: 'ConcP3' })
const cohort = await createTestCohort(stage.id, { name: 'Conc Cohort' })
await createTestCohortProject(cohort.id, project1.id, 0)
await createTestCohortProject(cohort.id, project2.id, 1)
await createTestCohortProject(cohort.id, project3.id, 2)
// Start session
await startSession(stage.id, admin.id, prisma)
// Fire 2 concurrent setActiveProject calls
const [result1, result2] = await Promise.all([
setActiveProject(stage.id, project2.id, admin.id, prisma),
setActiveProject(stage.id, project3.id, admin.id, prisma),
])
// Both should succeed (last-write-wins)
expect(result1.success).toBe(true)
expect(result2.success).toBe(true)
// Final cursor state should be consistent (one of the two writes wins)
const cursor = await prisma.liveProgressCursor.findUnique({
where: { stageId: stage.id },
})
expect(cursor).not.toBeNull()
// The active project should be either project2 or project3, not corrupted
expect([project2.id, project3.id]).toContain(cursor!.activeProjectId)
})
it('jump, pause/resume, and cohort window operations work correctly', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Full Live Test',
stageType: 'LIVE_FINAL',
status: 'STAGE_ACTIVE',
})
const p1 = await createTestProject(programId, { title: 'P1' })
const p2 = await createTestProject(programId, { title: 'P2' })
const cohort = await createTestCohort(stage.id, { name: 'Test Cohort', isOpen: false })
await createTestCohortProject(cohort.id, p1.id, 0)
await createTestCohortProject(cohort.id, p2.id, 1)
await startSession(stage.id, admin.id, prisma)
// Jump to index 1
const jumpResult = await jumpToProject(stage.id, 1, admin.id, prisma)
expect(jumpResult.success).toBe(true)
expect(jumpResult.projectId).toBe(p2.id)
// Pause
const pauseResult = await pauseResume(stage.id, true, admin.id, prisma)
expect(pauseResult.success).toBe(true)
const cursorPaused = await prisma.liveProgressCursor.findUnique({ where: { stageId: stage.id } })
expect(cursorPaused!.isPaused).toBe(true)
// Resume
const resumeResult = await pauseResume(stage.id, false, admin.id, prisma)
expect(resumeResult.success).toBe(true)
// Open cohort window
const openResult = await openCohortWindow(cohort.id, admin.id, prisma)
expect(openResult.success).toBe(true)
const openCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(openCohort!.isOpen).toBe(true)
expect(openCohort!.windowOpenAt).not.toBeNull()
// Close cohort window
const closeResult = await closeCohortWindow(cohort.id, admin.id, prisma)
expect(closeResult.success).toBe(true)
const closedCohort = await prisma.cohort.findUnique({ where: { id: cohort.id } })
expect(closedCohort!.isOpen).toBe(false)
expect(closedCohort!.windowCloseAt).not.toBeNull()
})
})

View File

@@ -1,103 +1,103 @@
/**
* U-008: Override — Missing Reason Fields
*
* Tests that the decision.override procedure rejects mutations
* with invalid or missing reasonCode via Zod validation.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
cleanupTestData,
} from '../helpers'
import { decisionRouter } from '@/server/routers/decision'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Override Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-008: Override — Missing Reason Fields', () => {
it('rejects override with invalid reasonCode', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, { status: 'STAGE_ACTIVE' })
const project = await createTestProject(programId)
const pss = await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const ctx = createTestContext(admin)
const caller = decisionRouter.createCaller(ctx)
// Attempt override with invalid reasonCode — should be rejected by Zod
await expect(
caller.override({
entityType: 'ProjectStageState',
entityId: pss.id,
newValue: { state: 'PASSED' },
reasonCode: 'INVALID_CODE' as any,
})
).rejects.toThrow()
})
it('accepts override with valid reasonCode and persists OverrideAction', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, { status: 'STAGE_ACTIVE' })
const project = await createTestProject(programId)
const pss = await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const ctx = createTestContext(admin)
const caller = decisionRouter.createCaller(ctx)
const result = await caller.override({
entityType: 'ProjectStageState',
entityId: pss.id,
newValue: { state: 'PASSED' },
reasonCode: 'ADMIN_DISCRETION',
reasonText: 'Manually promoted by admin',
})
expect(result.success).toBe(true)
// Verify OverrideAction was created
const overrideAction = await prisma.overrideAction.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id },
})
expect(overrideAction).not.toBeNull()
expect(overrideAction!.reasonCode).toBe('ADMIN_DISCRETION')
expect(overrideAction!.reasonText).toBe('Manually promoted by admin')
expect(overrideAction!.actorId).toBe(admin.id)
// Verify the PSS state was actually updated
const updatedPSS = await prisma.projectStageState.findUnique({
where: { id: pss.id },
})
expect(updatedPSS!.state).toBe('PASSED')
// Verify DecisionAuditLog was created
const auditLog = await prisma.decisionAuditLog.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id, eventType: 'override.applied' },
})
expect(auditLog).not.toBeNull()
})
})
/**
* U-008: Override — Missing Reason Fields
*
* Tests that the decision.override procedure rejects mutations
* with invalid or missing reasonCode via Zod validation.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createTestContext } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
cleanupTestData,
} from '../helpers'
import { decisionRouter } from '@/server/routers/decision'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Override Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-008: Override — Missing Reason Fields', () => {
it('rejects override with invalid reasonCode', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, { status: 'STAGE_ACTIVE' })
const project = await createTestProject(programId)
const pss = await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const ctx = createTestContext(admin)
const caller = decisionRouter.createCaller(ctx)
// Attempt override with invalid reasonCode — should be rejected by Zod
await expect(
caller.override({
entityType: 'ProjectStageState',
entityId: pss.id,
newValue: { state: 'PASSED' },
reasonCode: 'INVALID_CODE' as any,
})
).rejects.toThrow()
})
it('accepts override with valid reasonCode and persists OverrideAction', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, { status: 'STAGE_ACTIVE' })
const project = await createTestProject(programId)
const pss = await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const ctx = createTestContext(admin)
const caller = decisionRouter.createCaller(ctx)
const result = await caller.override({
entityType: 'ProjectStageState',
entityId: pss.id,
newValue: { state: 'PASSED' },
reasonCode: 'ADMIN_DISCRETION',
reasonText: 'Manually promoted by admin',
})
expect(result.success).toBe(true)
// Verify OverrideAction was created
const overrideAction = await prisma.overrideAction.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id },
})
expect(overrideAction).not.toBeNull()
expect(overrideAction!.reasonCode).toBe('ADMIN_DISCRETION')
expect(overrideAction!.reasonText).toBe('Manually promoted by admin')
expect(overrideAction!.actorId).toBe(admin.id)
// Verify the PSS state was actually updated
const updatedPSS = await prisma.projectStageState.findUnique({
where: { id: pss.id },
})
expect(updatedPSS!.state).toBe('PASSED')
// Verify DecisionAuditLog was created
const auditLog = await prisma.decisionAuditLog.findFirst({
where: { entityType: 'ProjectStageState', entityId: pss.id, eventType: 'override.applied' },
})
expect(auditLog).not.toBeNull()
})
})

View File

@@ -1,128 +1,128 @@
/**
* U-003: Routing — Multiple Rule Match (highest priority wins)
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
createTestRoutingRule,
cleanupTestData,
} from '../helpers'
import { evaluateRoutingRules } from '@/server/services/routing-engine'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Routing Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-003: Multiple Rule Match — Highest Priority Wins', () => {
it('returns the lowest-priority-number rule when multiple rules match', async () => {
const pipeline = await createTestPipeline(programId)
const mainTrack = await createTestTrack(pipeline.id, {
name: 'Main',
kind: 'MAIN',
sortOrder: 0,
})
const awardTrackA = await createTestTrack(pipeline.id, {
name: 'Award A',
kind: 'AWARD',
sortOrder: 1,
routingMode: 'PARALLEL',
})
const awardTrackB = await createTestTrack(pipeline.id, {
name: 'Award B',
kind: 'AWARD',
sortOrder: 2,
routingMode: 'PARALLEL',
})
const awardTrackC = await createTestTrack(pipeline.id, {
name: 'Award C',
kind: 'AWARD',
sortOrder: 3,
routingMode: 'PARALLEL',
})
const stage = await createTestStage(mainTrack.id, {
name: 'Eval',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Create destination stages
await createTestStage(awardTrackA.id, { name: 'A-Eval', sortOrder: 0 })
await createTestStage(awardTrackB.id, { name: 'B-Eval', sortOrder: 0 })
await createTestStage(awardTrackC.id, { name: 'C-Eval', sortOrder: 0 })
// Project from France — all 3 rules match since country=France
const project = await createTestProject(programId, { country: 'France' })
await createTestPSS(project.id, mainTrack.id, stage.id, { state: 'PENDING' })
// Rule 1: priority 10 (lowest = wins)
await createTestRoutingRule(pipeline.id, awardTrackA.id, {
name: 'Award A Rule',
priority: 10,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
// Rule 2: priority 20
await createTestRoutingRule(pipeline.id, awardTrackB.id, {
name: 'Award B Rule',
priority: 20,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
// Rule 3: priority 30
await createTestRoutingRule(pipeline.id, awardTrackC.id, {
name: 'Award C Rule',
priority: 30,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
const matched = await evaluateRoutingRules(project.id, stage.id, pipeline.id, prisma)
expect(matched).not.toBeNull()
expect(matched!.destinationTrackId).toBe(awardTrackA.id)
expect(matched!.priority).toBe(10)
expect(matched!.ruleName).toBe('Award A Rule')
})
it('returns null when no rules match', async () => {
const pipeline = await createTestPipeline(programId)
const mainTrack = await createTestTrack(pipeline.id, { kind: 'MAIN', sortOrder: 0 })
const awardTrack = await createTestTrack(pipeline.id, { kind: 'AWARD', sortOrder: 1 })
const stage = await createTestStage(mainTrack.id, {
name: 'Eval',
status: 'STAGE_ACTIVE',
})
await createTestStage(awardTrack.id, { name: 'A-Eval', sortOrder: 0 })
// Project from Germany, but rule matches only France
const project = await createTestProject(programId, { country: 'Germany' })
await createTestPSS(project.id, mainTrack.id, stage.id, { state: 'PENDING' })
await createTestRoutingRule(pipeline.id, awardTrack.id, {
name: 'France Only Rule',
priority: 0,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
const matched = await evaluateRoutingRules(project.id, stage.id, pipeline.id, prisma)
expect(matched).toBeNull()
})
})
/**
* U-003: Routing — Multiple Rule Match (highest priority wins)
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
createTestRoutingRule,
cleanupTestData,
} from '../helpers'
import { evaluateRoutingRules } from '@/server/services/routing-engine'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Routing Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-003: Multiple Rule Match — Highest Priority Wins', () => {
it('returns the lowest-priority-number rule when multiple rules match', async () => {
const pipeline = await createTestPipeline(programId)
const mainTrack = await createTestTrack(pipeline.id, {
name: 'Main',
kind: 'MAIN',
sortOrder: 0,
})
const awardTrackA = await createTestTrack(pipeline.id, {
name: 'Award A',
kind: 'AWARD',
sortOrder: 1,
routingMode: 'PARALLEL',
})
const awardTrackB = await createTestTrack(pipeline.id, {
name: 'Award B',
kind: 'AWARD',
sortOrder: 2,
routingMode: 'PARALLEL',
})
const awardTrackC = await createTestTrack(pipeline.id, {
name: 'Award C',
kind: 'AWARD',
sortOrder: 3,
routingMode: 'PARALLEL',
})
const stage = await createTestStage(mainTrack.id, {
name: 'Eval',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
})
// Create destination stages
await createTestStage(awardTrackA.id, { name: 'A-Eval', sortOrder: 0 })
await createTestStage(awardTrackB.id, { name: 'B-Eval', sortOrder: 0 })
await createTestStage(awardTrackC.id, { name: 'C-Eval', sortOrder: 0 })
// Project from France — all 3 rules match since country=France
const project = await createTestProject(programId, { country: 'France' })
await createTestPSS(project.id, mainTrack.id, stage.id, { state: 'PENDING' })
// Rule 1: priority 10 (lowest = wins)
await createTestRoutingRule(pipeline.id, awardTrackA.id, {
name: 'Award A Rule',
priority: 10,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
// Rule 2: priority 20
await createTestRoutingRule(pipeline.id, awardTrackB.id, {
name: 'Award B Rule',
priority: 20,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
// Rule 3: priority 30
await createTestRoutingRule(pipeline.id, awardTrackC.id, {
name: 'Award C Rule',
priority: 30,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
const matched = await evaluateRoutingRules(project.id, stage.id, pipeline.id, prisma)
expect(matched).not.toBeNull()
expect(matched!.destinationTrackId).toBe(awardTrackA.id)
expect(matched!.priority).toBe(10)
expect(matched!.ruleName).toBe('Award A Rule')
})
it('returns null when no rules match', async () => {
const pipeline = await createTestPipeline(programId)
const mainTrack = await createTestTrack(pipeline.id, { kind: 'MAIN', sortOrder: 0 })
const awardTrack = await createTestTrack(pipeline.id, { kind: 'AWARD', sortOrder: 1 })
const stage = await createTestStage(mainTrack.id, {
name: 'Eval',
status: 'STAGE_ACTIVE',
})
await createTestStage(awardTrack.id, { name: 'A-Eval', sortOrder: 0 })
// Project from Germany, but rule matches only France
const project = await createTestProject(programId, { country: 'Germany' })
await createTestPSS(project.id, mainTrack.id, stage.id, { state: 'PENDING' })
await createTestRoutingRule(pipeline.id, awardTrack.id, {
name: 'France Only Rule',
priority: 0,
predicateJson: { field: 'project.country', operator: 'eq', value: 'France' },
})
const matched = await evaluateRoutingRules(project.id, stage.id, pipeline.id, prisma)
expect(matched).toBeNull()
})
})

View File

@@ -1,170 +1,170 @@
/**
* 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)
})
})
/**
* 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)
})
})

View File

@@ -1,175 +1,175 @@
/**
* U-001: Stage Transition — Legal Transition
* U-002: Stage Transition — Illegal Transition
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestTransition,
createTestProject,
createTestPSS,
cleanupTestData,
} from '../helpers'
import { validateTransition, executeTransition } from '@/server/services/stage-engine'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'StageEngine Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-001: Legal Transition', () => {
it('validates and executes a legal transition between two stages', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stageA = await createTestStage(track.id, {
name: 'Stage A',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const stageB = await createTestStage(track.id, {
name: 'Stage B',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
sortOrder: 1,
})
// Create a legal transition path A → B
await createTestTransition(stageA.id, stageB.id)
// Create project with PSS in stage A
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stageA.id, { state: 'PENDING' })
// Validate
const validation = await validateTransition(project.id, stageA.id, stageB.id, prisma)
expect(validation.valid).toBe(true)
expect(validation.errors).toHaveLength(0)
// Execute
const result = await executeTransition(
project.id, track.id, stageA.id, stageB.id, 'PENDING', admin.id, prisma
)
expect(result.success).toBe(true)
expect(result.projectStageState).not.toBeNull()
expect(result.projectStageState!.stageId).toBe(stageB.id)
expect(result.projectStageState!.state).toBe('PENDING')
// Verify source PSS was exited
const sourcePSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, stageId: stageA.id },
})
expect(sourcePSS!.exitedAt).not.toBeNull()
// Verify dest PSS was created
const destPSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, stageId: stageB.id, exitedAt: null },
})
expect(destPSS).not.toBeNull()
expect(destPSS!.state).toBe('PENDING')
// Verify audit log entry was created
const auditLog = await prisma.decisionAuditLog.findFirst({
where: {
entityType: 'ProjectStageState',
eventType: 'stage.transitioned',
entityId: destPSS!.id,
},
})
expect(auditLog).not.toBeNull()
expect(auditLog!.actorId).toBe(admin.id)
})
})
describe('U-002: Illegal Transition', () => {
it('rejects transition when no StageTransition record exists', async () => {
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stageA = await createTestStage(track.id, {
name: 'Stage A (no path)',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const stageB = await createTestStage(track.id, {
name: 'Stage B (no path)',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
sortOrder: 1,
})
// No StageTransition created between A and B
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stageA.id, { state: 'PENDING' })
const validation = await validateTransition(project.id, stageA.id, stageB.id, prisma)
expect(validation.valid).toBe(false)
expect(validation.errors.length).toBeGreaterThan(0)
expect(validation.errors.some(e => e.includes('No transition defined'))).toBe(true)
})
it('rejects transition when destination stage is archived', async () => {
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stageA = await createTestStage(track.id, {
name: 'Active Stage',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const stageB = await createTestStage(track.id, {
name: 'Archived Stage',
status: 'STAGE_ARCHIVED',
sortOrder: 1,
})
await createTestTransition(stageA.id, stageB.id)
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stageA.id, { state: 'PENDING' })
const validation = await validateTransition(project.id, stageA.id, stageB.id, prisma)
expect(validation.valid).toBe(false)
expect(validation.errors.some(e => e.includes('archived'))).toBe(true)
})
it('rejects transition when project has no active PSS in source stage', async () => {
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stageA = await createTestStage(track.id, {
name: 'Source',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const stageB = await createTestStage(track.id, {
name: 'Dest',
status: 'STAGE_ACTIVE',
sortOrder: 1,
})
await createTestTransition(stageA.id, stageB.id)
const project = await createTestProject(programId)
// No PSS created for this project
const validation = await validateTransition(project.id, stageA.id, stageB.id, prisma)
expect(validation.valid).toBe(false)
expect(validation.errors.some(e => e.includes('no active state'))).toBe(true)
})
})
/**
* U-001: Stage Transition — Legal Transition
* U-002: Stage Transition — Illegal Transition
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestTransition,
createTestProject,
createTestPSS,
cleanupTestData,
} from '../helpers'
import { validateTransition, executeTransition } from '@/server/services/stage-engine'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'StageEngine Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-001: Legal Transition', () => {
it('validates and executes a legal transition between two stages', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stageA = await createTestStage(track.id, {
name: 'Stage A',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const stageB = await createTestStage(track.id, {
name: 'Stage B',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
sortOrder: 1,
})
// Create a legal transition path A → B
await createTestTransition(stageA.id, stageB.id)
// Create project with PSS in stage A
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stageA.id, { state: 'PENDING' })
// Validate
const validation = await validateTransition(project.id, stageA.id, stageB.id, prisma)
expect(validation.valid).toBe(true)
expect(validation.errors).toHaveLength(0)
// Execute
const result = await executeTransition(
project.id, track.id, stageA.id, stageB.id, 'PENDING', admin.id, prisma
)
expect(result.success).toBe(true)
expect(result.projectStageState).not.toBeNull()
expect(result.projectStageState!.stageId).toBe(stageB.id)
expect(result.projectStageState!.state).toBe('PENDING')
// Verify source PSS was exited
const sourcePSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, stageId: stageA.id },
})
expect(sourcePSS!.exitedAt).not.toBeNull()
// Verify dest PSS was created
const destPSS = await prisma.projectStageState.findFirst({
where: { projectId: project.id, stageId: stageB.id, exitedAt: null },
})
expect(destPSS).not.toBeNull()
expect(destPSS!.state).toBe('PENDING')
// Verify audit log entry was created
const auditLog = await prisma.decisionAuditLog.findFirst({
where: {
entityType: 'ProjectStageState',
eventType: 'stage.transitioned',
entityId: destPSS!.id,
},
})
expect(auditLog).not.toBeNull()
expect(auditLog!.actorId).toBe(admin.id)
})
})
describe('U-002: Illegal Transition', () => {
it('rejects transition when no StageTransition record exists', async () => {
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stageA = await createTestStage(track.id, {
name: 'Stage A (no path)',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const stageB = await createTestStage(track.id, {
name: 'Stage B (no path)',
stageType: 'EVALUATION',
status: 'STAGE_ACTIVE',
sortOrder: 1,
})
// No StageTransition created between A and B
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stageA.id, { state: 'PENDING' })
const validation = await validateTransition(project.id, stageA.id, stageB.id, prisma)
expect(validation.valid).toBe(false)
expect(validation.errors.length).toBeGreaterThan(0)
expect(validation.errors.some(e => e.includes('No transition defined'))).toBe(true)
})
it('rejects transition when destination stage is archived', async () => {
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stageA = await createTestStage(track.id, {
name: 'Active Stage',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const stageB = await createTestStage(track.id, {
name: 'Archived Stage',
status: 'STAGE_ARCHIVED',
sortOrder: 1,
})
await createTestTransition(stageA.id, stageB.id)
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stageA.id, { state: 'PENDING' })
const validation = await validateTransition(project.id, stageA.id, stageB.id, prisma)
expect(validation.valid).toBe(false)
expect(validation.errors.some(e => e.includes('archived'))).toBe(true)
})
it('rejects transition when project has no active PSS in source stage', async () => {
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stageA = await createTestStage(track.id, {
name: 'Source',
status: 'STAGE_ACTIVE',
sortOrder: 0,
})
const stageB = await createTestStage(track.id, {
name: 'Dest',
status: 'STAGE_ACTIVE',
sortOrder: 1,
})
await createTestTransition(stageA.id, stageB.id)
const project = await createTestProject(programId)
// No PSS created for this project
const validation = await validateTransition(project.id, stageA.id, stageB.id, prisma)
expect(validation.valid).toBe(false)
expect(validation.errors.some(e => e.includes('no active state'))).toBe(true)
})
})

View File

@@ -1,173 +1,173 @@
/**
* U-004: Filtering Gates — Missing Required Docs
* U-005: AI Banding — Uncertain Confidence Band
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
createTestFilteringRule,
cleanupTestData,
} from '../helpers'
import { runStageFiltering } from '@/server/services/stage-filtering'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Filtering Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-004: Filtering Gates — Missing Required Docs', () => {
it('rejects a project that lacks required document types', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Filter Stage',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
})
// Create a DOCUMENT_CHECK rule requiring EXEC_SUMMARY
await createTestFilteringRule(stage.id, {
name: 'Require Exec Summary',
ruleType: 'DOCUMENT_CHECK',
configJson: {
requiredFileTypes: ['EXEC_SUMMARY'],
action: 'REJECT',
},
priority: 0,
})
// Create project WITHOUT any files
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const result = await runStageFiltering(stage.id, admin.id, prisma)
expect(result.total).toBe(1)
expect(result.rejected).toBe(1)
expect(result.passed).toBe(0)
expect(result.manualQueue).toBe(0)
// Verify the FilteringResult was created with FILTERED_OUT
const filteringResult = await prisma.filteringResult.findFirst({
where: { stageId: stage.id, projectId: project.id },
})
expect(filteringResult).not.toBeNull()
expect(filteringResult!.outcome).toBe('FILTERED_OUT')
})
it('passes a project that has all required document types', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Filter Stage 2',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
})
await createTestFilteringRule(stage.id, {
name: 'Require Exec Summary',
ruleType: 'DOCUMENT_CHECK',
configJson: {
requiredFileTypes: ['EXEC_SUMMARY'],
action: 'REJECT',
},
priority: 0,
})
// Create project WITH the required file
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
// Create a ProjectFile with the required type
await prisma.projectFile.create({
data: {
projectId: project.id,
fileType: 'EXEC_SUMMARY',
fileName: 'summary.pdf',
mimeType: 'application/pdf',
size: 1024,
bucket: 'test-bucket',
objectKey: 'test/summary.pdf',
},
})
const result = await runStageFiltering(stage.id, admin.id, prisma)
expect(result.total).toBe(1)
// With no AI rules, a passing doc check results in PASSED
expect(result.passed).toBe(1)
expect(result.rejected).toBe(0)
})
})
describe('U-005: AI Banding — Uncertain Confidence Band', () => {
it('flags a project for manual review when AI confidence is in the uncertain band', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'AI Filter Stage',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
})
// Create an AI_SCREENING rule — the service generates a confidence value
// between 0.25 and 0.75 for projects with minimal data → FLAGGED
await prisma.filteringRule.create({
data: {
stageId: stage.id,
name: 'AI Screen',
ruleType: 'AI_SCREENING',
configJson: { criteriaText: 'Evaluate ocean impact' },
priority: 10,
isActive: true,
},
})
// Create project with title and description (hasMinimalData = true → confidence = 0.5)
const project = await createTestProject(programId, {
title: 'Ocean Cleanup',
description: 'A project to clean the ocean',
})
await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const result = await runStageFiltering(stage.id, admin.id, prisma)
expect(result.total).toBe(1)
// Confidence 0.5 is between 0.25 and 0.75 → FLAGGED → manual queue
expect(result.manualQueue).toBe(1)
expect(result.passed).toBe(0)
expect(result.rejected).toBe(0)
// Verify filtering result
const fr = await prisma.filteringResult.findFirst({
where: { stageId: stage.id, projectId: project.id },
})
expect(fr).not.toBeNull()
expect(fr!.outcome).toBe('FLAGGED')
expect(fr!.aiScreeningJson).not.toBeNull()
})
})
/**
* U-004: Filtering Gates — Missing Required Docs
* U-005: AI Banding — Uncertain Confidence Band
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma } from '../setup'
import {
createTestUser,
createTestProgram,
createTestPipeline,
createTestTrack,
createTestStage,
createTestProject,
createTestPSS,
createTestFilteringRule,
cleanupTestData,
} from '../helpers'
import { runStageFiltering } from '@/server/services/stage-filtering'
let programId: string
let userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: 'Filtering Test' })
programId = program.id
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
describe('U-004: Filtering Gates — Missing Required Docs', () => {
it('rejects a project that lacks required document types', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Filter Stage',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
})
// Create a DOCUMENT_CHECK rule requiring EXEC_SUMMARY
await createTestFilteringRule(stage.id, {
name: 'Require Exec Summary',
ruleType: 'DOCUMENT_CHECK',
configJson: {
requiredFileTypes: ['EXEC_SUMMARY'],
action: 'REJECT',
},
priority: 0,
})
// Create project WITHOUT any files
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const result = await runStageFiltering(stage.id, admin.id, prisma)
expect(result.total).toBe(1)
expect(result.rejected).toBe(1)
expect(result.passed).toBe(0)
expect(result.manualQueue).toBe(0)
// Verify the FilteringResult was created with FILTERED_OUT
const filteringResult = await prisma.filteringResult.findFirst({
where: { stageId: stage.id, projectId: project.id },
})
expect(filteringResult).not.toBeNull()
expect(filteringResult!.outcome).toBe('FILTERED_OUT')
})
it('passes a project that has all required document types', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'Filter Stage 2',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
})
await createTestFilteringRule(stage.id, {
name: 'Require Exec Summary',
ruleType: 'DOCUMENT_CHECK',
configJson: {
requiredFileTypes: ['EXEC_SUMMARY'],
action: 'REJECT',
},
priority: 0,
})
// Create project WITH the required file
const project = await createTestProject(programId)
await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
// Create a ProjectFile with the required type
await prisma.projectFile.create({
data: {
projectId: project.id,
fileType: 'EXEC_SUMMARY',
fileName: 'summary.pdf',
mimeType: 'application/pdf',
size: 1024,
bucket: 'test-bucket',
objectKey: 'test/summary.pdf',
},
})
const result = await runStageFiltering(stage.id, admin.id, prisma)
expect(result.total).toBe(1)
// With no AI rules, a passing doc check results in PASSED
expect(result.passed).toBe(1)
expect(result.rejected).toBe(0)
})
})
describe('U-005: AI Banding — Uncertain Confidence Band', () => {
it('flags a project for manual review when AI confidence is in the uncertain band', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const pipeline = await createTestPipeline(programId)
const track = await createTestTrack(pipeline.id)
const stage = await createTestStage(track.id, {
name: 'AI Filter Stage',
stageType: 'FILTER',
status: 'STAGE_ACTIVE',
})
// Create an AI_SCREENING rule — the service generates a confidence value
// between 0.25 and 0.75 for projects with minimal data → FLAGGED
await prisma.filteringRule.create({
data: {
stageId: stage.id,
name: 'AI Screen',
ruleType: 'AI_SCREENING',
configJson: { criteriaText: 'Evaluate ocean impact' },
priority: 10,
isActive: true,
},
})
// Create project with title and description (hasMinimalData = true → confidence = 0.5)
const project = await createTestProject(programId, {
title: 'Ocean Cleanup',
description: 'A project to clean the ocean',
})
await createTestPSS(project.id, track.id, stage.id, { state: 'PENDING' })
const result = await runStageFiltering(stage.id, admin.id, prisma)
expect(result.total).toBe(1)
// Confidence 0.5 is between 0.25 and 0.75 → FLAGGED → manual queue
expect(result.manualQueue).toBe(1)
expect(result.passed).toBe(0)
expect(result.rejected).toBe(0)
// Verify filtering result
const fr = await prisma.filteringResult.findFirst({
where: { stageId: stage.id, projectId: project.id },
})
expect(fr).not.toBeNull()
expect(fr!.outcome).toBe('FLAGGED')
expect(fr!.aiScreeningJson).not.toBeNull()
})
})