Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
This commit is contained in:
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user