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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
SUBMITTER,
} from '../../../config/index.config'
import {
isAiFailedReviewSubmission,
isContestReviewPhaseSubmission,
shouldIncludeInReviewPhase,
} from '../../utils/reviewPhaseGuards'
Expand Down Expand Up @@ -172,10 +173,6 @@ const sortSubmissionsByReviewScoreDesc = (
return entries.map(entry => entry.submission)
}

const isAiFailedReviewSubmission = (submission?: SubmissionInfo): boolean => (
(submission?.status || '').toUpperCase() === 'AI_FAILED_REVIEW'
)

const mergeSubmissionsById = (
primary: SubmissionInfo[],
additional: SubmissionInfo[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
BackendResource,
SubmissionInfo,
} from '../../models'
import { shouldIncludeInReviewPhase } from '../../utils/reviewPhaseGuards'
import {
isAiFailedReviewSubmission,
shouldIncludeInReviewPhase,
} from '../../utils/reviewPhaseGuards'

interface FilterIterativeReviewRowsArgs {
aiReviewDecisionsBySubmissionId?: Record<string, AiReviewDecision>
Expand All @@ -28,10 +31,6 @@ interface LimitFirst2FinishIterativeRowsOptions {
forceSingleRow?: boolean
}

function isAiFailedReviewSubmission(submission: SubmissionInfo): boolean {
return (submission.status ?? '').toUpperCase() === 'AI_FAILED_REVIEW'
}

function isAiLockedByDecision(
submission: SubmissionInfo,
aiReviewDecisionsBySubmissionId?: Record<string, AiReviewDecision>,
Expand Down
22 changes: 13 additions & 9 deletions src/apps/review/src/lib/components/TableReview/TableReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ import {
isSubmissionReviewerActionRow,
resolveSubmissionReviewResult,
} from '../common/reviewResult'
import { shouldIncludeInReviewPhase } from '../../utils/reviewPhaseGuards'
import {
isAiFailedReviewSubmission,
shouldIncludeInReviewPhase,
} from '../../utils/reviewPhaseGuards'
import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow'

import { EscalationModals } from './EscalationModals'
Expand Down Expand Up @@ -149,9 +152,11 @@ export const TableReview: FC<TableReviewProps> = (props: TableReviewProps) => {

const isTablet = useMemo<boolean>(() => screenWidth <= 744, [screenWidth])
const reviewPhaseDatas = useMemo<SubmissionInfo[]>(
() => datas.filter(submission => shouldIncludeInReviewPhase(
submission,
challengeInfo?.phases,
() => datas.filter(submission => (
// AI-locked submissions may carry no Review-phase review yet, but reviewers and
// copilots still need the row to escalate, verify, or unlock them.
isAiFailedReviewSubmission(submission)
|| shouldIncludeInReviewPhase(submission, challengeInfo?.phases)
)),
[challengeInfo?.phases, datas],
)
Expand Down Expand Up @@ -278,7 +283,7 @@ export const TableReview: FC<TableReviewProps> = (props: TableReviewProps) => {
return true
}

return (submission.status ?? '').toUpperCase() === 'AI_FAILED_REVIEW'
return isAiFailedReviewSubmission(submission)
},
),
[props.screeningOutcome.failingSubmissionIds],
Expand Down Expand Up @@ -370,7 +375,7 @@ export const TableReview: FC<TableReviewProps> = (props: TableReviewProps) => {
submission: SubmissionReviewerRow,
decision?: AiReviewEscalationDecision,
): boolean => {
if (submission.status !== 'AI_FAILED_REVIEW') {
if (!isAiFailedReviewSubmission(submission)) {
return false
}

Expand Down Expand Up @@ -839,16 +844,15 @@ export const TableReview: FC<TableReviewProps> = (props: TableReviewProps) => {
)
}

appendAction(buildPrimaryAction(), 'primary')
if (submission.isFirstReviewerRow) {
appendAction(buildPrimaryAction(), 'primary')
appendAction(buildEscalateAction(), 'escalate')
appendAction(buildVerifyAction(), 'verify')
appendAction(buildUnlockAction(), 'unlock')
appendAction(buildHistoryAction(), 'history')
appendAction(buildReopenAction(), 'reopen')
}

appendAction(buildReopenAction(), 'reopen')

if (!actionEntries.length) {
return (
<span className={styles.notReviewed}>
Expand Down
37 changes: 36 additions & 1 deletion src/apps/review/src/lib/utils/reviewPhaseGuards.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { BackendPhase, SubmissionInfo } from '../models'

import { isContestReviewPhaseSubmission } from './reviewPhaseGuards'
import {
isAiFailedReviewSubmission,
isContestReviewPhaseSubmission,
shouldIncludeInReviewPhase,
} from './reviewPhaseGuards'

const reviewPhase: BackendPhase = {
constraints: [],
Expand Down Expand Up @@ -93,3 +97,34 @@ describe('isContestReviewPhaseSubmission', () => {
.toBe(false)
})
})

describe('isAiFailedReviewSubmission', () => {
it('detects AI-locked submissions regardless of status casing', () => {
expect(isAiFailedReviewSubmission({ status: 'AI_FAILED_REVIEW' } as SubmissionInfo))
.toBe(true)
expect(isAiFailedReviewSubmission({ status: 'ai_failed_review' } as SubmissionInfo))
.toBe(true)
})

it('ignores other submission statuses', () => {
expect(isAiFailedReviewSubmission({ status: 'ACTIVE' } as SubmissionInfo))
.toBe(false)
expect(isAiFailedReviewSubmission(undefined))
.toBe(false)
})

it('keeps AI-failed submissions visible even when the phase guard excludes them', () => {
Comment thread
vas3a marked this conversation as resolved.
const aiFailedSubmission = {
id: 'submission-ai-failed',
memberId: '1001',
status: 'AI_FAILED_REVIEW',
type: 'Contest Submission',
} as SubmissionInfo

// No review-phase hints, so the phase guard alone would drop the row.
expect(shouldIncludeInReviewPhase(aiFailedSubmission, [reviewPhase]))
.toBe(false)
expect(isAiFailedReviewSubmission(aiFailedSubmission))
.toBe(true)
})
})
13 changes: 13 additions & 0 deletions src/apps/review/src/lib/utils/reviewPhaseGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,19 @@ export const isContestReviewPhaseSubmission = (
return normalizedCandidates.has(normalizeReviewPhaseKey(targetPhaseName))
}

/**
* Detects submissions the AI reviewer failed and locked.
*
* @param submission - Submission candidate.
* @returns True when the submission status marks an AI review failure.
* @throws This helper does not throw.
* Such submissions must stay visible on the Review tab so reviewers and copilots
* can escalate, verify, or unlock them even without a Review-phase review record.
*/
export const isAiFailedReviewSubmission = (submission?: SubmissionInfo): boolean => (
(submission?.status ?? '').toUpperCase() === 'AI_FAILED_REVIEW'
)

export const shouldIncludeInReviewPhase = (
submission?: SubmissionInfo,
phases?: BackendPhase[],
Expand Down
Loading