import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; const repoRoot = process.cwd(); const studentPagesDir = path.join(repoRoot, 'apps', 'taro', 'src', 'pages', 'student'); const appConfigPath = path.join(repoRoot, 'apps', 'taro', 'src', 'app.config.ts'); const studentProfileServicePath = path.join(repoRoot, 'apps', 'taro', 'src', 'services', 'profile.ts'); function readText(filePath) { return fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n'); } function walkFiles(dir) { const entries = fs.readdirSync(dir, { withFileTypes: true }); return entries.flatMap(entry => { const entryPath = path.join(dir, entry.name); if (entry.isDirectory()) return walkFiles(entryPath); if (!/\.(ts|tsx|css|json)$/.test(entry.name)) return []; return [entryPath]; }); } function relative(filePath) { return path.relative(repoRoot, filePath).replace(/\\/g, '/'); } const appConfig = readText(appConfigPath); assert.ok( !/pages\/student\/(?:leaderboard|rank|ranking)\b/i.test(appConfig), 'Student routes must not include a default leaderboard/ranking page. Leaderboard is optional and tenant-gated.', ); const studentFiles = walkFiles(studentPagesDir); const leaderboardViolations = []; const avatarUploadViolations = []; const avatarUrlViolations = []; for (const filePath of studentFiles) { const text = readText(filePath); if (/\bloadLeaderboard\b|\/api\/learning\/leaderboard|learning\/leaderboard|leaderboard|排行榜/.test(text)) { leaderboardViolations.push(relative(filePath)); } if (/\bchooseImage\b|\buploadFile\b|sign-upload|avatar\s*upload|头像上传|上传头像|裁剪头像/.test(text)) { avatarUploadViolations.push(relative(filePath)); } if (/\bavatarUrl\b|\bavatar_url\b|\bheadimgurl\b|\bheadImgUrl\b|\bfigureurl\b/i.test(text)) { avatarUrlViolations.push(relative(filePath)); } } assert.deepEqual( [...new Set(leaderboardViolations)].sort(), [], 'Student pages must not request or render leaderboard by default. Add a tenant-gated activity page only after explicit enablement and load testing.', ); assert.deepEqual( [...new Set(avatarUploadViolations)].sort(), [], 'Student pages must not add avatar upload/crop/storage flows. Only avatarPreset=male/female is supported.', ); assert.deepEqual( [...new Set(avatarUrlViolations)].sort(), [], 'Student pages must not read or write avatarUrl/headimgurl/figureurl fields. Use avatarPreset only.', ); const profileService = readText(studentProfileServicePath); assert.ok( /avatarPreset\?: 'male' \| 'female'/.test(profileService), 'Student profile service must expose avatarPreset=male/female.', ); assert.ok( !/\bavatarUrl\b|\bavatar_url\b|\bheadimgurl\b|\bheadImgUrl\b|\bfigureurl\b/i.test(profileService), 'Student profile service must not accept or expose avatar URL fields.', ); console.log('[PASS] Taro student product guardrails');