test: write stable launch persona evidence

This commit is contained in:
Codex
2026-07-01 08:45:20 +08:00
parent cf1ae6026b
commit 91abf854fe
8 changed files with 176 additions and 27 deletions

View File

@@ -604,10 +604,12 @@ npm run perf:postgres:sql -- --profile=shared-host
```powershell
$env:DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
npm run smoke:launch-persona
$env:LAUNCH_SMOKE_AUTH_MODE="app_session"
npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md
Remove-Item Env:\LAUNCH_SMOKE_AUTH_MODE
```
`smoke:launch-persona` 会从普通学生、租户管理员、平台管理员三个视角调用真实 API默认使用 `LAUNCH_SMOKE_AUTH_MODE=app_session` 的 Bearer `tk_` session不依赖旧 `x-user-id` 或平台本地 key覆盖 SVIP 后刷题、收藏、错题复习入口、租户数据看板/主题/学生/销售转化、平台租户/套餐/审计入口和越权拒绝。它会写入少量 `launch_persona_smoke` 测试记录,生产只建议在灰度或演练租户运行。
`smoke:launch-persona` 会从普通学生、租户管理员、平台管理员三个视角调用真实 API默认使用 `LAUNCH_SMOKE_AUTH_MODE=app_session` 的 Bearer `tk_` session不依赖旧 `x-user-id` 或平台本地 key覆盖 SVIP 后刷题、收藏、错题复习入口、租户数据看板/主题/学生/销售转化、平台租户/套餐/审计入口和越权拒绝。它会写入少量 `launch_persona_smoke` 测试记录,生产只建议在灰度或演练租户运行。脚本始终保留带时间戳的 JSON/Markdown 报告;写生产证据时用 `--write``--write-md` 同步生成 `production-launch-evidence.json` 里引用的稳定 artifact。
Taro H5 发布产物守卫:
@@ -632,12 +634,16 @@ node scripts\taro-h5-release-guardrails-test.js --require-dist --require-runtime
npm --silent run manifest:taro:h5 -- --require-dist --require-runtime-config --json --write docs/refactor/launch-artifacts/taro-h5-release-manifest.json > docs/refactor/launch-artifacts/taro-h5-release-manifest.stdout.json
```
最近一次本地真实迁移库已包含前期压测写入记录,当前规模约为 19 个租户、7.4 万题、1,630 个题目合集、3,120 个练习蓝图、3,519 个单词、2,692 条知识手册、3,722 个用户、7.3 万个练习 session、30.3 万条答题记录、3.8 万条错题、1,501 个内容资源和 472 条权益。压测 worker 是无停顿请求流,不能直接等同于真实在线学生数;前端完成后需要用真实页面埋点估算单个学生平均 RPS再折算在线容量。
最近一次本地真实迁移库已包含前期压测写入记录,当前规模约为 19 个租户、74,131 道题、1,631 个题目合集、3,120 个练习蓝图、3,519 个单词、2,692 条知识手册、3,722 个平台用户、3,712 个学生资料、81,160 个练习 session、327,925 条答题记录、38,207 条错题、1,501 个内容资源和 474 条权益。压测 worker 是无停顿请求流,不能直接等同于真实在线学生数;前端完成后需要用真实页面埋点估算单个学生平均 RPS再折算在线容量。
README 只保留最新 Docker API 受限资源复核。更早的高资源 Docker Desktop 历史结果和每轮说明见 `docs/refactor/performance-benchmark-summary-20260630.md`
README 只保留最新本地真实数据压测摘要。更早的 Docker API 受限资源复核高资源 Docker Desktop 历史结果和每轮说明见 `docs/refactor/performance-benchmark-summary-20260630.md``docs/refactor/backend-open-items-and-capacity-20260701.md`
| 并发 worker | 时长 | 刷题写入比例 | 请求数 | 错误率 | 吞吐 | P95 | P99 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| API 进程 / 30 | 120s | 0% | 37,520 | 0.00% | 310.34 req/s | 251.08 ms | 388.49 ms |
| API 进程 / 50 | 60s | 10% | 25,047 | 0.00% | 410.49 req/s | 237.41 ms | 344.51 ms |
| API 进程 / 100 | 60s | 8% | 24,116 | 0.00% | 393.56 req/s | 437.37 ms | 533.63 ms |
| API 进程 / 150 | 60s | 6% | 22,864 | 0.00% | 371.29 req/s | 633.46 ms | 759.62 ms |
| Docker API 2c4g / 30 | 120s | 0% | 36,800 | 0.00% | 305.59 req/s | 241.46 ms | 351.17 ms |
| Docker API 2c4g / 50 | 60s | 10% | 24,120 | 0.00% | 398.79 req/s | 231.28 ms | 325.94 ms |
| Docker API 2c4g / 100 | 60s | 8% | 22,462 | 0.00% | 370.86 req/s | 445.26 ms | 532.32 ms |
@@ -655,7 +661,7 @@ npm run perf:summary -- --input docs/refactor/performance-reports/api-benchmark-
使用 `--allow-writes` 时会输出 `capacityObservation`,不输出 `launchGateCheck`,不能把写入场景误填成生产上线门禁的只读证据。
本地结论:当前 Docker Desktop 分配 20 CPU、约 63GB 内存,高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。2026-07-01 07:59 只限制 API 容器时50 worker 混合读写为 24,120 请求、0 错误、398.79 req/s、P95 231.28ms这是“DB 未受限”的乐观观察;按 0.05 到 0.2 req/s/人的页面节奏粗略折算,约对应 1,994 到 7,976 名活跃在线学生请求吞吐。100 worker 上沿为 370.86 req/s、P95 445.26ms约对应 1,854 到 7,417 名150 worker 进入压力区,为 340.74 req/s、P95 689.67ms约对应 1,704 到 6,815 名。2026-07-01 06:53 同时限制 API 2C/4G 和 DB 2C/8G 时30 worker 只读和 50 worker 混合读写仍 0 错误,但 P95 分别升至 1183.29ms 和 996.01ms未通过上线延迟门禁这是“DB 受限但未调 PostgreSQL 参数”的悲观观察50 worker 109.61 req/s 只可粗略折算约 548 到 2,192 名活跃在线学生的下限吞吐,不能作为可售 SLA。正式对外容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL shared-host 参数、`pg_stat_statements`、生产对象存储/CDN 和真实前端请求节奏下复跑。脱敏摘要和剩余功能清单见:
本地结论:当前 Docker Desktop 分配 20 CPU、约 63GB 内存,高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。2026-07-01 08:41 本地 API 进程压测中50 worker 混合读写为 25,047 请求、0 错误、410.49 req/s、P95 237.41ms这是当前真实迁移库的舒适观察;按 0.05 到 0.2 req/s/人的页面节奏粗略折算,约对应 2,052 到 8,210 名活跃在线学生请求吞吐。100 worker 为 393.56 req/s、P95 437.37ms约对应 1,968 到 7,871 名,可作为本机可用上沿150 worker 进入压力区,为 371.29 req/s、P95 633.46ms约对应 1,856 到 7,426 名。2026-07-01 06:53 同时限制 API 2C/4G 和 DB 2C/8G 时30 worker 只读和 50 worker 混合读写仍 0 错误,但 P95 分别升至 1183.29ms 和 996.01ms未通过上线延迟门禁这是“DB 受限但未调 PostgreSQL 参数”的悲观观察50 worker 109.61 req/s 只可粗略折算约 548 到 2,192 名活跃在线学生的下限吞吐,不能作为可售 SLA。正式对外容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL shared-host 参数、`pg_stat_statements`、生产对象存储/CDN 和真实前端请求节奏下复跑。脱敏摘要和剩余功能清单见:
```text
docs/refactor/performance-benchmark-summary-20260630.md

View File

@@ -43,9 +43,24 @@
- 环境Windows + Docker Desktop + 本地 Supabase/PostgreSQL + 本地 API 进程。
- Docker Desktop 当前资源20 CPU、约 62.7GB 内存。这个本机结果会高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。
- 数据库PocketBase 真实导入数据,并已包含前期写入压测产生的练习/答题记录。
- 当前本地迁移库规模19 个租户、约 7.4 万道题、3,722 个用户、1,631 个题目合集、3,120 个练习蓝图、3,519 个单词、2,692 条知识手册条目、7.3 万练习 session、30.3 万条答题记录、3.8 万条错题、1,501 个内容资源、472 条权益。后续 `db:smoke-seed`、集成测试和压测会继续写入练习/答题/测试租户数据,因此这里按当前本地观察描述,不把每次测试后的瞬时行数当作生产 SLA 口径。
- 当前本地迁移库规模19 个租户、74,131 道题、3,722 个平台用户、3,712 个学生资料、1,631 个题目合集、3,120 个练习蓝图、3,519 个单词、2,692 条知识手册条目、81,160 个练习 session、327,925 条答题记录、38,207 条错题、1,501 个内容资源、474 条权益。后续 `db:smoke-seed`、集成测试和压测会继续写入练习/答题/测试租户数据,因此这里按当前本地观察描述,不把每次测试后的瞬时行数当作生产 SLA 口径。
- 排行榜未纳入默认负载,因为产品默认关闭。
### 2026-07-01 08:41 本地真实迁移库 API 进程压测
本轮使用本地 Node API 进程自动启动临时端口,数据库为 Docker Desktop 中的本地 Supabase/PostgreSQL鉴权使用 `PERF_AUTH_MODE=app_session` 的 Bearer session。Docker Desktop 当前分配 20 CPU、约 62.7GB 内存,高于常见 4 核 16G 云服务器;因此这轮是“真实迁移数据 + 本机较高资源”的代码与索引能力观察,不是生产 SLA。
| 场景 | 并发 worker | 时长 | 刷题写入比例 | 请求数 | 错误率 | 吞吐 | P95 | P99 | 结论 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| 只读上线门禁 | 30 | 120s | 0% | 37,520 | 0.00% | 310.34 req/s | 251.08 ms | 388.49 ms | 通过 |
| 混合读写 | 50 | 60s | 10% | 25,047 | 0.00% | 410.49 req/s | 237.41 ms | 344.51 ms | 舒适 |
| 混合读写 | 100 | 60s | 8% | 24,116 | 0.00% | 393.56 req/s | 437.37 ms | 533.63 ms | 可用上沿 |
| 混合读写 | 150 | 60s | 6% | 22,864 | 0.00% | 371.29 req/s | 633.46 ms | 759.62 ms | 压力区 |
混合读写包含真实刷题闭环:创建练习 session、拉取 session detail、提交答案、交卷、读取报告。150 worker 仍保持 0 错误,但 P95 已超过 600ms当前本机可把 100 worker 混合读写视为可用上沿,把 50 worker 混合读写视为舒适区。
同轮 `LAUNCH_SMOKE_AUTH_MODE=app_session npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md` 已通过,并生成稳定上线证据 artifact。角色旅程覆盖普通学生 SVIP 后刷题、收藏、错题复习入口,租户管理员 dashboard/主题/学生/销售转化入口,平台管理员租户/套餐/审计入口,以及学生越权后台和跨租户访问拒绝。
### 2026-07-01 07:59 真实迁移库 Docker API 受限资源复核
本轮使用 `npm run perf:api:docker-4c16g`API 容器限制为 2 CPU/4G、`DB_POOL_MAX=10`、关闭 legacy `x-user-id` 和平台本地 key压测鉴权使用 `PERF_AUTH_MODE=app_session` 的 Bearer session。数据库仍为本机 Docker Desktop Supabase/PostgreSQL未限制为 4 核 16G因此结果更接近“受限 API 容器 + 本机数据库”的预生产观察,不是最终云服务器 SLA。
@@ -67,13 +82,13 @@
压测 worker 是无停顿请求流,不等于真实在线学生。刷题学生会读题、思考、翻页和等待网络,前端完成后还需要用真实埋点计算“单个活跃学生平均 RPS”。
按目前常见页面节奏先用 `0.05 到 0.2 req/s/人` 粗略折算:
按目前常见页面节奏先用 `0.05 到 0.2 req/s/人` 粗略折算。本轮本地 API 进程压测的在线吞吐观察为
- 50 worker 舒适场景 398.79 req/s约等于 1,994 到 7,976 名活跃在线学生的请求吞吐。
- 100 worker 上沿场景 370.86 req/s约等于 1,854 到 7,417 名活跃在线学生的请求吞吐。
- 150 worker 压力场景 340.74 req/s约等于 1,704 到 6,815 名活跃在线学生的请求吞吐。
- 50 worker 舒适场景 410.49 req/s约等于 2,052 到 8,210 名活跃在线学生的请求吞吐。
- 100 worker 可用上沿场景 393.56 req/s约等于 1,968 到 7,871 名活跃在线学生的请求吞吐。
- 150 worker 压力场景 371.29 req/s约等于 1,856 到 7,426 名活跃在线学生的请求吞吐。
当前本地结论:在受限 API 容器下,后端真实刷题读写链路的舒适观察区间约为 399 req/s折算约 1,994 到 7,976 名活跃在线学生100 worker 仍可用但 P95 已超过 400ms150 worker 仍 0 错误但 P95 已接近 700ms视为压力区不建议作为生产承诺。受限 API 容器会明显压低上沿,因此上线承诺仍以云端受限资源复测为准。
当前本地结论:在真实迁移数据和本机较高资源下,后端刷题读写链路的舒适观察区间约为 410 req/s折算约 2,052 到 8,210 名活跃在线学生100 worker 仍满足混合写入观察线但 P95 已超过 400ms可作为当前本机可用上沿;150 worker 仍 0 错误但 P95 已超过 600ms视为压力区不建议作为生产承诺。正式对外容量必须以云端 4 核 16G、PostgreSQL shared-host 参数、真实对象存储/CDN 和前端真实请求节奏复测为准。
### 2026-07-01 Docker API + DB 显式受限复核

View File

@@ -52,10 +52,10 @@ npm run perf:postgres:sql -- --profile=shared-host
角色旅程烟测:
```bash
npm run smoke:launch-persona
npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md
```
该脚本从普通学生、租户管理员、平台管理员三个视角调用真实 API覆盖 SVIP 后刷题、收藏、错题复习入口、租户 dashboard/主题/学生/销售转化、平台租户/套餐/审计入口和越权拒绝。它会写入少量 `launch_persona_smoke` 测试练习与收藏记录,只建议在本地、预生产或灰度租户运行。
该脚本从普通学生、租户管理员、平台管理员三个视角调用真实 API覆盖 SVIP 后刷题、收藏、错题复习入口、租户 dashboard/主题/学生/销售转化、平台租户/套餐/审计入口和越权拒绝。它会写入少量 `launch_persona_smoke` 测试练习与收藏记录,只建议在本地、预生产或灰度租户运行。默认会生成带时间戳的报告;生产证据必须额外带 `--write` 生成稳定 JSON`production-launch-evidence.json``launch:gate` 引用。
## 前置条件

View File

@@ -136,7 +136,7 @@
{
"id": "api.launch-persona-smoke",
"status": "pass",
"command": "LAUNCH_SMOKE_AUTH_MODE=app_session npm run smoke:launch-persona > docs/refactor/launch-artifacts/launch-persona-smoke.log",
"command": "LAUNCH_SMOKE_AUTH_MODE=app_session npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md > docs/refactor/launch-artifacts/launch-persona-smoke.log",
"completedAt": "2026-06-30T10:49:30+08:00",
"artifact": "launch-artifacts/launch-persona-smoke.json",
"summary": {

View File

@@ -87,10 +87,10 @@ npm run readiness:production:db
npm run smoke:taro:h5
npm run smoke:taro:h5:interaction
node scripts/taro-h5-release-guardrails-test.js --require-dist
npm run smoke:launch-persona
npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md
```
`smoke:launch-persona` 是真实 API 角色旅程烟测,必须进入生产上线证据;正式证据必须使用 `LAUNCH_SMOKE_AUTH_MODE=app_session`,通过 Bearer `tk_` session 验证身份,不使用旧 `x-user-id` 或平台本地 key。普通学生要能在 SVIP 权益下创建练习、答题、收藏题目、进入收藏复习和错题复习入口;租户管理员要能读取看板/主题/学生/销售转化并拒绝学生或跨租户访问;平台管理员要能读取租户/套餐/审计入口并拒绝学生访问平台后台。
`smoke:launch-persona` 是真实 API 角色旅程烟测,必须进入生产上线证据;正式证据必须使用 `LAUNCH_SMOKE_AUTH_MODE=app_session`,通过 Bearer `tk_` session 验证身份,不使用旧 `x-user-id` 或平台本地 key。普通学生要能在 SVIP 权益下创建练习、答题、收藏题目、进入收藏复习和错题复习入口;租户管理员要能读取看板/主题/学生/销售转化并拒绝学生或跨租户访问;平台管理员要能读取租户/套餐/审计入口并拒绝学生访问平台后台。生产证据命令必须带 `--write docs/refactor/launch-artifacts/launch-persona-smoke.json`,确保 `production-launch-evidence.json` 引用的 artifact 是稳定路径,不是只存在带时间戳的本地报告。
`smoke:taro:h5` 会启动临时静态服务器和 mock API验证三套 H5 的 `index.html`、JS/CSS 资源、history fallback、公开 runtime config 和 `/api/tenant/resolve` 契约。`smoke:taro:h5:interaction` 会在真实 Chrome/Edge 中点击学生、租户后台、平台后台关键路径,覆盖静态烟测发现不了的 JS 运行时、直接 history 路由刷新和 Taro 点击事件问题;当前脚本覆盖 32 项检查,包括学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台内容导入、公共题库采纳/同步/冲突处理、学生运营、营销/CRM/分佣、主题/角色/成员写操作,以及平台后台租户、账务、公共题库授权和员工写操作。`taro-h5-release-guardrails-test` 会扫描源码、三套 H5 产物和 runtime-config 边界,防止旧 PocketBase、`x-user-id``x-platform-admin-key`、数据库连接串和服务端密钥形态进入前端发布目录。若刚构建完但未放入真实 `runtime-config.json`,脚本允许 warning正式部署目录必须补齐。
@@ -100,6 +100,7 @@ npm run smoke:launch-persona
npm --silent run smoke:taro:h5 -- --json > docs/refactor/launch-artifacts/taro-h5-static-smoke.json
npm --silent run smoke:taro:h5:interaction -- --json > docs/refactor/launch-artifacts/taro-h5-interaction-smoke.json
node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime-config --json > docs/refactor/launch-artifacts/taro-h5-release-guardrails.json
npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md > docs/refactor/launch-artifacts/launch-persona-smoke.log
```
`security:repo` 是仓库自带的静态安全扫描,会拦截密钥形态、前端旧鉴权头、真实 runtime-config 和生产证据误入 Git。它不能替代真实 `@codex-security`;如插件在当前 Codex 环境暴露扫描工具,再补插件扫描结果。若工具不可用,不能把该项标记为已完成,只能在上线证据里标记为待补。

View File

@@ -58,7 +58,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/product-scope-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/taro-h5-release-manifest-test.js && node scripts/taro-visual-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/docker-benchmark-resource-evidence-test.js && node scripts/repo-security-scan-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/product-scope-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/taro-h5-release-manifest-test.js && node scripts/taro-visual-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/docker-benchmark-resource-evidence-test.js && node scripts/repo-security-scan-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/launch-persona-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,48 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import { parseCliArgs } from './launch-persona-smoke.js';
const repoRoot = process.cwd();
const writeOnly = parseCliArgs(['--write', 'docs/refactor/launch-artifacts/launch-persona-smoke.json']);
assert.equal(
writeOnly.writePath,
path.resolve(repoRoot, 'docs/refactor/launch-artifacts/launch-persona-smoke.json'),
'--write should resolve stable JSON evidence paths from the repo root',
);
assert.equal(writeOnly.writeMdPath, '', '--write should not imply a stable Markdown report path');
const equalsForm = parseCliArgs([
'--output-dir=docs/refactor/launch-artifacts',
'--write=docs/refactor/launch-artifacts/launch-persona-smoke.json',
'--write-md=docs/refactor/launch-artifacts/launch-persona-smoke.md',
'--json',
]);
assert.equal(
equalsForm.outputDir,
path.resolve(repoRoot, 'docs/refactor/launch-artifacts'),
'--output-dir should resolve stable report directories',
);
assert.equal(
equalsForm.writePath,
path.resolve(repoRoot, 'docs/refactor/launch-artifacts/launch-persona-smoke.json'),
'--write= should resolve stable JSON evidence paths',
);
assert.equal(
equalsForm.writeMdPath,
path.resolve(repoRoot, 'docs/refactor/launch-artifacts/launch-persona-smoke.md'),
'--write-md= should resolve stable Markdown evidence paths',
);
assert.throws(
() => parseCliArgs(['--write']),
/requires a file path value/,
'--write without a value should fail fast',
);
assert.throws(
() => parseCliArgs(['--unknown']),
/Unknown launch persona smoke option/,
'unknown options should fail fast instead of silently dropping evidence paths',
);
console.log('[PASS] launch persona smoke cli options');

View File

@@ -4,11 +4,16 @@ import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import pg from 'pg';
const DEFAULT_DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
const cliOptions = parseCliArgs(process.argv.slice(2));
const databaseUrl = process.env.DATABASE_URL || DEFAULT_DATABASE_URL;
const outputDir = process.env.LAUNCH_SMOKE_OUTPUT_DIR || 'docs/refactor/launch-artifacts';
const outputDir =
cliOptions.outputDir || resolvePathOption(process.env.LAUNCH_SMOKE_OUTPUT_DIR || 'docs/refactor/launch-artifacts');
const stableJsonPath = cliOptions.writePath || resolvePathOption(process.env.LAUNCH_SMOKE_WRITE_PATH || '');
const stableMdPath = cliOptions.writeMdPath || resolvePathOption(process.env.LAUNCH_SMOKE_WRITE_MD_PATH || '');
const startServer = boolEnv('LAUNCH_SMOKE_START_SERVER', !process.env.LAUNCH_SMOKE_API_BASE);
const fixedPort = Number(process.env.LAUNCH_SMOKE_API_PORT || 0) || 0;
const authMode = normalizeAuthMode(process.env.LAUNCH_SMOKE_AUTH_MODE || 'app_session');
@@ -29,6 +34,62 @@ let serverProcess = null;
let serverLogs = '';
let authTokens = {};
function resolvePathOption(value) {
if (!value) return '';
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
}
function readValueArg(argv, index, flag) {
const value = argv[index + 1];
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a file path value.`);
return value;
}
function parseCliArgs(argv = []) {
const options = {
outputDir: '',
writePath: '',
writeMdPath: '',
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === '--json') {
continue;
}
if (arg === '--output-dir') {
options.outputDir = resolvePathOption(readValueArg(argv, index, arg));
index += 1;
continue;
}
if (arg.startsWith('--output-dir=')) {
options.outputDir = resolvePathOption(arg.slice('--output-dir='.length));
continue;
}
if (arg === '--write') {
options.writePath = resolvePathOption(readValueArg(argv, index, arg));
index += 1;
continue;
}
if (arg.startsWith('--write=')) {
options.writePath = resolvePathOption(arg.slice('--write='.length));
continue;
}
if (arg === '--write-md') {
options.writeMdPath = resolvePathOption(readValueArg(argv, index, arg));
index += 1;
continue;
}
if (arg.startsWith('--write-md=')) {
options.writeMdPath = resolvePathOption(arg.slice('--write-md='.length));
continue;
}
throw new Error(`Unknown launch persona smoke option: ${arg}`);
}
return options;
}
function boolEnv(key, fallback) {
const value = process.env[key];
if (value === undefined || value === '') return fallback;
@@ -483,7 +544,8 @@ async function writeReport(report) {
await fs.mkdir(outputDir, { recursive: true });
const jsonPath = path.join(outputDir, `launch-persona-smoke-${shanghaiStamp()}.json`);
const mdPath = jsonPath.replace(/\.json$/, '.md');
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
const jsonPayload = `${JSON.stringify(report, null, 2)}\n`;
await fs.writeFile(jsonPath, jsonPayload, 'utf8');
const lines = [
'# 上线前角色旅程烟测报告',
'',
@@ -498,8 +560,19 @@ async function writeReport(report) {
'说明:该脚本会写入少量 `launch_persona_smoke` 测试权益、练习 session、答题和收藏记录。生产环境仅建议在灰度/演练租户运行。',
'',
];
await fs.writeFile(mdPath, `${lines.join('\n')}\n`, 'utf8');
return { jsonPath, mdPath };
const mdPayload = `${lines.join('\n')}\n`;
await fs.writeFile(mdPath, mdPayload, 'utf8');
if (stableJsonPath) {
await fs.mkdir(path.dirname(stableJsonPath), { recursive: true });
await fs.writeFile(stableJsonPath, jsonPayload, 'utf8');
}
if (stableMdPath) {
await fs.mkdir(path.dirname(stableMdPath), { recursive: true });
await fs.writeFile(stableMdPath, mdPayload, 'utf8');
}
return { jsonPath, mdPath, stableJsonPath, stableMdPath };
}
async function runStep(name, fn) {
@@ -543,6 +616,8 @@ async function main() {
console.log(`[launch-smoke] status=${report.status}`);
console.log(`[launch-smoke] wrote ${files.jsonPath}`);
console.log(`[launch-smoke] wrote ${files.mdPath}`);
if (files.stableJsonPath) console.log(`[launch-smoke] wrote ${files.stableJsonPath}`);
if (files.stableMdPath) console.log(`[launch-smoke] wrote ${files.stableMdPath}`);
if (report.status !== 'pass') process.exitCode = 1;
} finally {
await pool.end();
@@ -550,9 +625,13 @@ async function main() {
}
}
main().catch(error => {
console.error(error);
if (serverLogs) console.error(serverLogs);
stopServer();
process.exit(1);
});
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
main().catch(error => {
console.error(error);
if (serverLogs) console.error(serverLogs);
stopServer();
process.exit(1);
});
}
export { parseCliArgs };