import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; const repoRoot = process.cwd(); const taroSrc = path.join(repoRoot, 'apps', 'taro', 'src'); const contracts = [ { id: 'student.home-launcher', file: 'pages/student/home/index.tsx', mustContain: [ 'loadStudentDashboard', 'useApp', '/pages/student/catalog/index', '/pages/student/review/index', '/pages/student/profile/index', '/pages/student/assets/index', ], mustNotContain: ['loadLeaderboard', 'leaderboard', '排行榜'], }, { id: 'student.catalog-to-practice', file: 'pages/student/catalog/index.tsx', mustContain: [ 'loadContentEntries', 'loadContentNodes', 'loadQuestionCollections', 'loadPracticeBlueprints', '/pages/student/practice/index?', 'collectionId=', 'blueprintId=', 'mode=', ], }, { id: 'student.practice-svip-loop', file: 'pages/student/practice/index.tsx', mustContain: [ 'createPracticeSession', 'loadPracticeSessionDetail', 'submitAnswer', 'submitPracticeSession', 'toggleQuestionFavorite', 'submitFeedback', 'loadCollectionQuestions', 'loadQuestions', 'answerStorageKey', 'indexStorageKey', 'selfJudgedCorrect', 'subAnswers', '/pages/student/reports/index?practiceSessionId=', '/pages/student/review/index?type=wrong', '/pages/student/video/index?questionId=', ], }, { id: 'student.review-wrong-favorite', file: 'pages/student/review/index.tsx', mustContain: [ 'loadWrongQuestionReviewPlan', 'loadFavoriteQuestions', 'resolveWrongQuestion', 'wrong_review', 'favorite_review', '/pages/student/practice/index?mode=', ], }, { id: 'student.checkout-svip-order', file: 'pages/student/checkout/index.tsx', mustContain: [ 'loadSvipPlans', 'claimCoupon', 'createOrder', 'createPayment', 'loadOrderStatus', 'launchPayment', '/pages/student/order-detail/index?orderNo=', ], }, { id: 'cross-platform.payment-adapter', file: 'capabilities/payment.ts', mustContain: [ 'isWeappRuntime', 'Taro.requestPayment', 'openExternalUrl', 'copyText', ], }, { id: 'student.profile-center', file: 'pages/student/profile/index.tsx', mustContain: [ 'loadProfile', 'loadEntitlements', 'loadLearningStats', 'loadPracticeHistory', 'checkActivationCode', 'redeemActivationCode', 'loadNotifications', 'updateNotificationStatus', 'loadBadges', 'avatarPreset', '/pages/student/review/index?type=wrong', '/pages/student/review/index?type=favorite', '/pages/student/checkout/index', ], mustNotContain: ['chooseImage', 'uploadFile', 'avatarUrl', 'headimgurl', 'figureurl'], }, { id: 'tenant.workbench-permission-modules', file: 'pages/tenant-admin/workbench/index.tsx', mustContain: [ 'useApp', 'canTenantMenu', '/pages/tenant-admin/dashboard/index', '/pages/tenant-admin/students/index', '/pages/tenant-admin/content/index', '/pages/tenant-admin/marketing/index', '/pages/tenant-admin/finance/index', '/pages/tenant-admin/settings/index', ], }, { id: 'tenant.students-ops-crm-supervision', file: 'pages/tenant-admin/students/index.tsx', mustContain: [ 'loadTenantStudents', 'upsertTenantStudent', 'bulkUpsertTenantStudents', 'bulkAssignTenantClassMembers', 'updateTenantStudentStatus', 'upsertTenantStudentNote', 'upsertTenantStudentFollowup', 'previewTenantStudentSupervision', 'generateTenantStudentSupervision', 'upsertTenantStudentSupervisionRule', 'pushTenantStudentsToCrm', ], mustNotContain: ['avatarUrl', 'headimgurl', 'figureurl', 'primaryRole'], }, { id: 'tenant.content-import-public-bank', file: 'pages/tenant-admin/content/index.tsx', mustContain: [ 'previewContentImport', 'executeContentImport', 'loadImportJobs', 'loadImportJobDetail', 'loadImportIssues', 'loadImportFieldMapping', 'loadImportTemplate', 'runImportPostCheck', 'loadPublicQuestionBanks', 'adoptPublicQuestionBank', 'syncPublicQuestionBank', 'loadPublicQuestionBankConflicts', 'resolvePublicQuestionBankConflict', 'resolvePublicQuestionBankConflicts', 'loadTenantContentNotifications', ], }, { id: 'tenant.marketing-commerce-growth', file: 'pages/tenant-admin/marketing/index.tsx', mustContain: [ 'loadCoupons', 'upsertCoupon', 'loadCouponRedemptions', 'loadCouponReport', 'loadCrmConfig', 'upsertCrmConfig', 'loadCrmQueue', 'loadCrmDeadLetters', 'updateCrmQueueAction', 'loadReferralConversionReport', 'loadCommissionSettings', 'updateCommissionSettings', 'generateCommissionSettlement', 'updateCommissionSettlementStatus', 'createCommissionSettlementProof', 'upsertPointActivityTask', 'upsertPointExchangeItem', 'loadUserNotifications', ], }, { id: 'tenant.finance-reconciliation', file: 'pages/tenant-admin/finance/index.tsx', mustContain: [ 'loadTenantRefunds', 'createTenantRefund', 'updateTenantRefundStatus', 'requestProviderBillDownload', 'loadProviderBillJobs', 'loadReconciliationBatches', 'loadReconciliationItems', 'createReconciliationIssue', 'updateReconciliationIssueStatus', 'loadCommerceOperationAnomalies', 'createAdjustmentVoucher', 'updateAdjustmentVoucherStatus', 'loadAdjustmentVoucherReport', ], }, { id: 'tenant.settings-brand-auth-role', file: 'pages/tenant-admin/settings/index.tsx', mustContain: [ 'loadTenantOverview', 'loadTenantDomains', 'loadPaymentAccounts', 'loadAuthProviders', 'loadThemeTemplates', 'loadTenantTheme', 'previewTenantTheme', 'publishTenantTheme', 'loadTenantPermissions', 'loadRoleTemplates', 'upsertRoleTemplate', 'disableRoleTemplate', 'loadTenantMembers', 'upsertTenantMember', 'disableTenantMember', ], }, { id: 'platform.workbench-governance', file: 'pages/platform-admin/workbench/index.tsx', mustContain: [ 'loadPlatformOverview', 'loadPlatformTenants', 'loadPlatformInvoices', 'loadPlatformQuestionBanks', 'loadPlatformQuestionBankGrants', 'loadPlatformAuditLogs', 'exportPlatformAuditLogs', 'loadPlatformAuditAlerts', 'updatePlatformAuditAlertStatus', 'loadPlatformAuditNotificationChannels', 'loadPlatformDunningNotificationChannels', '/pages/platform-admin/tenants/index', '/pages/platform-admin/billing/index', '/pages/platform-admin/question-banks/index', '/pages/platform-admin/staff/index', ], }, { id: 'platform.tenant-lifecycle', file: 'pages/platform-admin/tenants/index.tsx', mustContain: [ 'loadPlatformPlans', 'loadPlatformTenants', 'createPlatformTenant', 'loadPlatformTenantDetail', 'updatePlatformTenantStatus', 'upsertPlatformTenantBillingProfile', 'loadPlatformAuditLogs', ], }, { id: 'platform.billing-usage-dunning', file: 'pages/platform-admin/billing/index.tsx', mustContain: [ 'loadPlatformPlans', 'createPlatformSubscription', 'loadPlatformInvoices', 'createPlatformInvoiceFromSubscription', 'loadPlatformSubscriptionInvoiceCandidates', 'createPlatformInvoicesBatchFromSubscriptions', 'loadPlatformUsageOverageInvoiceCandidates', 'createPlatformInvoicesFromUsageOverage', 'confirmPlatformInvoicePayment', 'loadPlatformUsage', 'recordPlatformUsage', 'processPlatformOverdueInvoices', 'loadPlatformInvoiceReminders', ], }, { id: 'platform.public-bank-grants', file: 'pages/platform-admin/question-banks/index.tsx', mustContain: [ 'loadPlatformQuestionBanks', 'loadPlatformQuestionBankGrants', 'upsertPlatformQuestionBankGrant', 'allowedPlanCodes', 'allowedTenantIds', 'all_active_tenants', ], }, { id: 'platform.staff-permissions', file: 'pages/platform-admin/staff/index.tsx', mustContain: [ 'loadPlatformPermissions', 'loadPlatformStaff', 'upsertPlatformStaff', 'updatePlatformStaffStatus', 'authUserId', 'platformPermissions', 'revokeSessions', ], }, ]; const routeContracts = [ { id: 'student-first-web-launch', portal: 'student', routes: [ 'pages/student/home/index', 'pages/student/catalog/index', 'pages/student/practice/index', 'pages/student/review/index', 'pages/student/checkout/index', 'pages/student/order-detail/index', 'pages/student/profile/index', 'pages/student/notifications/index', ], }, { id: 'tenant-admin-launch', portal: 'tenant-admin', routes: [ 'pages/tenant-admin/workbench/index', 'pages/tenant-admin/dashboard/index', 'pages/tenant-admin/students/index', 'pages/tenant-admin/content/index', 'pages/tenant-admin/marketing/index', 'pages/tenant-admin/finance/index', 'pages/tenant-admin/settings/index', ], }, { id: 'platform-admin-launch', portal: 'platform-admin', routes: [ 'pages/platform-admin/workbench/index', 'pages/platform-admin/tenants/index', 'pages/platform-admin/billing/index', 'pages/platform-admin/question-banks/index', 'pages/platform-admin/staff/index', ], }, ]; function readText(relativePath) { return fs.readFileSync(path.join(taroSrc, relativePath), 'utf8').replace(/\r\n/g, '\n'); } function assertContains(text, token, label) { assert.ok(text.includes(token), `${label} must contain ${token}`); } function assertNotContains(text, token, label) { assert.ok(!text.includes(token), `${label} must not contain ${token}`); } async function loadPortalRoutes(portal) { process.env.TARO_APP_PORTAL = portal; process.env.TARO_ENV = 'h5'; globalThis.defineAppConfig = value => value; const moduleUrl = pathToFileURL(path.join(taroSrc, 'app.config.ts')); moduleUrl.searchParams.set('persona', portal); return new Set(((await import(moduleUrl.href)).default.pages || [])); } const originalPortal = process.env.TARO_APP_PORTAL; const originalTaroEnv = process.env.TARO_ENV; const routesByPortal = { student: await loadPortalRoutes('student'), 'tenant-admin': await loadPortalRoutes('tenant-admin'), 'platform-admin': await loadPortalRoutes('platform-admin'), }; if (originalPortal === undefined) delete process.env.TARO_APP_PORTAL; else process.env.TARO_APP_PORTAL = originalPortal; if (originalTaroEnv === undefined) delete process.env.TARO_ENV; else process.env.TARO_ENV = originalTaroEnv; for (const contract of routeContracts) { const appRoutes = routesByPortal[contract.portal]; for (const route of contract.routes) { assert.ok(appRoutes.has(route), `${contract.id} route must be registered: ${route}`); assert.ok(fs.existsSync(path.join(taroSrc, `${route}.tsx`)), `${contract.id} route file must exist: ${route}.tsx`); } } for (const contract of contracts) { const text = readText(contract.file); for (const token of contract.mustContain || []) assertContains(text, token, contract.id); for (const token of contract.mustNotContain || []) assertNotContains(text, token, contract.id); } console.log(`[PASS] Taro persona contract (${contracts.length} page contracts, ${routeContracts.length} route groups)`);