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

176 lines
5.6 KiB
TypeScript
Raw Normal View History

/**
* 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)
})
})