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
10 changes: 7 additions & 3 deletions src/apps/opportunities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1535,7 +1535,7 @@ const SubmissionsTab: FC<SubmissionsTabProps> = props => {
<tbody>
{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(' ', '')}`]
Expand Down
84 changes: 82 additions & 2 deletions src/apps/opportunities/src/utils/marathon-match.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
49 changes: 47 additions & 2 deletions src/apps/opportunities/src/utils/marathon-match.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChallengeOpportunity, 'currentPhase' | 'currentPhaseNames' | 'phases'>,
): 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<NonNullable<MarathonTestProgress['process']>>()
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.
*
Expand Down Expand Up @@ -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<ChallengeOpportunity, 'currentPhase' | 'currentPhaseNames' | 'phases'>,
): MarathonTestProgress {
const candidates = submissionSummations(submission)
.map((summation, index) => {
Expand Down Expand Up @@ -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 {}
Expand Down
Loading