Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions src/services/ChallengeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,9 +1025,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(
Expand Down
90 changes: 90 additions & 0 deletions test/unit/ChallengeService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -3234,6 +3249,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("delete challenge tests", () => {
const challengeIds = [];
let originalEnsureUserCanModifyChallenge;
Expand Down
Loading