@@ -672,6 +672,152 @@ describe('challenge service unit tests', () => {
672672 } )
673673
674674 describe ( 'update challenge tests' , ( ) => {
675+ const ensureSkipTimelineTemplate = async ( ) => {
676+ const templateId = config . SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID
677+ let template = await prisma . timelineTemplate . findUnique ( { where : { id : templateId } } )
678+ if ( ! template ) {
679+ template = await prisma . timelineTemplate . create ( {
680+ data : {
681+ id : templateId ,
682+ name : `skip-template-${ templateId } ` ,
683+ description : 'Template used to bypass project requirements in activation tests' ,
684+ isActive : true ,
685+ createdBy : 'activation-test' ,
686+ updatedBy : 'activation-test'
687+ }
688+ } )
689+ }
690+
691+ const existingPhases = await prisma . timelineTemplatePhase . findMany ( {
692+ where : { timelineTemplateId : templateId }
693+ } )
694+ const existingPhaseIds = new Set ( existingPhases . map ( p => p . phaseId ) )
695+ const phaseRows = [ ]
696+ if ( ! existingPhaseIds . has ( data . phase . id ) ) {
697+ phaseRows . push ( {
698+ timelineTemplateId : templateId ,
699+ phaseId : data . phase . id ,
700+ defaultDuration : 1000 ,
701+ createdBy : 'activation-test' ,
702+ updatedBy : 'activation-test'
703+ } )
704+ }
705+ if ( ! existingPhaseIds . has ( data . phase2 . id ) ) {
706+ phaseRows . push ( {
707+ timelineTemplateId : templateId ,
708+ phaseId : data . phase2 . id ,
709+ predecessor : data . phase . id ,
710+ defaultDuration : 1000 ,
711+ createdBy : 'activation-test' ,
712+ updatedBy : 'activation-test'
713+ } )
714+ }
715+ if ( phaseRows . length > 0 ) {
716+ await prisma . timelineTemplatePhase . createMany ( { data : phaseRows } )
717+ }
718+ return templateId
719+ }
720+
721+ const createChallengeWithRequiredReviewPhases = async ( ) => {
722+ await ensureSkipTimelineTemplate ( )
723+
724+ const phaseNames = [ 'Screening' , 'Review' ]
725+ const createdPhaseIds = [ ]
726+ const phaseRecords = [ ]
727+
728+ for ( const phaseName of phaseNames ) {
729+ let phaseRecord = await prisma . phase . findFirst ( { where : { name : phaseName } } )
730+ if ( ! phaseRecord ) {
731+ phaseRecord = await prisma . phase . create ( {
732+ data : {
733+ id : uuid ( ) ,
734+ name : phaseName ,
735+ description : `${ phaseName } phase` ,
736+ isOpen : false ,
737+ duration : 86400 ,
738+ createdBy : 'activation-test' ,
739+ updatedBy : 'activation-test'
740+ }
741+ } )
742+ createdPhaseIds . push ( phaseRecord . id )
743+ }
744+ phaseRecords . push ( phaseRecord )
745+ }
746+
747+ const challenge = await prisma . challenge . create ( {
748+ data : {
749+ id : uuid ( ) ,
750+ name : `Activation coverage ${ Date . now ( ) } ` ,
751+ description : 'Activation coverage test' ,
752+ typeId : data . challenge . typeId ,
753+ trackId : data . challenge . trackId ,
754+ timelineTemplateId : config . SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID ,
755+ startDate : new Date ( ) ,
756+ status : ChallengeStatusEnum . DRAFT ,
757+ tags : [ ] ,
758+ groups : [ ] ,
759+ createdBy : 'activation-test' ,
760+ updatedBy : 'activation-test'
761+ }
762+ } )
763+
764+ const challengePhaseIds = [ ]
765+ await prisma . challengePhase . createMany ( {
766+ data : phaseRecords . map ( ( phase ) => {
767+ const cpId = uuid ( )
768+ challengePhaseIds . push ( cpId )
769+ return {
770+ id : cpId ,
771+ challengeId : challenge . id ,
772+ phaseId : phase . id ,
773+ name : phase . name ,
774+ duration : phase . duration || 86400 ,
775+ isOpen : false ,
776+ createdBy : 'activation-test' ,
777+ updatedBy : 'activation-test'
778+ }
779+ } )
780+ } )
781+
782+ return { challenge, phaseRecords, createdPhaseIds, challengePhaseIds }
783+ }
784+
785+ const cleanupChallengeWithRequiredReviewPhases = async ( {
786+ challenge,
787+ createdPhaseIds = [ ] ,
788+ challengePhaseIds = [ ]
789+ } ) => {
790+ if ( challengePhaseIds . length > 0 ) {
791+ await prisma . challengePhase . deleteMany ( { where : { id : { in : challengePhaseIds } } } )
792+ }
793+ if ( challenge ) {
794+ await prisma . challenge . delete ( { where : { id : challenge . id } } )
795+ }
796+ if ( createdPhaseIds . length > 0 ) {
797+ await prisma . phase . deleteMany ( { where : { id : { in : createdPhaseIds } } } )
798+ }
799+ }
800+
801+ const createActivationChallenge = async ( status = ChallengeStatusEnum . NEW ) => {
802+ const timelineTemplateId = await ensureSkipTimelineTemplate ( )
803+ return prisma . challenge . create ( {
804+ data : {
805+ id : uuid ( ) ,
806+ name : `Activation reviewer check ${ Date . now ( ) } ` ,
807+ description : 'activation reviewer check' ,
808+ typeId : data . challenge . typeId ,
809+ trackId : data . challenge . trackId ,
810+ timelineTemplateId,
811+ startDate : new Date ( ) ,
812+ status,
813+ tags : [ ] ,
814+ groups : [ ] ,
815+ createdBy : 'activation-test' ,
816+ updatedBy : 'activation-test'
817+ }
818+ } )
819+ }
820+
675821 it ( 'update challenge successfully 1' , async ( ) => {
676822 const challengeData = testChallengeData
677823 const result = await service . updateChallenge ( { isMachine : true , sub : 'sub3' , userId : 22838965 } , id , {
@@ -1128,6 +1274,107 @@ describe('challenge service unit tests', () => {
11281274 throw new Error ( 'should not reach here' )
11291275 } )
11301276
1277+ it ( 'update challenge - prevent activating without reviewers' , async ( ) => {
1278+ const activationChallenge = await createActivationChallenge ( ChallengeStatusEnum . DRAFT )
1279+ try {
1280+ await service . updateChallenge ( { isMachine : true , sub : 'sub-activate' } , activationChallenge . id , {
1281+ status : ChallengeStatusEnum . ACTIVE
1282+ } )
1283+ } catch ( e ) {
1284+ should . equal ( e . message . indexOf ( 'reviewer configured' ) >= 0 , true )
1285+ return
1286+ } finally {
1287+ await prisma . challenge . delete ( { where : { id : activationChallenge . id } } )
1288+ }
1289+ throw new Error ( 'should not reach here' )
1290+ } )
1291+
1292+ it ( 'update challenge - allow activating with reviewers provided' , async ( ) => {
1293+ const activationChallenge = await createActivationChallenge ( )
1294+ try {
1295+ const updated = await service . updateChallenge (
1296+ { isMachine : true , sub : 'sub-activate' , userId : 22838965 } ,
1297+ activationChallenge . id ,
1298+ {
1299+ status : ChallengeStatusEnum . ACTIVE ,
1300+ reviewers : [
1301+ {
1302+ phaseId : data . phase . id ,
1303+ scorecardId : 'activation-scorecard' ,
1304+ isMemberReview : false ,
1305+ aiWorkflowId : 'workflow-123'
1306+ }
1307+ ]
1308+ }
1309+ )
1310+ should . equal ( updated . status , ChallengeStatusEnum . ACTIVE )
1311+ should . exist ( updated . reviewers )
1312+ should . equal ( updated . reviewers . length , 1 )
1313+ should . equal ( updated . reviewers [ 0 ] . scorecardId , 'activation-scorecard' )
1314+ } finally {
1315+ await prisma . challenge . delete ( { where : { id : activationChallenge . id } } )
1316+ }
1317+ } )
1318+
1319+ it ( 'update challenge - enforce reviewers for required phases' , async ( ) => {
1320+ const setup = await createChallengeWithRequiredReviewPhases ( )
1321+ try {
1322+ await service . updateChallenge (
1323+ { isMachine : true , sub : 'sub-activate' , userId : 22838965 } ,
1324+ setup . challenge . id ,
1325+ {
1326+ status : ChallengeStatusEnum . ACTIVE ,
1327+ reviewers : [
1328+ {
1329+ phaseId : setup . phaseRecords [ 0 ] . id ,
1330+ scorecardId : 'screening-scorecard' ,
1331+ isMemberReview : false ,
1332+ aiWorkflowId : 'workflow-screening'
1333+ }
1334+ ]
1335+ }
1336+ )
1337+ } catch ( e ) {
1338+ should . equal ( e . message . indexOf ( 'missing reviewers for phase(s): Review' ) >= 0 , true )
1339+ return
1340+ } finally {
1341+ await cleanupChallengeWithRequiredReviewPhases ( setup )
1342+ }
1343+ throw new Error ( 'should not reach here' )
1344+ } )
1345+
1346+ it ( 'update challenge - allow activation when required phases have reviewers' , async ( ) => {
1347+ const setup = await createChallengeWithRequiredReviewPhases ( )
1348+ try {
1349+ const updated = await service . updateChallenge (
1350+ { isMachine : true , sub : 'sub-activate' , userId : 22838965 } ,
1351+ setup . challenge . id ,
1352+ {
1353+ status : ChallengeStatusEnum . ACTIVE ,
1354+ reviewers : [
1355+ {
1356+ phaseId : setup . phaseRecords [ 0 ] . id ,
1357+ scorecardId : 'screening-scorecard' ,
1358+ isMemberReview : false ,
1359+ aiWorkflowId : 'workflow-screening'
1360+ } ,
1361+ {
1362+ phaseId : setup . phaseRecords [ 1 ] . id ,
1363+ scorecardId : 'review-scorecard' ,
1364+ isMemberReview : false ,
1365+ aiWorkflowId : 'workflow-review'
1366+ }
1367+ ]
1368+ }
1369+ )
1370+ should . equal ( updated . status , ChallengeStatusEnum . ACTIVE )
1371+ should . exist ( updated . reviewers )
1372+ should . equal ( updated . reviewers . length , 2 )
1373+ } finally {
1374+ await cleanupChallengeWithRequiredReviewPhases ( setup )
1375+ }
1376+ } )
1377+
11311378 it ( 'update challenge - set winners with non-completed Active status' , async ( ) => {
11321379 try {
11331380 await service . updateChallenge ( { isMachine : true , sub : 'sub3' } , id , {
0 commit comments