31 · 部署验收:从 HTTP 请求追到 Worker,再回到下载#
小周看到镜像启动、Web 200,就准备宣布发布完成。小陈还想确认:注册后的 Cookie 是否带进业务请求?容器 Worker 是否真的消费了导出?下载拿到的是这次验收创建的工单,还是碰巧某个成功响应?
本章沿 deployment-smoke.mjs 的真实请求逐行验证,再补它没覆盖的页面与故障故事。验收结论必须写明环境、版本、执行证据和未执行部分;教程内容完成不表示远端 CI、生产发布或故障演练已经完成。
1. 先把四类证据放到各自位置#
| 证据 | 可以回答 | 不能替代 |
|---|---|---|
| 静态检查与生产构建 | 代码能否通过规则、类型与打包 | 运行时数据库、队列与权限 |
| 数据库 / Redis 测试 | 指定业务规则与调度场景成立吗 | 真实生产镜像入口与 HTTP |
| 镜像 smoke | HTTP、会话、数据库与独立 Worker 能接棒吗 | 浏览器控件与所有角色流程 |
| 页面及故障验收 | 用户操作和故障恢复是否符合预期 | 未执行的容量、备份与外部服务保障 |
先按第 28 章跑代码检查,按第 27 章跑显式连接的集成测试。看到 skipped 要写“未执行”,不要把命令退出 0 的截图当成全部通过。教程阅读站另跑 tutorials:test,和业务部署是不同产物。
2. 给 smoke 一个可确认的数据写入目标#
沿用第 29 章的隔离项目 teamdesk-stage6 和 .env.stage6,确认这是你自己的学习环境,URL 与 BETTER_AUTH_URL、WEB_PORT 一致。若它们改过端口,下面 URL 同步修改。
docker compose -p teamdesk-stage6 --env-file .env.stage6 -f compose.deploy.yaml ps -a
curl --fail http://localhost:3100/api/health
docker compose -p teamdesk-stage6 --env-file .env.stage6 -f compose.deploy.yaml exec -T worker ./node_modules/.bin/tsx scripts/worker-health.ts核对 migrate 成功、Web / Worker 状态及探针,再运行:
SMOKE_ALLOW_CREATE=1 SMOKE_BASE_URL=http://localhost:3100 node scripts/deployment-smoke.mjs脚本 必须同时有这两个变量,否则入口抛错。SMOKE_ALLOW_CREATE 是明确的数据创建开关,不是检测目标是否生产的自动保护;设置为 1 并不会证明 URL 安全。应由操作者确认目标,不指向已有真实用户的实例。
脚本生成随机邮箱和密码,创建账号、团队、工单和任务,结束不自动删这些数据,失败后也可能留下已完成的前半段。不要反复运行而不记环境和账号;CI 用一次性部署整体回收,不代表本机要清掉现有数据卷。
3. request 帮助函数怎样把会话传下去#
先读 request(path, body),不要从最后一句“验收通过”反推覆盖范围:
// 源码节选:每次请求与 Cookie 接棒
headers: {
'Content-Type': 'application/json',
Origin: base,
Cookie: cookie,
},有 body 就 POST、否则 GET;每个 fetch 配 10000ms 超时。响应有 Set-Cookie 时,提取各项分号前的 name=value 拼成后续 Cookie;再断言 response.ok。
这模拟了当前验收流程所需的会话接棒,不是浏览器完整 Cookie jar:它没有实现通用域、路径、过期管理,也不验证浏览器 SameSite、Secure 或跨站行为。不能用 Node fetch 成功证明所有浏览器认证属性已验收。
Origin=base 用于与认证目标一致;配置错地址时先检查站点 URL,不应删掉服务端 Origin 检查来让脚本通过。
4. 按请求顺序读每一条断言#
| 顺序 | 请求 / 操作 | 直接检查的证据 |
|---|---|---|
| 1 | GET /api/health | HTTP 成功,基础数据库检查可用 |
| 2 | POST /api/auth/sign-up/email | 注册成功,Cookie 非空 |
| 3 | RPC team.create | 新团队 ID 可用于后续业务 |
| 4 | RPC ticket.create | 新团队建一张普通工单 |
| 5 | RPC task.createExport | 保存导出任务,得到 ID |
| 6 | GET task.list,按 ID 查找 | 没有 FAILED,最终见 COMPLETED |
| 7 | GET /api/exports/任务ID | 请求成功,文本包含验收标题 |
rpc 包装返回 tRPC JSON 的 result.data;列表查询将 { teamId, page:1, pageSize:10 } JSON 编码进 input。它们经过真实 HTTP 与会话,和 createCaller 直接注入 userId 的测试不同。
注意第二步是注册后建立会话,没有另一次邮箱密码登录调用。脚本末尾提示写“登录”,但解释源码时要说清它并未覆盖退出后重新登录、密码恢复或邮箱验证。
创建普通工单也会产生默认延迟提醒,但脚本不等它到期,不断言通知。管理员导出自己的新团队数据,不能证明同筛选不同客户、跨团队或跨所有者下载的负向边界;这些仍要前几章对应的测试与页面验收。
5. 轮询的“30 秒”不是总时长硬上限#
// 源码节选:循环的关键判断
const current = result.items.find((item) => item.id === task.id);
assert(current?.status !== 'FAILED', '独立 Worker 执行失败');
if (current?.status === 'COMPLETED') {
completed = true;
break;
}循环最多查 30 次,每次未完成等 1 秒;每个请求还会消耗自身网络时间,另有单次 10 秒超时。虽然最终断言消息说“30 秒内完成”,代码没有一个覆盖整个流程的 30 秒 deadline,实际墙钟时长可能更长。
列表没找到目标时 current 为 undefined,这一轮不会立即失败,而是继续等。直到没有见到 COMPLETED 才触发最终断言。排错时要区分“读到了 WAITING”和“根本没在列表找到那个 ID”,不要凭同一句超时提示推断原因。
下载只断言文本包含“容器异步导出验收”,没有解析全文件、核对全部行数或断言 Content-Disposition 等响应头。它是一条有针对性的连通性证据,不是 CSV 全部规则的测试替代。
6. 根据失败阶段追踪源码与数据#
| 失败阶段 | 优先阅读 / 核对 | 需要得到的事实 |
|---|---|---|
| health 请求失败 | 健康路由、Web / 数据库 | 状态码、连接与迁移是否准备好 |
| 注册或 Cookie 断言失败 | 认证入口、站点 URL | 目标实例、Origin、会话响应 |
| team / ticket RPC 失败 | router、成员与工单服务 | 会话是否带到请求、输入是否合规 |
| 任务一直等待 | dispatcher、Worker | 原 taskId/run、投递与消费在哪停下 |
| task FAILED | processor、lastError | 明确业务拒绝还是临时失败耗尽 |
| 下载失败或缺标题 | 下载服务、实际文件 | 所有者、资格、状态、期限、筛选 |
脚本的请求错误提示含路径和状态码,并不输出完整响应体、密码或 Cookie。诊断也应保持这个边界。独立任务失败时用 taskId/run 关联日志与账本;不要用再提交一份成功导出来掩盖原任务未恢复。
当前脚本没有打印创建的 taskId。为了事后能找到它,先记本轮开始时间、一次只运行一轮;在这套隔离部署中可做以下只读查询,再按时间和 teamId 与日志对应:
docker compose -p teamdesk-stage6 --env-file .env.stage6 -f compose.deploy.yaml exec -T postgres psql -U teamdesk -d teamdesk <<'SQL'
SELECT task.id, task."teamId", task.status, task.run,
task.attempts, task."createdAt", task."lastError"
FROM "BackgroundTask" AS task
JOIN "Team" AS team ON team.id = task."teamId"
WHERE team.name = '一次性部署验收团队' AND task.kind = 'EXPORT'
ORDER BY task."createdAt" DESC
LIMIT 10;
SQL这个查询不读取密钥、CSV 或工单正文。多轮 smoke 会有多个同名团队,不能只凭团队名称认定是同一轮;没有候选记录时,可能失败还没走到创建导出的步骤。这里也没有在宿主机直接连接部署数据库的 5433 端口。
7. 再用浏览器补一条多角色故事#
在同一学习部署准备管理员 A、客服 C、客户 D / E,以及另一团队账号。用分开的浏览器会话确认当前身份,再按顺序走:
- A 建团队,添加已注册成员并设置角色。记录 teamId 与各角色;检查客户界面不显示工作人员操作。
- D 创建“第六阶段登录问题”,C 分配给自己并回复,D 查看回复。E 读取 D 的详情和历史应被拒绝,另一团队同样不能访问。
- C 按 OPEN → IN_PROGRESS → RESOLVED 操作,解决时填写说明;再重开为 OPEN 并说明,最后关闭。历史应能对应真实操作者与动作,关闭后没有后续状态路径。
- 另用一张未关闭的学习工单,在回复输入中保留草稿后离开再回来,核对账号 / 团队 / 工单隔离;改变筛选、复制 URL,验证恢复条件与浏览器前进后退。
- C 按唯一标题筛选导出,确认任务页与下载;D 导出本人范围,另一账号访问同一文件链接应被拒绝。另用新未解决工单做一分钟提醒,不在已关闭工单上设置。
记录 Network 请求、页面提示、账本与历史中的实际值,不要求不同机器截图中的时间和版本数字完全一样;Worker 或额外修改也可能推进版本。真正要解释的是每次有效变化与权限边界。
这份清单是手动验收步骤,不是已有 Playwright 自动测试。smoke 没有浏览器 UI、Zustand 或 nuqs 操作,不能用它的一句 passed 替代这五步。
8. 故障恢复用什么证据验收#
| 故障实验 | 恢复后的核对 | 已有证据及边界 |
|---|---|---|
| 停止自己部署的 Worker,再提交导出 | 原任务 ID 从等待变完成,文件正确 | 可按第 30 章操作;本章 smoke 单次成功不覆盖中断 |
| Redis 投递失败 | 待办保留,恢复后可继续投递执行 | 第 23 章已有失败桩;不是完整真实断线恢复 |
| 旧 run / 旧执行者回来 | 新轮与租约不被旧结果覆盖 | 第 25~27 章有并发关卡与真实 Worker stalled / 迟到失败回调集成;完整容器中断仍需验收 |
| 事件写入失败 | 工单、回复、成员联动共同回滚 | 第 21 章真实数据库触发器用例;不在 HTTP smoke 内 |
| 成员降权或文件过期 | 执行 / 下载不延用旧资格 | 第 25、27 章有读取中撤权、最终授权锁与下载拒绝断言;已下载副本无法撤回 |
| SIGTERM 排空 / 退出超时 | 心跳撤销、在途租约与重启最终结果一致 | 第 30 章真实子进程用例;完整容器信号转发与默认 75 秒期限仍需验收 |
| 迁移失败或代码回退 | 不让不兼容业务恢复,确认结构与版本 | 当前没有自动回滚或备份恢复演练脚本 |
故障实验优先用受控测试和一次性环境,不删除正在使用的 Redis 队列、不对真实数据清卷。没有执行的窗口写成“待验证”,而不是“理论可恢复,所以验收通过”。
9. 写一份可以复核的交付记录#
建议每次记录:提交 SHA、镜像 / 部署目标、日期与端口、迁移结果、检查命令和退出结果、测试通过与跳过数、smoke 是否创建数据、页面步骤结果、故障窗口及未执行项。日志与截图只留必要定位字段,不包含真实密钥和业务正文。
分开写“教程构建通过”“日志单元测试通过”“镜像 smoke 通过”“远端 CI 通过”,不要压成一个含糊的“全部通过”。Docker 未启动、缺测试连接或没有远端运行时,保留对应的未执行说明。
拓展阅读从可运行到可上线还差什么?
当前仓库有检查、迁移、独立进程、探针、日志和受限导出,但尚无自动远程发布、数据库恢复演练、容量与告警保障。域名 / HTTPS、生产备份与恢复、公开账号恢复等需要具体方案和验证;S3 与 Sentry 的后续范围见第 32 章。不能把“具备本地验收手段”直接写成“生产运行已保障”。
10. 练习与答案#
练习 A:脚本通过,能证明退出后重新登录正常吗#
动手练习查看参考答案
参考答案:不能。当前调用 sign-up/email 后沿注册会话继续请求,没有登出与 sign-in/email 流程。要单独设计已有账号登录、退出和 Cookie 生命周期的验证,浏览器属性还需真实浏览器检查。
练习 B:如何补强下载验收而不堆重复断言#
动手练习查看参考答案
参考答案:按目标风险补证据:核对附件响应头和有效 CSV 解析结果;让其他客户 / 团队也有匹配筛选的数据;用另一个真实会话请求同一任务文件并检查拒绝。分别验证文件结构、内容范围与所有权,不只再检查一次包含标题。
练习 C:关闭 Worker 后运行 smoke 失败,是否说明重启恢复也失败#
动手练习查看参考答案
参考答案:不能。它只说明这次等待未见完成或遇到错误。恢复实验需要保留原 taskId,重启后查同一任务、结果与日志;重新运行 smoke 会创建新账号、新团队和新任务,不能替原任务的恢复作证。
第六阶段链路:独立检查环境验证代码 → 镜像生成与迁移 → Web 会话和输入授权 → 数据库存待办 → Redis / Worker 执行 → 结果落库 → HTTP 鉴权下载 → 页面与故障补证据 → 据实记录可用范围。