feat: add learning leaderboards

This commit is contained in:
Codex
2026-06-29 02:11:52 +08:00
parent 8f92d0427e
commit 9122bb0829
15 changed files with 724 additions and 33 deletions

View File

@@ -994,6 +994,55 @@ async function testProfile() {
assert.equal(crossTenantFeedback.code, 'AUTH_TENANT_MISMATCH', 'feedback submission must use authenticated tenant context');
}
async function testLearningLeaderboard() {
const questions = await request('/api/learning/leaderboard', {
query: { metric: 'questions', period: 'all', limit: 10 },
});
assert.equal(questions.metric, 'questions', 'question leaderboard should echo metric');
assert.ok(questions.items?.some(item => item.userId === SECOND_STUDENT_USER_ID), 'question leaderboard should include second student');
assert.ok(questions.items?.some(item => item.userId === USER_ID), 'question leaderboard should include current student');
const secondQuestions = questions.items?.find(item => item.userId === SECOND_STUDENT_USER_ID);
const currentQuestions = questions.currentUser;
assert.ok(secondQuestions?.value >= currentQuestions?.value, 'second student should rank at least as high as smoke user by questions');
assert.equal(currentQuestions?.userId, USER_ID, 'question leaderboard should include current user rank');
assert.equal(currentQuestions?.isCurrentUser, true, 'current user rank should be flagged');
const score = await request('/api/learning/leaderboard', {
query: { metric: 'score', period: 'all', limit: 10 },
});
const scoreLeader = score.items?.find(item => item.userId === SECOND_STUDENT_USER_ID);
assert.ok(scoreLeader?.value >= 30, 'score leaderboard should use platform user score');
assert.ok(score.currentUser?.value >= 10, 'score leaderboard should include current user score');
const vocabulary = await request('/api/learning/leaderboard', {
query: { metric: 'vocabulary', period: 'all', limit: 10 },
});
const vocabularyLeader = vocabulary.items?.find(item => item.userId === SECOND_STUDENT_USER_ID);
assert.equal(vocabularyLeader?.value, 2, 'vocabulary leaderboard should count mastered words');
const mockExam = await request('/api/learning/leaderboard', {
query: { metric: 'mock_exam', period: '30d', limit: 10 },
});
const mockLeader = mockExam.items?.find(item => item.userId === SECOND_STUDENT_USER_ID);
assert.equal(mockLeader?.value, 95, 'mock exam leaderboard should use best report score');
assert.ok(mockExam.currentUser?.value >= 70, 'mock exam leaderboard should include current user best score');
const classScoped = await request('/api/learning/leaderboard', {
query: { metric: 'questions', classId: ids.tenantClass, limit: 10 },
});
assert.ok(classScoped.items?.some(item => item.userId === USER_ID), 'class scoped leaderboard should include class student');
assert.ok(!classScoped.items?.some(item => item.userId === SECOND_STUDENT_USER_ID), 'class scoped leaderboard should exclude other class student');
const trustedLogin = await loginBySms('13800000000');
const crossTenantDenied = await request('/api/learning/leaderboard', {
tenantId: PARTNER_TENANT_ID,
userId: false,
headers: { authorization: `Bearer ${trustedLogin.session.token}` },
expectStatus: 403,
});
assert.equal(crossTenantDenied.code, 'AUTH_TENANT_MISMATCH', 'leaderboard must reject trusted session cross-tenant access');
}
async function testScoreline() {
const fields = await request('/api/scoreline/fields', { query: { regionId: ids.region } });
assert.ok(fields.items?.some(item => item.fieldKey === 'minScore'), 'scoreline fields should include minScore');
@@ -3326,6 +3375,7 @@ async function main() {
await check('legacy auth headers disabled', testLegacyAuthHeadersDisabled);
await check('catalog and learning', testCatalogAndLearning);
await check('profile', testProfile);
await check('learning leaderboard', testLearningLeaderboard);
await check('scoreline', testScoreline);
await check('question videos', testVideos);
await check('vocabulary', testVocabulary);