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
19 changes: 11 additions & 8 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1709,10 +1709,12 @@ paths:

Copilot `trackCounts` includes only non-zero Development, Design,
Quality Assurance, and Data Science keys. Fulfillment is
`COMPLETED / (COMPLETED + CANCELLED*) * 100`; `CANCELLED` and every
`CANCELLED_*` status count as cancelled, non-terminal statuses are
excluded, a zero denominator returns 0, and the percentage is rounded
to two decimal places on a 0-100 scale.
`COMPLETED / (COMPLETED + qualifying CANCELLED*) * 100`.
`CANCELLED_CLIENT_REQUEST` remains in challenge history and track counts
but is excluded from fulfillment because it is not a Copilot failure.
Other `CANCELLED` and `CANCELLED_*` statuses count as cancelled,
non-terminal statuses are excluded, a zero denominator returns 0, and
the percentage is rounded to two decimal places on a 0-100 scale.

Cross-schema special-role queries require the `challenges` and `resources`
schemas to be co-located in the challenge database, as in the supported
Expand Down Expand Up @@ -2842,21 +2844,22 @@ definitions:
format: int32
minimum: 0
description: >
Anonymous-visible Copilot challenges in CANCELLED or any CANCELLED_*
status.
Anonymous-visible Copilot challenges in CANCELLED or CANCELLED_*
statuses included as Copilot failures. CANCELLED_CLIENT_REQUEST is
excluded.
total:
type: integer
format: int32
minimum: 0
description: Completed plus cancelled terminal challenges.
description: Completed plus included cancelled terminal challenges.
rate:
type: number
format: double
minimum: 0
maximum: 100
description: >
Completed percentage on a 0-100 scale, rounded to two decimals; zero
when there are no terminal outcomes.
when there are no included terminal outcomes.
SpecialRoleChallengeDimension:
type: object
required:
Expand Down
23 changes: 16 additions & 7 deletions src/services/SpecialRoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const { ChallengesPrisma } = prismaManager
const COPILOT_ROLE = 'copilot'
const REVIEWER_ROLE = 'reviewer'
const SPECIAL_ROLES = [COPILOT_ROLE, REVIEWER_ROLE]
const FULFILLMENT_EXCLUDED_STATUSES = new Set([
'CANCELLED_CLIENT_REQUEST'
])

const REVIEWER_ROLE_NAMES_LOWER = [
'iterative reviewer',
Expand Down Expand Up @@ -107,12 +110,14 @@ function getProfileTrackKey (track) {

/**
* Calculate Copilot fulfillment from terminal outcome counts. The percentage
* is completed divided by completed plus every `CANCELLED`/`CANCELLED_*`
* status, rounded to two decimal places on a 0-100 scale. It returns zero when
* no terminal challenge exists and does not raise.
* is completed divided by completed plus qualifying `CANCELLED`/
* `CANCELLED_*` statuses, rounded to two decimal places on a 0-100 scale.
* Client-request cancellations are excluded because they are not Copilot
* failures. It returns zero when no included terminal challenge exists and
* does not raise.
* @param {Number} completed number of completed public Copilot challenges
* @param {Number} cancelled number of cancelled public Copilot challenges
* @returns {Object} completed, cancelled, terminal total, and percentage rate
* @param {Number} cancelled number of included cancelled public Copilot challenges
* @returns {Object} completed, cancelled, included terminal total, and rate
*/
function buildFulfillment (completed, cancelled) {
const total = completed + cancelled
Expand Down Expand Up @@ -266,6 +271,7 @@ async function loadVisibleCopilotMetrics (userId) {
let cancelled = 0
_.forEach(rows, row => {
const challengeCount = Number(row.challengeCount) || 0
const status = String(row.status || '')
const trackKey = getProfileTrackKey({
track: row.track,
name: row.trackName,
Expand All @@ -274,9 +280,12 @@ async function loadVisibleCopilotMetrics (userId) {
if (trackKey && challengeCount > 0) {
trackCounts[trackKey] = (trackCounts[trackKey] || 0) + challengeCount
}
if (row.status === 'COMPLETED') {
if (status === 'COMPLETED') {
completed += challengeCount
} else if (String(row.status || '').startsWith('CANCELLED')) {
} else if (
status.startsWith('CANCELLED') &&
!FULFILLMENT_EXCLUDED_STATUSES.has(status)
) {
cancelled += challengeCount
}
})
Expand Down
38 changes: 33 additions & 5 deletions test/unit/SpecialRoleService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,24 @@ describe('special role service unit tests', () => {
}
})

it('getMemberRoleChallenges should return all Copilot rows and aggregate visible terminal metrics', async () => {
it('getMemberRoleChallenges should exclude client-request cancellations only from Copilot fulfillment', async () => {
const { service, restore } = loadSpecialRoleService({
onResourceQuery: async () => [],
onChallengeQuery: async (sql) => {
if (sql.includes('challengeType."name" AS "typeName"')) {
return [
{
id: 'copilot-4',
name: 'Client-Cancelled Copilot Challenge',
status: 'CANCELLED_CLIENT_REQUEST',
startDate: new Date('2024-04-01T00:00:00Z'),
endDate: new Date('2024-04-02T00:00:00Z'),
resourceCreatedAt: new Date('2024-04-01T12:00:00Z'),
trackId: 'data-science',
trackName: 'Data Science',
typeId: 'challenge',
typeName: 'Challenge'
},
{
id: 'copilot-3',
name: 'Newest Copilot Challenge',
Expand Down Expand Up @@ -363,6 +375,13 @@ describe('special role service unit tests', () => {
trackName: 'Design',
trackAbbreviation: 'DES',
challengeCount: 1
},
{
status: 'CANCELLED_CLIENT_REQUEST',
track: 'DATA_SCIENCE',
trackName: 'Data Science',
trackAbbreviation: 'DS',
challengeCount: 1
}
]
}
Expand All @@ -373,21 +392,30 @@ describe('special role service unit tests', () => {
try {
const result = await service.getMemberRoleChallenges('devtest1400', 'copilot')

result.total.should.equal(3)
result.trackCounts.should.deep.equal({ DEVELOPMENT: 2, DESIGN: 1 })
result.total.should.equal(4)
result.trackCounts.should.deep.equal({
DEVELOPMENT: 2,
DESIGN: 1,
DATA_SCIENCE: 1
})
result.fulfillment.should.deep.equal({
completed: 1,
cancelled: 1,
total: 2,
rate: 50
})
result.challenges.should.have.length(3)
result.challenges.should.have.length(4)
result.challenges.map(challenge => challenge.id).should.deep.equal([
'copilot-4',
'copilot-3',
'copilot-2',
'copilot-1'
])
result.challenges[1].should.deep.equal({
result.challenges[0].should.include({
id: 'copilot-4',
status: 'CANCELLED_CLIENT_REQUEST'
})
result.challenges[2].should.deep.equal({
id: 'copilot-2',
name: 'Second Copilot Challenge',
status: 'CANCELLED_ZERO_SUBMISSIONS',
Expand Down
Loading