diff --git a/src/apps/opportunities/README.md b/src/apps/opportunities/README.md index 08df66cae..0a0fb6aad 100644 --- a/src/apps/opportunities/README.md +++ b/src/apps/opportunities/README.md @@ -455,9 +455,13 @@ Marathon Match attempts fall back to Review submission, virus-scan, and scoring lifecycle fields when test metadata is absent, preserving truthful Failed, In progress, and completed states. Virus-scan and quarantine failures are reported as Failed in the Provisional process with explicit 0% progress; later review -failures remain System failures. Their actions include the clean submission, -scorer artifacts, and submission history, while the single page-level button -owns the Review App handoff. +failures remain System failures. A newly active attempt stays in the Provisional +process while its first scorer result is pending during Submission, then uses the +System process during Review. Unknown phases are not guessed. Active My Submissions +views refresh attempts and their embedded scorer summaries so completed results +appear without a page reload. Their actions include the clean submission, scorer +artifacts, and submission history, while the single page-level button owns the +Review App handoff. The Marathon Match My Submissions table reserves enough width for the complete submission timestamp and keeps its date heading and sort icon on one line, aligned with the dates beneath it. Score columns remain right aligned. diff --git a/src/apps/opportunities/src/pages/ChallengeDetailsPage.tsx b/src/apps/opportunities/src/pages/ChallengeDetailsPage.tsx index 7e5dd8825..aa7ca6903 100644 --- a/src/apps/opportunities/src/pages/ChallengeDetailsPage.tsx +++ b/src/apps/opportunities/src/pages/ChallengeDetailsPage.tsx @@ -1535,7 +1535,7 @@ const SubmissionsTab: FC = props => { {submissions.map(submission => { const scores = marathonSubmissionScores(submission) - const progress = marathonSubmissionTestProgress(submission) + const progress = marathonSubmissionTestProgress(submission, props.challenge) const reviewUrl = challengeReviewAppUrl(props.challenge.id) const statusClass = progress.status ? styles[`testStatus${progress.status.replace(' ', '')}`] diff --git a/src/apps/opportunities/src/utils/marathon-match.utils.spec.ts b/src/apps/opportunities/src/utils/marathon-match.utils.spec.ts index fda59e8ef..caf10a21a 100644 --- a/src/apps/opportunities/src/utils/marathon-match.utils.spec.ts +++ b/src/apps/opportunities/src/utils/marathon-match.utils.spec.ts @@ -219,14 +219,94 @@ describe('Marathon Match challenge detail utilities', () => { })) .toEqual({ process: 'Provisional', progress: 0, status: 'Failed' }) expect(marathonSubmissionTestProgress({ - id: 'active', + id: 'active-system-review', + review: [{ status: 'IN_PROGRESS' }], status: 'ACTIVE', })) + .toEqual({ process: 'System', progress: 0, status: 'In progress' }) + }) + + it('keeps an active submission provisional through its first scorer result', () => { + const submissionPhase = { + currentPhaseNames: ['Registration', 'Submission'], + phases: [ + { isOpen: true, name: 'Registration' }, + { isOpen: true, name: 'Submission' }, + { isOpen: false, name: 'Review' }, + ], + } + expect(marathonSubmissionTestProgress({ + id: 'awaiting-provisional-score', + status: 'ACTIVE', + virusScan: true, + }, submissionPhase)) .toEqual({ - process: 'System', + process: 'Provisional', progress: 0, status: 'In progress', }) + expect(marathonSubmissionTestProgress({ + id: 'provisional-score-complete', + reviewSummation: [{ + aggregateScore: 0, + id: 'provisional-result', + isProvisional: true, + metadata: { + testProgress: 1, + testStatus: 'SUCCESS', + }, + }], + status: 'ACTIVE', + virusScan: true, + }, submissionPhase)) + .toEqual({ + process: 'Provisional', + progress: 100, + status: 'Passed', + }) + }) + + it('uses only an unambiguous challenge phase for an active submission', () => { + const activeSubmission = { id: 'active', status: 'ACTIVE' } + expect(marathonSubmissionTestProgress(activeSubmission, { + currentPhase: { isOpen: true, name: 'Review' }, + })) + .toEqual({ process: 'System', progress: 0, status: 'In progress' }) + expect(marathonSubmissionTestProgress(activeSubmission, { + currentPhaseNames: ['Open'], + })) + .toEqual({ process: 'Provisional', progress: 0, status: 'In progress' }) + expect(marathonSubmissionTestProgress(activeSubmission, { + phases: [{ isOpen: true, name: 'Topcoder Submission Final Fix' }], + })) + .toEqual({ process: 'Provisional', progress: 0, status: 'In progress' }) + expect(marathonSubmissionTestProgress(activeSubmission, { + currentPhaseNames: ['Registration'], + phases: [{ isOpen: false, name: 'Submission' }], + })) + .toEqual({}) + expect(marathonSubmissionTestProgress(activeSubmission, { + currentPhaseNames: ['Submission', 'Review'], + })) + .toEqual({}) + expect(marathonSubmissionTestProgress(activeSubmission)) + .toEqual({}) + }) + + it('uses challenge context instead of inventing a phase for a generic active review', () => { + const activeReview = { + id: 'active-review', + review: [{ status: 'IN_PROGRESS' }], + status: 'ACTIVE', + } + expect(marathonSubmissionTestProgress(activeReview, { + currentPhaseNames: ['Submission'], + })) + .toEqual({ process: 'Provisional', progress: 0, status: 'In progress' }) + expect(marathonSubmissionTestProgress(activeReview, { + currentPhaseNames: ['Submission', 'Review'], + })) + .toEqual({ progress: 0, status: 'In progress' }) }) it('builds the provisional score timeline, excludes failures, and keeps the latest rewrite', () => { diff --git a/src/apps/opportunities/src/utils/marathon-match.utils.ts b/src/apps/opportunities/src/utils/marathon-match.utils.ts index 4eb539e43..c87d73a7f 100644 --- a/src/apps/opportunities/src/utils/marathon-match.utils.ts +++ b/src/apps/opportunities/src/utils/marathon-match.utils.ts @@ -189,6 +189,36 @@ function testProcessValue(value: ChallengeReviewSummation): MarathonTestProgress return undefined } +/** + * Infers the scorer process from an explicitly current or open challenge phase. + * + * @param challenge optional Challenge API phase context. + * @returns the process expected during that phase, or undefined when it is ambiguous. + * @throws Does not throw. + */ +function challengePhaseTestProcess( + challenge?: Pick, +): MarathonTestProgress['process'] { + if (!challenge) return undefined + const phaseNames = [ + ...(challenge.currentPhaseNames ?? []), + challenge.currentPhase?.isOpen === false ? undefined : challenge.currentPhase?.name, + ...(challenge.phases ?? []) + .filter(phase => phase.isOpen === true) + .map(phase => phase.name), + ] + .map(normalizeToken) + .filter(Boolean) + const processes = new Set>() + phaseNames.forEach(name => { + if (name === 'review') processes.add('System') + if (name === 'open' || name.includes('submission') || name === 'finalfix') { + processes.add('Provisional') + } + }) + return processes.size === 1 ? [...processes][0] : undefined +} + /** * Selects the newest usable aggregate score for one phase. * @@ -332,11 +362,13 @@ export function shouldShowFinalSubmissionScores( * back to virus-scan, review, score, and submission lifecycle fields. * * @param submission Review API submission with attached summations. + * @param challenge optional Challenge API context used to identify the active scoring phase. * @returns highest-priority truthful process, progress, and status values. * @throws Does not throw. */ export function marathonSubmissionTestProgress( submission: ChallengeSubmission, + challenge?: Pick, ): MarathonTestProgress { const candidates = submissionSummations(submission) .map((summation, index) => { @@ -409,8 +441,21 @@ export function marathonSubmissionTestProgress( return { process: 'Provisional', progress: 100, status: 'Passed' } } - if (reviewStatuses.includes('In progress') || normalizeToken(submission.status) === 'active') { - return { process: 'System', progress: 0, status: 'In progress' } + const challengeProcess = challengePhaseTestProcess(challenge) + if (reviewStatuses.includes('In progress')) { + if (challengeProcess) { + return { process: challengeProcess, progress: 0, status: 'In progress' } + } + + return challenge + ? { progress: 0, status: 'In progress' } + : { process: 'System', progress: 0, status: 'In progress' } + } + + if (normalizeToken(submission.status) === 'active') { + return challengeProcess + ? { process: challengeProcess, progress: 0, status: 'In progress' } + : {} } return {}