From 7a8f91ed8cd621b4e6c2421de217c24d4d32a78f Mon Sep 17 00:00:00 2001 From: jmgasper Date: Mon, 22 Jun 2026 15:34:51 +1000 Subject: [PATCH] PM-5397: Preserve AI Engineering history dimensions What was broken AI Engineering stats could show challenge counts, but the profile challenge details view could still be empty because the member stats history response did not expose those rows under the configured AI Engineering rating path. Root cause When history rows were enriched with Challenge API metadata, configured rating-path rows kept their challenge id and name but had their stored track/type overwritten by the source challenge's native Challenge or Marathon Match dimensions. What was changed Preserve configured rating-path dimensions during history metadata enrichment so AI Engineering history remains under DATA_SCIENCE.AI Engineering while still receiving canonical challenge metadata. Any added/updated tests Added a StatisticsService regression test that verifies an AI Engineering rating-path history row stays under DATA_SCIENCE.AI Engineering after challenge metadata enrichment. --- src/services/StatisticsService.js | 17 +++++++- test/unit/StatisticsService.test.js | 60 +++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/services/StatisticsService.js b/src/services/StatisticsService.js index fc4ce6f..97e9408 100644 --- a/src/services/StatisticsService.js +++ b/src/services/StatisticsService.js @@ -1265,6 +1265,21 @@ function filterUnifiedHistoryRowsToCompletedChallenges (rows, challengeMetadataB }) } +/** + * Check whether a history row belongs to a configured rating path. + * Rating-path rows intentionally keep their stored type even when the source + * challenge was a native Challenge or Marathon Match event. + * @param {Object} row unified history row annotated with stored track/type names + * @returns {boolean} true when the row should preserve its stored rating-path dimensions + */ +function isConfiguredRatingPathHistoryRow (row) { + return !!( + getConfiguredRatingPath(config.RATING_PATHS, row && row.typeName) || + getConfiguredRatingPath(config.RATING_PATHS, row && row.typeId) || + getConfiguredRatingPathByTypeId(config.RATING_PATHS, row && row.typeId) + ) +} + /** * Attach canonical challenge ids and names to unified history rows before shaping * the response payload consumed by the profiles UI. @@ -1292,7 +1307,7 @@ function enrichUnifiedHistoryRowsWithChallengeMetadata (rows, challengeMetadataB : _.get(challenge, 'legacyRecord.legacySystemId') ) const preserveLegacyChallengeId = isLegacyNumericMarathonHistoryRow(row) - const dimension = challenge.trackId && challenge.typeId + const dimension = !isConfiguredRatingPathHistoryRow(row) && challenge.trackId && challenge.typeId ? resolveStatsDimensionForChallengeRow({ trackId: String(challenge.trackId), typeId: String(challenge.typeId) diff --git a/test/unit/StatisticsService.test.js b/test/unit/StatisticsService.test.js index 799a0fa..5078da0 100644 --- a/test/unit/StatisticsService.test.js +++ b/test/unit/StatisticsService.test.js @@ -728,6 +728,66 @@ describe('statistics service unit tests', () => { } }) + it('getHistoryStats should preserve configured rating path dimensions after challenge metadata enrichment', async () => { + const ratingDate = new Date('2026-06-18T05:41:34.931Z') + const { service, restore } = loadStatisticsService({ + ratingPaths: [ + { name: 'AI Engineering', track: 'DATA_SCIENCE', tags: ['AI', 'AI Exponential League'] } + ], + prismaStub: { + $queryRaw: async () => [], + memberStats: { + findMany: async () => [{ + trackId: 'track-ds-id', + typeId: 'rating-path-ai-engineering', + challenges: 1, + mostRecentEventDate: ratingDate + }] + }, + memberStatsHistory: { + findMany: async () => [{ + trackId: 'track-ds-id', + typeId: 'rating-path-ai-engineering', + challengeId: 'ai-history-challenge', + challengeName: null, + newRating: 840, + eventDate: ratingDate, + placement: 1, + mostRecent: true + }] + } + }, + challengeRows: [{ + id: 'ai-history-challenge', + legacyId: null, + name: 'AI Engineering Challenge', + status: 'COMPLETED', + trackId: 'track-dev-id', + typeId: 'type-challenge-id', + endDate: ratingDate, + track: { name: 'Development' }, + type: { name: 'Challenge' }, + metadata: [], + legacyRecord: null + }] + }) + + try { + const result = await service.getHistoryStats({ isMachine: true }, 'devtest1400', {}) + + result.should.have.length(1) + should.exist(result[0].DATA_SCIENCE) + should.exist(result[0].DATA_SCIENCE['AI Engineering']) + result[0].DATA_SCIENCE['AI Engineering'].history.should.have.length(1) + result[0].DATA_SCIENCE['AI Engineering'].history[0].challengeId.should.equal('ai-history-challenge') + result[0].DATA_SCIENCE['AI Engineering'].history[0].challengeName.should.equal('AI Engineering Challenge') + result[0].DATA_SCIENCE['AI Engineering'].history[0].newRating.should.equal(840) + should.not.exist(result[0].DEVELOP) + } finally { + restore() + } + }) + it('rerateMemberStats should route configured rating paths to the Marathon Match engine', async () => { let capturedOptions const { service, restore } = loadStatisticsService({