From c3e1f27a0aec03f135853d57d4a6d4ad783c6eaa Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 1 Jul 2026 01:59:23 +0800 Subject: [PATCH] test: guard taro persona flows --- README.md | 6 +- docs/refactor/ai-development-guardrails.md | 1 + docs/refactor/frontend-handoff-index.md | 1 + docs/refactor/next-development-todo.md | 4 +- .../performance-benchmark-summary-20260630.md | 17 +- package.json | 2 +- scripts/taro-persona-contract-test.js | 370 ++++++++++++++++++ 7 files changed, 394 insertions(+), 7 deletions(-) create mode 100644 scripts/taro-persona-contract-test.js diff --git a/README.md b/README.md index 686f50d0..e0c4437b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/refactor/ai-development-guardrails.md b/docs/refactor/ai-development-guardrails.md index 6f9c9896..c67f2c9a 100644 --- a/docs/refactor/ai-development-guardrails.md +++ b/docs/refactor/ai-development-guardrails.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 直连表的准入条件 diff --git a/docs/refactor/frontend-handoff-index.md b/docs/refactor/frontend-handoff-index.md index 7fbdab99..4f85c016 100644 --- a/docs/refactor/frontend-handoff-index.md +++ b/docs/refactor/frontend-handoff-index.md @@ -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`。当前环境没有暴露安全扫描工具时只能标记待补,不能把模板占位当完成。 diff --git a/docs/refactor/next-development-todo.md b/docs/refactor/next-development-todo.md index 8cd89685..94b727b7 100644 --- a/docs/refactor/next-development-todo.md +++ b/docs/refactor/next-development-todo.md @@ -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、督导触达联动和小程序兼容验证。 ### 第一批页面 diff --git a/docs/refactor/performance-benchmark-summary-20260630.md b/docs/refactor/performance-benchmark-summary-20260630.md index 8a20ce8b..24fe6f73 100644 --- a/docs/refactor/performance-benchmark-summary-20260630.md +++ b/docs/refactor/performance-benchmark-summary-20260630.md @@ -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、真实前端请求节奏和生产网络下复跑。 ## 后续复测 diff --git a/package.json b/package.json index 06f4485b..e8d284bd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/taro-persona-contract-test.js b/scripts/taro-persona-contract-test.js new file mode 100644 index 00000000..485db9db --- /dev/null +++ b/scripts/taro-persona-contract-test.js @@ -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)`);