Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s

Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 23:04:15 +01:00
parent 9ab4717f96
commit 6ca39c976b
349 changed files with 69938 additions and 28767 deletions

View File

@@ -0,0 +1,382 @@
/**
* Unit tests for the assignment policy resolution engine.
*
* These are pure-logic tests — no database or Prisma needed.
* We construct MemberContext objects inline and verify the resolved policies.
*/
import { describe, it, expect } from 'vitest'
import type { CapMode } from '@prisma/client'
import type { MemberContext } from '@/server/services/competition-context'
import {
SYSTEM_DEFAULT_CAP,
SYSTEM_DEFAULT_CAP_MODE,
SYSTEM_DEFAULT_SOFT_BUFFER,
resolveEffectiveCap,
resolveEffectiveCapMode,
resolveEffectiveSoftCapBuffer,
resolveEffectiveCategoryBias,
evaluateAssignmentPolicy,
} from '@/server/services/assignment-policy'
// ============================================================================
// Helpers — build minimal MemberContext stubs
// ============================================================================
function baseMemberContext(overrides: Partial<MemberContext> = {}): MemberContext {
return {
competition: {} as any,
round: {} as any,
roundConfig: {} as any,
submissionWindows: [],
juryGroup: null,
member: {
id: 'member-1',
juryGroupId: 'jg-1',
userId: 'user-1',
role: 'MEMBER',
maxAssignmentsOverride: null,
capModeOverride: null,
categoryQuotasOverride: null,
preferredStartupRatio: null,
availabilityNotes: null,
selfServiceCap: null,
selfServiceRatio: null,
joinedAt: new Date(),
} as any,
user: { id: 'user-1', name: 'Test Juror', email: 'test@example.com', role: 'JURY_MEMBER' },
currentAssignmentCount: 0,
assignmentsByCategory: {},
pendingIntents: [],
...overrides,
}
}
function withJuryGroup(
ctx: MemberContext,
groupOverrides: Record<string, unknown> = {},
): MemberContext {
const defaultGroup = {
id: 'jg-1',
competitionId: 'comp-1',
name: 'Panel A',
slug: 'panel-a',
description: null,
sortOrder: 0,
defaultMaxAssignments: 20,
defaultCapMode: 'SOFT' as CapMode,
softCapBuffer: 3,
categoryQuotasEnabled: false,
defaultCategoryQuotas: null,
allowJurorCapAdjustment: false,
allowJurorRatioAdjustment: false,
createdAt: new Date(),
updatedAt: new Date(),
...groupOverrides,
}
return { ...ctx, juryGroup: defaultGroup as any }
}
// ============================================================================
// resolveEffectiveCap
// ============================================================================
describe('resolveEffectiveCap', () => {
it('returns system default (15) when no jury group', () => {
const ctx = baseMemberContext()
const result = resolveEffectiveCap(ctx)
expect(result.value).toBe(SYSTEM_DEFAULT_CAP)
expect(result.source).toBe('system')
})
it('returns jury group default when group is set', () => {
const ctx = withJuryGroup(baseMemberContext(), { defaultMaxAssignments: 25 })
const result = resolveEffectiveCap(ctx)
expect(result.value).toBe(25)
expect(result.source).toBe('jury_group')
})
it('admin per-member override takes precedence over group default', () => {
let ctx = withJuryGroup(baseMemberContext(), { defaultMaxAssignments: 25 })
ctx = { ...ctx, member: { ...ctx.member, maxAssignmentsOverride: 10 } }
const result = resolveEffectiveCap(ctx)
expect(result.value).toBe(10)
expect(result.source).toBe('member')
expect(result.explanation).toContain('Admin per-member override')
})
it('self-service cap overrides admin when allowJurorCapAdjustment is true', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 25,
allowJurorCapAdjustment: true,
})
ctx = { ...ctx, member: { ...ctx.member, selfServiceCap: 12 } }
const result = resolveEffectiveCap(ctx)
expect(result.value).toBe(12)
expect(result.source).toBe('member')
expect(result.explanation).toContain('Self-service cap')
})
it('self-service cap is bounded by admin max (override)', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 25,
allowJurorCapAdjustment: true,
})
ctx = {
...ctx,
member: {
...ctx.member,
selfServiceCap: 30, // Juror wants 30 but admin max is 10
maxAssignmentsOverride: 10,
},
}
const result = resolveEffectiveCap(ctx)
expect(result.value).toBe(10) // Bounded to admin max
expect(result.explanation).toContain('bounded')
})
it('self-service cap is bounded by group default when no admin override', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 20,
allowJurorCapAdjustment: true,
})
ctx = { ...ctx, member: { ...ctx.member, selfServiceCap: 50 } }
const result = resolveEffectiveCap(ctx)
expect(result.value).toBe(20) // Bounded to group default
})
it('self-service cap is ignored when allowJurorCapAdjustment is false', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 20,
allowJurorCapAdjustment: false,
})
ctx = { ...ctx, member: { ...ctx.member, selfServiceCap: 5 } }
const result = resolveEffectiveCap(ctx)
// Should fall through to group default since self-service is disabled
expect(result.value).toBe(20)
expect(result.source).toBe('jury_group')
})
})
// ============================================================================
// resolveEffectiveCapMode
// ============================================================================
describe('resolveEffectiveCapMode', () => {
it('returns system default (SOFT) when no group', () => {
const ctx = baseMemberContext()
const result = resolveEffectiveCapMode(ctx)
expect(result.value).toBe(SYSTEM_DEFAULT_CAP_MODE)
expect(result.source).toBe('system')
})
it('returns jury group default cap mode', () => {
const ctx = withJuryGroup(baseMemberContext(), { defaultCapMode: 'HARD' })
const result = resolveEffectiveCapMode(ctx)
expect(result.value).toBe('HARD')
expect(result.source).toBe('jury_group')
})
it('admin per-member cap mode override takes precedence', () => {
let ctx = withJuryGroup(baseMemberContext(), { defaultCapMode: 'SOFT' })
ctx = { ...ctx, member: { ...ctx.member, capModeOverride: 'NONE' as CapMode } }
const result = resolveEffectiveCapMode(ctx)
expect(result.value).toBe('NONE')
expect(result.source).toBe('member')
})
})
// ============================================================================
// resolveEffectiveSoftCapBuffer
// ============================================================================
describe('resolveEffectiveSoftCapBuffer', () => {
it('returns system default (2) when no group', () => {
const ctx = baseMemberContext()
const result = resolveEffectiveSoftCapBuffer(ctx)
expect(result.value).toBe(SYSTEM_DEFAULT_SOFT_BUFFER)
expect(result.source).toBe('system')
})
it('returns jury group buffer', () => {
const ctx = withJuryGroup(baseMemberContext(), { softCapBuffer: 5 })
const result = resolveEffectiveSoftCapBuffer(ctx)
expect(result.value).toBe(5)
expect(result.source).toBe('jury_group')
})
})
// ============================================================================
// resolveEffectiveCategoryBias
// ============================================================================
describe('resolveEffectiveCategoryBias', () => {
it('returns null when no bias configured', () => {
const ctx = baseMemberContext()
const result = resolveEffectiveCategoryBias(ctx)
expect(result.value).toBeNull()
expect(result.source).toBe('system')
})
it('uses admin-set preferredStartupRatio', () => {
let ctx = baseMemberContext()
ctx = { ...ctx, member: { ...ctx.member, preferredStartupRatio: 0.7 } }
const result = resolveEffectiveCategoryBias(ctx)
expect(result.value!.STARTUP).toBeCloseTo(0.7)
expect(result.value!.BUSINESS_CONCEPT).toBeCloseTo(0.3)
expect(result.source).toBe('member')
})
it('uses self-service ratio when allowJurorRatioAdjustment is true', () => {
let ctx = withJuryGroup(baseMemberContext(), {
allowJurorRatioAdjustment: true,
})
ctx = { ...ctx, member: { ...ctx.member, selfServiceRatio: 0.6 } }
const result = resolveEffectiveCategoryBias(ctx)
expect(result.value).toEqual({ STARTUP: 0.6, BUSINESS_CONCEPT: 0.4 })
expect(result.source).toBe('member')
})
it('self-service ratio takes precedence over admin ratio', () => {
let ctx = withJuryGroup(baseMemberContext(), {
allowJurorRatioAdjustment: true,
})
ctx = {
...ctx,
member: { ...ctx.member, selfServiceRatio: 0.8, preferredStartupRatio: 0.5 },
}
const result = resolveEffectiveCategoryBias(ctx)
expect(result.value!.STARTUP).toBe(0.8) // Self-service wins
})
it('derives bias from group category quotas', () => {
const ctx = withJuryGroup(baseMemberContext(), {
categoryQuotasEnabled: true,
defaultCategoryQuotas: {
STARTUP: { min: 5, max: 15 },
BUSINESS_CONCEPT: { min: 3, max: 5 },
},
})
const result = resolveEffectiveCategoryBias(ctx)
expect(result.source).toBe('jury_group')
// 15/(15+5) = 0.75, 5/(15+5) = 0.25
expect(result.value!.STARTUP).toBe(0.75)
expect(result.value!.BUSINESS_CONCEPT).toBe(0.25)
})
it('ignores group quotas when categoryQuotasEnabled is false', () => {
const ctx = withJuryGroup(baseMemberContext(), {
categoryQuotasEnabled: false,
defaultCategoryQuotas: {
STARTUP: { min: 5, max: 15 },
BUSINESS_CONCEPT: { min: 3, max: 5 },
},
})
const result = resolveEffectiveCategoryBias(ctx)
expect(result.value).toBeNull()
})
})
// ============================================================================
// evaluateAssignmentPolicy — canAssignMore / remainingCapacity / isOverCap
// ============================================================================
describe('evaluateAssignmentPolicy', () => {
describe('HARD cap mode', () => {
it('canAssignMore is true when below cap', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 10,
defaultCapMode: 'HARD',
})
ctx = { ...ctx, currentAssignmentCount: 7 }
const result = evaluateAssignmentPolicy(ctx)
expect(result.canAssignMore).toBe(true)
expect(result.remainingCapacity).toBe(3)
expect(result.isOverCap).toBe(false)
})
it('canAssignMore is false when at cap', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 10,
defaultCapMode: 'HARD',
})
ctx = { ...ctx, currentAssignmentCount: 10 }
const result = evaluateAssignmentPolicy(ctx)
expect(result.canAssignMore).toBe(false)
expect(result.remainingCapacity).toBe(0)
})
it('detects over-cap with correct count', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 10,
defaultCapMode: 'HARD',
})
ctx = { ...ctx, currentAssignmentCount: 13 }
const result = evaluateAssignmentPolicy(ctx)
expect(result.isOverCap).toBe(true)
expect(result.overCapBy).toBe(3)
expect(result.canAssignMore).toBe(false)
})
})
describe('SOFT cap mode', () => {
it('canAssignMore is true within buffer zone', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 10,
defaultCapMode: 'SOFT',
softCapBuffer: 3,
})
ctx = { ...ctx, currentAssignmentCount: 11 } // Over cap but within buffer
const result = evaluateAssignmentPolicy(ctx)
expect(result.canAssignMore).toBe(true)
expect(result.isOverCap).toBe(true) // Over the nominal cap
expect(result.remainingCapacity).toBe(2) // 10+3-11
})
it('canAssignMore is false beyond buffer', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 10,
defaultCapMode: 'SOFT',
softCapBuffer: 3,
})
ctx = { ...ctx, currentAssignmentCount: 13 } // cap(10) + buffer(3) = 13 → full
const result = evaluateAssignmentPolicy(ctx)
expect(result.canAssignMore).toBe(false)
expect(result.remainingCapacity).toBe(0)
})
})
describe('NONE cap mode', () => {
it('canAssignMore is always true', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 10,
defaultCapMode: 'NONE',
})
ctx = { ...ctx, currentAssignmentCount: 100 }
const result = evaluateAssignmentPolicy(ctx)
expect(result.canAssignMore).toBe(true)
expect(result.remainingCapacity).toBe(Infinity)
})
})
describe('aggregate provenance', () => {
it('returns all four policy resolutions with correct sources', () => {
let ctx = withJuryGroup(baseMemberContext(), {
defaultMaxAssignments: 20,
defaultCapMode: 'SOFT',
softCapBuffer: 2,
})
ctx = { ...ctx, currentAssignmentCount: 5 }
const result = evaluateAssignmentPolicy(ctx)
expect(result.effectiveCap.source).toBe('jury_group')
expect(result.effectiveCap.value).toBe(20)
expect(result.effectiveCapMode.source).toBe('jury_group')
expect(result.effectiveCapMode.value).toBe('SOFT')
expect(result.softCapBuffer.source).toBe('jury_group')
expect(result.softCapBuffer.value).toBe(2)
expect(result.categoryBias.source).toBe('system')
expect(result.categoryBias.value).toBeNull()
})
})
})

View File

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