176 lines
5.6 KiB
TypeScript
176 lines
5.6 KiB
TypeScript
|
|
/**
|
||
|
|
* 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)
|
||
|
|
})
|
||
|
|
})
|