From 520280d145fffe760d86e06edf53e71f77382182 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 27 Feb 2026 14:09:07 +0200 Subject: [PATCH 1/2] Add index on challenge term --- .../20260227120000_add_challenge_term_index/migration.sql | 2 ++ prisma/schema.prisma | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 prisma/migrations/20260227120000_add_challenge_term_index/migration.sql diff --git a/prisma/migrations/20260227120000_add_challenge_term_index/migration.sql b/prisma/migrations/20260227120000_add_challenge_term_index/migration.sql new file mode 100644 index 0000000..2d7465d --- /dev/null +++ b/prisma/migrations/20260227120000_add_challenge_term_index/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "ChallengeTerm_challengeId_idx" ON "ChallengeTerm"("challengeId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 036da57..94c10e3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -375,6 +375,8 @@ model ChallengeTerm { createdBy String updatedAt DateTime @updatedAt updatedBy String + + @@index([challengeId]) } ////////////////////////////////////////// From a1de0f9cb5763dea484a95bbb20fd444514afd75 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 31 Jul 2026 13:41:31 +0300 Subject: [PATCH 2/2] Fix: allow closing ai phase when virusscan submission is true --- src/services/ChallengeService.js | 29 +++++++++- test/unit/ChallengeService.test.js | 90 ++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 2bb4b00..5b88e5f 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -913,9 +913,32 @@ async function ensureAIPhaseCanBeClosed(challengeId, phaseName = 'AI Screening') reviewPrisma.$queryRaw( Prisma.sql` SELECT "id", "legacySubmissionId" - FROM ${submissionTable} - WHERE "challengeId" = ${challengeId} - AND "status"::text <> 'DELETED' + FROM ( + SELECT + "id", + "legacySubmissionId", + ROW_NUMBER() OVER ( + PARTITION BY COALESCE("memberId", "id") + ORDER BY + "submittedDate" DESC NULLS LAST, + "createdAt" DESC NULLS LAST, + "updatedAt" DESC NULLS LAST, + "id" DESC + ) AS "rowNumber" + FROM ${submissionTable} + WHERE "challengeId" = ${challengeId} + AND ( + "status" IS NULL + OR "status"::text = 'ACTIVE' + OR "status"::text = 'AI_FAILED_REVIEW' + ) + AND ( + "type" IS NULL + OR UPPER(("type")::text) = 'CONTEST_SUBMISSION' + ) + AND ("virusScan" IS NULL OR "virusScan" = TRUE) + ) latest + WHERE "rowNumber" = 1 `, ), reviewPrisma.$queryRaw( diff --git a/test/unit/ChallengeService.test.js b/test/unit/ChallengeService.test.js index 6c5a47f..118c55f 100644 --- a/test/unit/ChallengeService.test.js +++ b/test/unit/ChallengeService.test.js @@ -25,6 +25,7 @@ const prisma = getClient(); const reviewSchema = config.get("REVIEW_DB_SCHEMA"); const reviewTableName = `"${reviewSchema}"."review"`; const submissionTableName = `"${reviewSchema}"."submission"`; +const aiReviewDecisionTableName = `"${reviewSchema}"."aiReviewDecision"`; const should = chai.should(); let reviewClient; @@ -107,7 +108,21 @@ describe("challenge service unit tests", () => { ALTER TABLE ${submissionTableName} ADD COLUMN IF NOT EXISTS "isLatest" boolean `); + await reviewClient.$executeRawUnsafe(` + ALTER TABLE ${submissionTableName} + ADD COLUMN IF NOT EXISTS "virusScan" boolean + `); + await reviewClient.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS ${aiReviewDecisionTableName} ( + "id" varchar(36) PRIMARY KEY, + "submissionId" varchar(64), + "configId" varchar(255), + "isFinal" boolean + ) + `); await reviewClient.$executeRawUnsafe(`DELETE FROM ${submissionTableName}`); + await reviewClient.$executeRawUnsafe(`DELETE FROM ${aiReviewDecisionTableName}`); + await reviewClient.$executeRawUnsafe(`DELETE FROM ${aiReviewDecisionTableName}`); testChallengeData = { typeId: data.challenge.typeId, @@ -2965,6 +2980,81 @@ describe("challenge service unit tests", () => { }); }); + describe("AI phase closure tests", () => { + let originalGetAIReviewConfigByChallengeId; + + beforeEach(() => { + originalGetAIReviewConfigByChallengeId = helper.getAIReviewConfigByChallengeId; + }); + + afterEach(async () => { + helper.getAIReviewConfigByChallengeId = originalGetAIReviewConfigByChallengeId; + await reviewClient.$executeRawUnsafe( + `DELETE FROM ${submissionTableName} WHERE "challengeId" = '${data.challenge.id}'`, + ); + await reviewClient.$executeRawUnsafe( + `DELETE FROM ${aiReviewDecisionTableName} WHERE "configId" = 'ai-config-test'`, + ); + await prisma.challengeReviewer.deleteMany({ where: { challengeId: data.challenge.id } }); + }); + + it("skips latest submissions that failed virus scan when checking AI phase closure readiness", async () => { + helper.getAIReviewConfigByChallengeId = async () => ({ id: "ai-config-test" }); + await prisma.challengeReviewer.create({ + data: { + id: uuid(), + challengeId: data.challenge.id, + scorecardId: "ai-scorecard-id", + isMemberReview: false, + aiWorkflowId: "workflow-1", + createdBy: "test", + updatedBy: "test", + }, + }); + + await reviewClient.$executeRawUnsafe(` + INSERT INTO ${submissionTableName} + ("id", "challengeId", "memberId", "type", "status", "submittedDate", "virusScan") + VALUES + ('sub-1', '${data.challenge.id}', 'member-1', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-01T00:00:00Z', TRUE), + ('sub-2', '${data.challenge.id}', 'member-1', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-02T00:00:00Z', FALSE) + `); + + await reviewClient.$executeRawUnsafe(` + INSERT INTO ${aiReviewDecisionTableName} + ("id", "submissionId", "configId", "isFinal") + VALUES + ('dec-1', 'sub-1', 'ai-config-test', TRUE) + `); + + await service.ensureAIPhaseCanBeClosed(data.challenge.id, "AI Screening"); + }); + + it("allows AI phase closure when all latest submissions failed virus scan", async () => { + helper.getAIReviewConfigByChallengeId = async () => ({ id: "ai-config-test" }); + await prisma.challengeReviewer.create({ + data: { + id: uuid(), + challengeId: data.challenge.id, + scorecardId: "ai-scorecard-id", + isMemberReview: false, + aiWorkflowId: "workflow-2", + createdBy: "test", + updatedBy: "test", + }, + }); + + await reviewClient.$executeRawUnsafe(` + INSERT INTO ${submissionTableName} + ("id", "challengeId", "memberId", "type", "status", "submittedDate", "virusScan") + VALUES + ('sub-3', '${data.challenge.id}', 'member-2', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-03T00:00:00Z', FALSE) + `); + + await service.ensureAIPhaseCanBeClosed(data.challenge.id, "AI Screening"); + }); + }); + describe("close marathon match tests", () => { const adminUser = { isMachine: false, roles: [constants.UserRoles.Admin], userId: "admin" }; const m2mUser = { isMachine: true };