forked from wangziqi/gongxue-base
test: guard taro persona flows
This commit is contained in:
@@ -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 仍低于 300ms;150 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 仍低于 300ms;150 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
|
||||
|
||||
@@ -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 直连表的准入条件
|
||||
|
||||
|
||||
@@ -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`。当前环境没有暴露安全扫描工具时只能标记待补,不能把模板占位当完成。
|
||||
|
||||
@@ -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.75ms;150 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.75ms;150 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、督导触达联动和小程序兼容验证。
|
||||
|
||||
### 第一批页面
|
||||
|
||||
|
||||
@@ -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、真实前端请求节奏和生产网络下复跑。
|
||||
|
||||
## 后续复测
|
||||
|
||||
@@ -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",
|
||||
|
||||
370
scripts/taro-persona-contract-test.js
Normal file
370
scripts/taro-persona-contract-test.js
Normal 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)`);
|
||||
Reference in New Issue
Block a user