test: guard taro persona flows

This commit is contained in:
Codex
2026-07-01 01:59:23 +08:00
parent dffc319d66
commit c3e1f27a0a
7 changed files with 394 additions and 7 deletions

View File

@@ -143,11 +143,13 @@ apps/taro/dist/h5-platform-admin
```bash
node scripts/taro-route-contract-test.js
node scripts/taro-api-contract-test.js
node scripts/taro-persona-contract-test.js
npm run smoke:taro:h5
```
`taro-route-contract-test` 会校验 `apps/taro/src/app.config.ts`、真实 `pages/**/index.tsx`、启动页三端跳转、H5 静态烟测入口和前端交接文档中的页面引用保持一致。新增或删除页面时必须同步路由和文档,避免 H5/小程序构建后才发现入口漂移。
`taro-api-contract-test` 会比对 `apps/taro/src` 中所有 `apiRequest('/api/...')` 调用与 `apps/api/src/features/*/index.ts` 注册路由,阻断前端调用不存在 API、method 写错或绕过统一 `/api` 命名空间的漂移;动态导入和少量 server alias 需要在脚本 allowlist 中显式声明。
`taro-persona-contract-test` 会从学生、租户管理员、平台管理员三类前端视角检查关键页面、路由和服务调用,阻断刷题、会员订单、错题收藏、学生运营、内容导入、营销财务、租户设置、平台租户账务和公共题库授权入口被误删或漂移。
H5 线上推荐每个静态目录放独立 `runtime-config.json` 覆盖公开配置,避免 API/Auth 域名变化时重打包:
@@ -598,6 +600,8 @@ node scripts\taro-h5-release-guardrails-test.js --require-dist --require-runtime
| 150 | 60s | 6% | 44,889 | 0.00% | 735.22 req/s | 357.74 ms | 469.28 ms |
| 100 | 60s | 8% | 43,379 | 0.00% | 710.38 req/s | 257.59 ms | 328.75 ms |
| 150 | 60s | 6% | 43,738 | 0.00% | 716.06 req/s | 375.03 ms | 485.60 ms |
| 100 | 60s | 8% | 45,047 | 0.00% | 737.22 req/s | 244.12 ms | 320.26 ms |
| 150 | 60s | 6% | 41,707 | 0.00% | 681.44 req/s | 387.58 ms | 510.79 ms |
| 30 | 120s | 10% | 112,896 | 0.00% | 934.74 req/s | 64.26 ms | 81.14 ms |
| 50 | 120s | 10% | 92,737 | 0.00% | 767.24 req/s | 121.51 ms | 159.12 ms |
| 100 | 120s | 8% | 84,608 | 0.00% | 699.27 req/s | 254.69 ms | 331.84 ms |
@@ -611,7 +615,7 @@ npm run perf:summary -- --input docs/refactor/performance-reports/api-benchmark-
使用 `--allow-writes` 时会输出 `capacityObservation`,不输出 `launchGateCheck`,不能把写入场景误填成生产上线门禁的只读证据。
本地结论100 个无停顿 worker 内 P95 仍低于 300ms150 worker 零错误但 P95 已明显上升,可作为本机 Docker 环境的压力拐点参考。最新只读 30 worker/120s 为 897.04 req/s最新混合读写 100 worker/60s/8% 写入为 710.38 req/s、P95 257.59ms、0 错误150 worker/60s/6% 写入仍 0 错误,但 P95 已到 375.03ms。工程上建议把本机 Docker 舒适区暂记为 100 个无停顿压测 worker按单学生 0.05 到 0.2 req/s 的页面节奏粗略折算,约可支撑 3,000 到 14,000 名活跃在线学生的请求吞吐观察区间,但这不是生产 SLA。当前 PostgreSQL evidence 提醒本地默认库仍是 `jit=on``statement_timeout``idle_in_transaction_session_timeout``lock_timeout` 未设置,上云必须按调参文档复核。正式对外容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL 参数、生产对象存储/CDN 和真实前端请求节奏下复跑。脱敏摘要见:
本地结论100 个无停顿 worker 内 P95 仍低于 300ms150 worker 零错误但 P95 已明显上升,可作为本机 Docker 环境的压力拐点参考。最新只读 30 worker/120s 为 897.04 req/s最新混合读写 100 worker/60s/8% 写入为 737.22 req/s、P95 244.12ms、P99 320.26ms、0 错误150 worker/60s/6% 写入仍 0 错误,但 P95 已到 387.58ms、P99 510.79ms。工程上建议把本机 Docker 舒适区暂记为 100 个无停顿压测 worker按单学生 0.05 到 0.2 req/s 的页面节奏粗略折算,约可支撑 3,700 到 14,700 名活跃在线学生的请求吞吐观察区间;按 150 worker 压力区吞吐折算约为 3,400 到 13,600 名,但这不是生产 SLA。当前 PostgreSQL evidence 提醒本地默认库仍是 `jit=on``statement_timeout``idle_in_transaction_session_timeout``lock_timeout` 未设置,上云必须按调参文档复核。正式对外容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL 参数、生产对象存储/CDN 和真实前端请求节奏下复跑。脱敏摘要见:
```text
docs/refactor/performance-benchmark-summary-20260630.md

View File

@@ -55,6 +55,7 @@
- 严禁在学生端默认请求或展示排行榜;排行榜接口只作为租户显式开启后的活动能力,默认学习激励以后台配置的勋章自动发放为主。
- 严禁新增 Taro 页面后不注册路由、不更新启动页/静态烟测入口或不更新前端交接清单;页面变更后必须运行 `node scripts/taro-route-contract-test.js`,确保 `app.config.ts``pages/**/index.tsx`、启动页跳转和交接文档一致。
- 严禁在 Taro service 中调用未注册的后端 API、写错 HTTP method 或绕过统一 `/api` 命名空间API service 变更后必须运行 `node scripts/taro-api-contract-test.js`,动态路由必须在脚本 allowlist 中显式说明后端落点。
- 严禁在重做页面样式时删除学生刷题/会员订单/错题收藏、租户学生运营/内容导入/营销财务/品牌权限、平台租户/账务/公共题库/员工权限这些关键角色旅程的服务调用或跳转;相关页面变更后必须运行 `node scripts/taro-persona-contract-test.js`,真实业务确实改名时要同步更新脚本、接口文档和前端交接清单。
## Supabase 直连表的准入条件

View File

@@ -43,6 +43,7 @@
- `apps/taro/src/services/api.ts` 现在默认 Supabase JWT 优先、迁移期 `tk_` 兜底;公共接口必须显式 `authMode='none'`。页面不要手写 `Authorization``x-tenant-id``x-user-id`
- 新增、删除或重命名 Taro 页面时必须同步 `apps/taro/src/app.config.ts`、启动页跳转、H5 静态烟测入口和本文页面清单,并运行 `node scripts/taro-route-contract-test.js`。该脚本会阻断“页面文件存在但未注册”“路由注册但文件缺失”“启动页或烟测跳到不存在页面”的漂移。
- 新增或修改 Taro API service 时必须运行 `node scripts/taro-api-contract-test.js`。该脚本会比对前端 `apiRequest('/api/...')` 与后端 `RouteDefinition[]` 注册表阻断调用不存在的接口、method 写错或绕过统一 API client动态路由只能通过脚本 allowlist 明确声明。
- 修改学生端、租户后台或平台后台关键页面时必须运行 `node scripts/taro-persona-contract-test.js`。该脚本按学生刷题/会员订单/错题收藏、租户学生运营/内容导入/营销财务/品牌权限、平台租户/账务/公共题库/员工权限三类角色旅程检查路由和服务调用,避免前端样式重做时误删核心业务入口。
- H5 可以优先验证 `@supabase/supabase-js` 管理 Auth session微信小程序端先验证运行时兼容性业务数据默认仍走 `apps/api`
- H5 生产部署优先用每个静态目录自己的 `runtime-config.json` 配置 `apiBaseUrl``supabaseUrl``supabasePublishableKey``tenantCode`;不要为了换域名重打包,也不要把任何 service role、数据库、支付、短信、对象存储密钥放进该文件。
- 上线前需要把三套 H5 构建、`npm run smoke:taro:h5` 静态启动烟测、严格 `taro-h5-release-guardrails-test --require-runtime-config``runtime-config.json` 人工复核、真实 Auth/RLS、迁移 dry-run、对象存储、支付对账和真实 `@codex-security` 结果写入 `production-launch-evidence.json`,并通过 `npm run launch:gate`。当前环境没有暴露安全扫描工具时只能标记待补,不能把模板占位当完成。

View File

@@ -94,7 +94,7 @@
- 已补 `npm run launch:gate` 生产上线证据门禁和 `docs/refactor/production-launch-evidence.template.json` 模板;最终切换前必须把 readiness、远程 Auth、RLS、生产 dry-run、导入校验、`pb:import:sample` 业务抽样、真实数据 API 读路径压测、API/worker/Taro、运行时审计、`@codex-security`、备份/回滚/真实抽样/生产 provider 等证据填入本地 `production-launch-evidence.json` 并通过门禁。当前 Codex 环境未暴露可调用的 `@codex-security` 扫描工具时,该项只能标为待补,不能伪造完成。
- 确认数据库迁移流程、备份恢复、日志、告警。
- 准备 API 容器部署和 Supabase 云端/自托管连接方案。
- 已补 `npm run perf:api:local``npm run perf:summary``npm run perf:postgres:evidence``npm run smoke:launch-persona``npm run smoke:taro:h5``node scripts/taro-api-contract-test.js``docs/refactor/performance-benchmark-runbook.md`可在本地或云端对真实迁移数据做只读门禁、混合读写容量观察、PostgreSQL 调参证据、三类角色旅程烟测、H5 发布目录启动烟测和前端 API 契约检查。2026-07-01 本地真实迁移库只读 30 worker/120s 为 108336 请求、0 错误、897.04 req/s、P95 68.32ms、P99 84.50ms;最新混合读写复测 100 worker/60s/8% 写入为 43379 请求、0 错误、710.38 req/s、P95 257.59ms、P99 328.75ms150 worker/60s/6% 写入为 43738 请求、0 错误、716.06 req/s、P95 375.03ms、P99 485.60ms,说明本地舒适区暂按 100 个无停顿 worker 估算,压力上沿仍在 100 到 150 worker 之间。
- 已补 `npm run perf:api:local``npm run perf:summary``npm run perf:postgres:evidence``npm run smoke:launch-persona``npm run smoke:taro:h5``node scripts/taro-api-contract-test.js``node scripts/taro-persona-contract-test.js``docs/refactor/performance-benchmark-runbook.md`可在本地或云端对真实迁移数据做只读门禁、混合读写容量观察、PostgreSQL 调参证据、三类后端角色旅程烟测、三类 Taro 前端角色旅程契约、H5 发布目录启动烟测和前端 API 契约检查。2026-07-01 本地真实迁移库只读 30 worker/120s 为 108336 请求、0 错误、897.04 req/s、P95 68.32ms、P99 84.50ms;最新混合读写复测 100 worker/60s/8% 写入为 43379 请求、0 错误、710.38 req/s、P95 257.59ms、P99 328.75ms150 worker/60s/6% 写入为 43738 请求、0 错误、716.06 req/s、P95 375.03ms、P99 485.60ms,说明本地舒适区暂按 100 个无停顿 worker 估算,压力上沿仍在 100 到 150 worker 之间。
- 当前本地 PostgreSQL evidence 仍提示 `jit=on``statement_timeout=0``idle_in_transaction_session_timeout=0``lock_timeout=0`;上云后必须按 `docs/refactor/postgresql-4c16g-tuning.md` 调整参数并复跑 evidence。4 核 16G 正式容量报告需上云后按 6/30/50/100 阶梯并发复跑并归档到本地上线证据。
### P1 商用功能完善
@@ -209,7 +209,7 @@
- 旧原生小程序前端位于 `F:\project\参考\旧题库小程序前端文件`,后续 Taro 小程序/H5 交互补齐时只作为交互、页面状态和微信平台能力参考,不继承旧直连 PocketBase 或旧鉴权逻辑。
- 租户通过域名、小程序配置或启动参数解析。
- 页面主题、品牌、功能开关都从后端租户配置读取;学生端和后台只消费 `/api/tenant/resolve` 的已发布 `branding.theme/publicAssets`,租户后台草稿只通过 `/api/tenant-admin/theme` 查看。
- 当前已完成 H5 学生端、租户后台、平台后台三套构建入口和统一 API client已补 `apps/taro/src/index.html` 模板,三套 H5 构建都会生成 `index.html`;已补 `scripts/taro-route-contract-test.js``scripts/taro-h5-release-guardrails-test.js``scripts/taro-h5-static-smoke.js`,分别校验页面路由契约、源码/产物/runtime-config 安全边界和 H5 静态启动契约,阻断旧 PocketBase、`x-user-id`、平台本地 key、数据库连接串、服务端密钥形态页面路由漂移进入前端产物。学生端、租户后台、平台后台都有第一批真实 API 页面;学生端已补地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题、错题/收藏复习、题目反馈、视频解析、练习/模考报告、个人中心学习报告可视化、会员收银台、订单详情、售后入口、独立消息中心、积分任务/兑换/积分明细第一版、男女预设头像选择 UI、题干/选项/解析/知识手册 RichContent 安全渲染、知识手册章节内搜索/安全摘要高亮/目录定位、H5 KaTeX 公式渲染、私有资源 ID 题图短签名渲染、逐题复盘、背单词卡片学习/发音/收藏练习第一版;平台后台已接入创建租户、租户详情、状态变更、账务资料维护、平台员工列表/创建/编辑/禁用恢复、平台审计查询/CSV 导出、开放审计告警展示/确认/解决、审计告警外部通知渠道/事件状态摘要、催缴外部通知渠道/事件摘要、订阅、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、收款、用量和公共题库授权第一版写操作;租户后台已接权限驱动工作台、学生运营操作台、学习督导自动化/每日规则保存、主题模板预览/发布、角色模板、成员绑定、CRM/分佣操作台、积分任务/兑换操作台和用户通知查看第一版;下一步补小程序公式真机验收、题图资源字段化、状态管理、首屏包体优化、更细数据范围 UI、督导触达联动和小程序兼容验证。
- 当前已完成 H5 学生端、租户后台、平台后台三套构建入口和统一 API client已补 `apps/taro/src/index.html` 模板,三套 H5 构建都会生成 `index.html`;已补 `scripts/taro-route-contract-test.js``scripts/taro-api-contract-test.js``scripts/taro-persona-contract-test.js``scripts/taro-h5-release-guardrails-test.js``scripts/taro-h5-static-smoke.js`,分别校验页面路由契约、前端 API 契约、学生/租户/平台三类关键角色旅程、源码/产物/runtime-config 安全边界和 H5 静态启动契约,阻断旧 PocketBase、`x-user-id`、平台本地 key、数据库连接串、服务端密钥形态页面路由漂移、API 漂移和关键业务入口误删进入前端产物。学生端、租户后台、平台后台都有第一批真实 API 页面;学生端已补地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题、错题/收藏复习、题目反馈、视频解析、练习/模考报告、个人中心学习报告可视化、会员收银台、订单详情、售后入口、独立消息中心、积分任务/兑换/积分明细第一版、男女预设头像选择 UI、题干/选项/解析/知识手册 RichContent 安全渲染、知识手册章节内搜索/安全摘要高亮/目录定位、H5 KaTeX 公式渲染、私有资源 ID 题图短签名渲染、逐题复盘、背单词卡片学习/发音/收藏练习第一版;平台后台已接入创建租户、租户详情、状态变更、账务资料维护、平台员工列表/创建/编辑/禁用恢复、平台审计查询/CSV 导出、开放审计告警展示/确认/解决、审计告警外部通知渠道/事件状态摘要、催缴外部通知渠道/事件摘要、订阅、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、收款、用量和公共题库授权第一版写操作;租户后台已接权限驱动工作台、学生运营操作台、学习督导自动化/每日规则保存、主题模板预览/发布、角色模板、成员绑定、CRM/分佣操作台、积分任务/兑换操作台和用户通知查看第一版;下一步补小程序公式真机验收、题图资源字段化、状态管理、首屏包体优化、更细数据范围 UI、督导触达联动和小程序兼容验证。
### 第一批页面

View File

@@ -69,11 +69,22 @@
同轮 `npm run perf:summary -- --allow-writes` 已分别通过 100 worker `P95 <= 300ms/P99 <= 800ms` 和 150 worker `P95 <= 500ms/P99 <= 1200ms` 的容量观察阈值150 worker 不建议作为当前本机 Docker 日常舒适区。
### 2026-07-01 Taro 角色旅程契约后读写复核
本轮在新增 `node scripts/taro-persona-contract-test.js` 并纳入 `npm run test:readiness` 后复跑,确认前端角色旅程契约守卫不会影响后端真实数据刷题读写容量。压测仍使用本地 Docker/Supabase 真实迁移库,排行榜未纳入默认负载。
| 并发 worker | 时长 | 刷题写入比例 | 请求数 | 错误率 | 吞吐 | P95 | P99 | 结论 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| 100 | 60s | 8% | 45,047 | 0.00% | 737.22 req/s | 244.12 ms | 320.26 ms | 通过 100 worker 舒适区门槛 |
| 150 | 60s | 6% | 41,707 | 0.00% | 681.44 req/s | 387.58 ms | 510.79 ms | 零错误但进入压力区 |
同轮 `npm run perf:summary -- --allow-writes` 已分别通过 100 worker `P95 <= 300ms/P99 <= 800ms` 和 150 worker `P95 <= 500ms/P99 <= 1200ms` 的容量观察阈值。
## 初步结论
- 本地 Docker 环境下100 个无停顿 worker 内 P95 仍低于 300ms可以作为当前代码和索引状态的本地舒适区参考2026-07-01 最新 100 worker/60s/8% 写入为 710.38 req/s、P95 257.59ms、0 错误。
- 150 个无停顿 worker 仍然 0 错误,但 P95 在不同轮次中接近或超过 300ms,最新 150 worker/60s/6% 写入为 716.06 req/s、P95 375.03ms已经能看到本地压力拐点。
- 按最新 897 req/s 只读吞吐粗略折算,如果未来前端真实埋点显示每名在线学生平均 0.05 到 0.2 req/s则理论请求吞吐约对应 4,485 到 17,940 名活跃在线学生;按更保守的 700 req/s 估算约为 3,500 到 14,000 名。这只是吞吐换算,不是生产 SLA。
- 本地 Docker 环境下100 个无停顿 worker 内 P95 仍低于 300ms可以作为当前代码和索引状态的本地舒适区参考2026-07-01 最新 100 worker/60s/8% 写入为 737.22 req/s、P95 244.12ms、P99 320.26ms、0 错误。
- 150 个无停顿 worker 仍然 0 错误,但 P95 在不同轮次中明显升高,最新 150 worker/60s/6% 写入为 681.44 req/s、P95 387.58ms、P99 510.79ms已经能看到本地压力拐点。
- 按最新 897 req/s 只读吞吐粗略折算,如果未来前端真实埋点显示每名在线学生平均 0.05 到 0.2 req/s则理论请求吞吐约对应 4,485 到 17,940 名活跃在线学生;按最新混合读写舒适区 737 req/s 估算约为 3,685 到 14,744 名;按 150 worker 压力区 681 req/s 估算约为 3,407 到 13,629 名。这只是吞吐换算,不是生产 SLA。
- 正式容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL 参数、对象存储/CDN、真实前端请求节奏和生产网络下复跑。
## 后续复测

View File

@@ -55,7 +55,7 @@
"test:worker:public-banks": "npm run db:smoke-seed && npm run build:worker && node scripts/public-bank-worker-integration-test.js",
"test:worker:student-supervision": "npm run db:smoke-seed && npm run build:worker && node scripts/student-supervision-worker-integration-test.js",
"test:rls": "npm run db:smoke-seed && node scripts/rls-tenant-isolation-test.js",
"test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/production-launch-gate-test.js",
"test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/production-launch-gate-test.js",
"test:auth:remote-smoke": "node scripts/remote-auth-jwt-smoke-test.js",
"test:launch-gate": "node scripts/production-launch-gate-test.js",
"smoke:launch-persona": "npm run build:api && node scripts/launch-persona-smoke.js",

View File

@@ -0,0 +1,370 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
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',
'loadCurrentUser',
'/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',
'Taro.requestPayment',
'/pages/student/order-detail/index?orderNo=',
],
},
{
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: [
'loadTenantPermissions',
'canOpenModule',
'/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',
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',
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',
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}`);
}
function parseAppRoutes() {
const text = readText('app.config.ts');
const match = text.match(/pages\s*:\s*\[([\s\S]*?)\]/m);
assert.ok(match, 'apps/taro/src/app.config.ts must define pages');
return new Set([...match[1].matchAll(/['"`]([^'"`]+)['"`]/g)].map(item => item[1]));
}
const appRoutes = parseAppRoutes();
for (const contract of routeContracts) {
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)`);