2026-02-14 15:26:42 +01:00
|
|
|
/**
|
|
|
|
|
* 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()
|
|
|
|
|
})
|
|
|
|
|
})
|