TTeamDesk 学习手册
本章目录 展开导航 ↓

27 · 任务验收与排错:把等待、失败和跳过查成证据#

小陈说:“导出一直等,提醒却显示完成,但我没收到通知。”这句话可能同时描述两件正常事:导出没有 Worker,旧提醒被取消后正常跳过;也可能确有投递故障。第五阶段的验收,要把每句话落到任务 ID、状态、时间、权限与结果上。

本章分开执行基础规则、真实数据库业务、真实 Redis 调度、正式 Worker 消费恢复和页面故事。它们不能互相替代;下面给的是可执行步骤与预期证据,实际通过与否以你运行的输出为准。

1. 先给问题建立一张记录卡#

遇到异常先记 taskId、kind、teamId、当前账号、run、attempts、status、runAt、updatedAt、摘要 / 错误。不要把连接串、Cookie 或整份 CSV 贴到日志里。

现象 第一份证据 接下来区分什么
提交失败、没有 ID Network 中 tRPC 错误 未登录、非成员、限流、数据库不可用
WAITING runAt、dispatchedAt、Worker 投递日志 未来延迟、尚未投递、同轮重试
RUNNING 无推进 updatedAt、executionId、taskId/run 日志 正在读取、执行卡住、恢复扫描未完成
FAILED lastError、attempts、run 明确拒绝还是次数耗尽,重试是否有意义
COMPLETED 无通知 resultSummary、工单 revision、通知接收人 旧提醒跳过、当前负责人不是我、无接收者
COMPLETED 无法下载 ownerId、当前成员、scopeCreatorId、expiresAt 身份、范围、期限或正文变化

任务健康查询读取 Redis 和近期心跳,业务列表读取 PostgreSQL。绿色健康标记不是某个任务正确的断言;红色标记也不代表已受理账本消失。先确认自己在同一环境查同一 ID。

2. 第一层:无需数据库的三个基础规则#

在项目根目录运行:

bash
pnpm exec vitest run tests/tasks.test.ts --reporter=verbose

tasks.test.ts 的三个 it 验证:CSV 逗号 / 引号 / 换行结构;等号等公式开头与前置空白防护;固定 now 下优先级的 1/4/24/48 小时差值。

这些用例应实际 passed。它们不启动 Worker,不验证成员资格、数据库回滚、队列延迟,也不证明用 Excel 打开后的所有显示行为。固定日期减少时钟干扰,但不意味着已经实现工作日历。

3. 第二层:准备独立数据库再测试业务#

先启动 Docker 引擎,在项目根目录逐条执行:

bash
pnpm db:up
# 只在该学习测试库尚不存在时执行;存在则跳过,不删库重建
docker compose exec -T postgres createdb -U teamdesk teamdesk_stage5_test
DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_stage5_test pnpm db:migrate
pnpm db:generate

临时 DATABASE_URL 只用于迁移命令,不改 .env。生成 Client 不会建表;迁移必须指向测试实际使用的同一个库。不要将开发 Worker 指向这个测试库,测试文件会自己控制执行时刻。

bash
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_stage5_test \
pnpm exec vitest run tests/tasks.integration.test.ts --reporter=verbose

只运行此文件不需要 TEST_REDIS_URL。它连接真实 PostgreSQL,但 execute 直接调用 processTask,updateProgress 是可控函数;Redis 故障用例传的是会抛错的队列桩。这样可以明确在指定业务位置失败,不依赖网络故障碰运气。

4. 先读夹具,防止误把准备动作当业务保证#

打开 tasks.integration.test.ts,按 describe.skipIf → beforeAll → api / ticket / exportTask / execute → afterAll 阅读:

帮助函数 实际作用 读断言时的边界
api(userId) appRouter.createCaller 注入上下文 测服务授权,不经过 Cookie 与 HTTP
ticket() 客户通过服务创建工单 会附带创建默认提醒待办
exportTask() 直接在表里建立受控导出任务 绕开受理限流,不证明 createExport 全流程
execute(id, attemptsMade) 查当前 run,直接调用 processor 不经过 BullMQ 延迟和自动退避
afterAll 清理本轮团队、工单与账号并断连 不清全库;强杀可能留下夹具

部分测试通过直接改 dueAt、assigneeMembershipId 或成员角色准备边界状态。这样能短而稳定地测执行分支,但这些修改不是建议用户在正常业务库中操作的方法。

整个文件共享这一轮团队夹具;不要假设每个用例开始时只有一张工单或一个任务。精确检查时按本用例 ID、唯一标题或操作前数量定位。

5. 把实际断言映射到业务保证#

测试场景 当前真正检查的证据 不能顺带宣称
匿名与跨团队提交 UNAUTHORIZED / FORBIDDEN 浏览器登录 Cookie 传输已验证
客户导出 有 100 进度、公式前缀、无秘密标题、他人下载 NOT_FOUND 所有相同筛选下的归属攻击都覆盖
排队后降权 待执行被拒绝,已有文件下载 FORBIDDEN 排队用例本身证明全部并发窗口
移出团队 执行被拒绝 同名用例还调用了下载;它没有
过期文件 改 expiresAt 后下载 NOT_FOUND 定时清理已真实等过 24 小时
默认提醒和设置权限 创建后有一个任务,客户 / 外团队不能设置 通过失败注入证明所有创建关联写入回滚
提醒重复执行 通知 1 条、系统事件 1 条、已读更新 两个并发 Worker 的全部竞争均覆盖
提醒并发接手 E1 回滚后四项一致,E2 完成后四项一致;恢复清租约同样验证 真实 BullMQ stalled 接手全过程已测试
提醒事件后故障 三项业务写入回滚、任务 WAITING,重试只留一份结果 所有数据库失败点都已穷举
已解决后重新打开 旧任务无通知,重开后任务总数变 2 新任务到期后一定成功通知
手动重试 非 FAILED 拒绝、他人拒绝、旧 run 不执行、新 run=1 该旧 run 用例也证明同 run 接手
导出并发接手 两个暂停点,旧执行者无 100,新租约不变,B 的 CSV 最终保存 真实 Worker stalled 全过程已测试
导出读取中撤权 合法降权 / 移除后不发布,无正文、期限,下载拒绝 能撤回已下载副本
导出最终授权锁 权限连接真实等待数据库锁;提交后降权拒绝下载 任意绕过团队锁的直接改表也安全
导出结果写入故障 事务内 UPDATE 后抛错整笔回滚;提交后汇报失败保留结果 PostgreSQL 与 Redis 已是同一事务
205 行导出 有中间进度,标题在 CSV 中出现 205 次 5 MiB 边界与动态快照压力已测试

这是测试阅读练习,不是否定现有测试。明确证据边界,才能选出下一份有价值的测试,而不是重复已有 happy path。

6. 亲手跑一次中途故障与恢复#

在已准备好的独立库上聚焦:

bash
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_stage5_test \
pnpm exec vitest run tests/tasks.integration.test.ts \
-t '中途进度写入故障' --reporter=verbose

用例连续传 attemptsMade=0、1、2,updateProgress 每次抛错。它依次断言 WAITING、WAITING、FAILED,attempts 分别 1、2、3,resultCsv 始终 null;然后调用 retry,正常 execute,最终 COMPLETED。

这里预期业务调用 reject,而用例应 passed,因为拒绝与回查数据符合预期。其他 it 因名称过滤跳过是正常的;整组因没配置 TEST_DATABASE_URL 而 skipped 则表示数据库实验没有执行。

投递失败用例仅断言任务仍 WAITING,不能代替提醒事务回滚证据。现在还有专门的真实数据库故障注入:Prisma 查询扩展在事件 INSERT 完成后抛错,通知、version、事件都应撤销;任务回到 WAITING、executionId=null,第二次执行才提交一份结果。这里不用数据库触发器,也不修改生产代码来添加测试暂停接口。

继续运行并发场景:

bash
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_stage5_test \
pnpm exec vitest run tests/tasks.integration.test.ts \
-t '提醒并发接手|提醒写入事件后故障' --reporter=verbose

“提醒并发接手”有两条参数化用例。A 在真实事务内读到 E1 后等待;一条让 B 的认领真正提交 E2 后等待,另一条模拟恢复扫描清租约并置 WAITING。释放 A 后,先检查通知 0、事件 0、工单 version 不变,任务与接手后的整行快照一致。再释放 B 或重新执行,检查通知和事件各 1、事件版本与工单新版本一致、提醒意图版本未变、任务 COMPLETED / progress=100 / executionId=null / lastError=null。最后再执行一次也不追加结果。

这会发现旧实现中“末尾更新 count=0 却仍提交副作用”的问题;仅检查最终通知数会被唯一约束掩盖。同步关卡决定交错顺序,超时只负责防止测试无限等待;失败清理也必须放行并等待所有执行者。第 26 章 解释为什么失去提交资格要在事务内抛错、在事务外安静退出。这里直接调用 processor,不测试真实 stalled 检测、两分钟恢复时钟或 Redis 重投。

导出也要区分旧资格、当前授权和发布结果#

第 25 章 列出八条导出用例。运行:

bash
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_stage5_test \
pnpm exec vitest run tests/tasks.integration.test.ts \
-t '导出并发接手|导出读取中|导出旧执行者|导出最终写入后|导出最终授权|导出已提交' --reporter=verbose

两条接手用例检查 E2 已提交而尚未读取时,A 不改任务、不报 100;随后 B 才保存带“新快照”标记的文件。权限变化用例经过真正的成员服务;最终锁用例额外观察 PostgreSQL 的真实阻塞,证明成员降权无法插入授权与保存之间。旧权限错误用例让 A 读到客户身份后暂停、恢复为客服并让 B 接手,检验旧 TaskRejected 不再终止新执行。

提交失败要分前后:事务内结果 UPDATE 后抛错,CSV、摘要、期限和完成状态全回滚,任务回 WAITING;事务外报告 100 失败,已完成账本与文件保留。这个区分比“只断言最后 COMPLETED”更能发现半份结果或无效完成汇报。

7. 第三层:真实 Redis 调度单独验证#

开发 Compose 已把 Redis 暴露在本机 6380。运行:

bash
TEST_REDIS_URL=redis://127.0.0.1:6380 \
pnpm exec vitest run tests/queue.integration.test.ts --reporter=verbose

queue.integration.test.ts 创建随机 test-UUID 前缀和 test-tasks 队列,结束只 obliterate 本轮队列,不清空开发前缀。三个场景分别检查两次临时失败后成功并保留进度、三次失败后停止且可调用队列 job.retry、同 jobId 的 300ms 延迟任务重复投递只执行一次。

测试退避起点是 30ms,不是应用配置的 2000ms;延迟断言允许至少 250ms,不能据此声称验证了毫秒级精确调度。第二个场景调用的是 BullMQ job.retry,不是项目服务里 run+1 的手动重试,两种 API 的语义不同。

这个测试的处理函数是小型测试函数,没有调用本项目 processTask,也不连接 PostgreSQL。它证明真实队列行为,不能单独证明提醒权限或导出内容。

第三层补充:让正式 Worker 真正接手#

此前 queue.integration 的小型函数与 tasks.integration 的处理器直调各有边界。worker.integration.test.ts 把这两段接起来,复用 正式 Worker 工厂。两个测试连接缺一都会跳过整组:

bash
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_stage5_test \
TEST_REDIS_URL=redis://127.0.0.1:6380 \
pnpm exec vitest run tests/worker.integration.test.ts --reporter=verbose

它为每例创建 worker-test-UUID 队列前缀,在本轮团队里直接准备工单与任务。dispatcher 的测试查询扩展只额外限定本轮 teamId,避免其他套件的任务被投到本例队列;正式恢复条件保持不变。

用例 可控故障 同时检查的结果
导出 stalled 接手 A 停止续锁,B 真实检测后换租约,A 晚到读取错误 E2 中间态不变;队列 completed / 100,账本 COMPLETED、CSV / 摘要 / 期限正确
提醒 stalled 接手 同样交错,A 在提醒事务中暂停 E2 不被改写;通知、系统事件各 1,工单 version 仅加 1,任务完成
failed 回调迟到 A 的失败状态写入故障、队列 failed,单个测试 job 丢失后恢复补投 旧回调不能清 E2;新执行者完成且正文与摘要正确
真实自动重试 首次读取故障 failed 日志 exhausted=false,下一次成功,数据库与队列尝试计数均为 2
永久拒绝 无效筛选 两边 FAILED,只尝试 1 次;数据库保留准确业务错误
stalled 上限耗尽 maxStalledCount=0,等待真实锁过期 队列先 failed,账本经 RUNNING → WAITING → FAILED,旧执行者不改终态

事件决定何时继续,Promise 关卡决定谁暂停;没有删除 Redis 锁或手动 emit 事件。测试在事务读取、失败状态写入等边界注入错误,这是为了稳定命中特定故障窗口,并不是伪造数据库或队列返回值。迟到回调的数据库扩展只延迟旧实现可能发出的 SQL;恢复旧无租约 UPDATE 后,E2 会实际被改成 FAILED,断言因此失败。

测试最后放行关卡、等待 Worker 关闭,再关 QueueEvents,只 obliterate 本例随机前缀,清理本轮数据库团队与账号。不连接开发队列,不运行 FLUSHDB。锁自然过期真实等待约一秒;两分钟恢复阈值用调旧本任务 updatedAt 加速,不能据此声称测过真实两分钟计时。

数据库与 Redis 准备好后,可一次运行本阶段四个自动测试文件:

bash
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_stage5_test \
TEST_REDIS_URL=redis://127.0.0.1:6380 \
pnpm exec vitest run tests/tasks.test.ts tests/tasks.integration.test.ts tests/queue.integration.test.ts tests/worker.integration.test.ts --reporter=verbose

进程级的心跳、SIGTERM、退出期限和重启接手,另见 第 30 章,运行 tests/worker-process.integration.test.ts。该文件已加入 pnpm test:integration,CI 的 pnpm test 也会自动发现;它要求两个测试连接,缺一则整组跳过。

8. 第四层:用页面补上真实进程之间的接棒#

在独立学习团队、常规开发库中进行,不与上面的自动测试库混用:

  1. 停止你自己的开发 Worker,提交一份带唯一筛选的导出。记任务 ID,确认原账本 WAITING;界面健康提示可能要等心跳过期和下一次查询。
  2. 用同一开发配置启动 pnpm worker,按 taskId / run 找任务日志,观察原任务完成,下载核对范围与内容;不要靠再提交一份完成的任务替代恢复证据。
  3. 新工单设一分钟提醒,分配给小陈;到期后核对通知归属、系统事件和工单版本。页面只有站内通知,不会出现邮件或系统弹窗。
  4. 另一新工单设后取消,核对旧任务跳过且没有通知;重新设置需携带最新版本。再走解决 / 重新打开,核对新时限与新待办。
  5. 另一账号访问导出下载地址应被拒绝;切回所有者核对有效结果。再结合自动测试中的降权、过期与重试断言完成记录。

第 1~2 步证明真实 Web、数据库、Worker、Redis 接棒,不故意破坏 Redis。新 Worker 集成用例已覆盖自然锁过期接手、stalled 上限耗尽和单个 job 记录丢失后的补投;真实网络断线、Redis 整库丢失、完整 Web / 容器中断仍是其他故障窗口,不能将这六条测试扩大为全部端到端覆盖。若继续扩展,应使用一次性隔离环境,不通过删除开发队列验证。

9. 常见失败按哪一层排查#

输出 / 现象 下一步
Docker socket 不存在 启动 Docker 引擎,再检查 docker compose ps
数据库拒绝连接 / 缺表 核对 5433、库名、迁移命令的临时 URL
测试全 skipped 检查 TEST_DATABASE_URL / TEST_REDIS_URL 是否给到该命令
同任务重复通知 按 ticketId+revision+userId 查行,再查事件与任务完成事务
等待超过两分钟 先看是否 RUNNING 过期,再看扫描是否成功走到恢复末尾
第一次失败立即终止 检查是否 TaskRejected,不能一律判断自动重试失效
下载报错但账本完成 核对当前所有者资格、expiresAt、正文,而不篡改 status

记录“做了什么、读到什么、是否符合预期、还有哪层没验证”。不要用测试通过截图代替输出中被跳过的真实依赖测试。

拓展阅读怎样选择下一份最有价值的测试?

围绕失败窗口补证据:同筛选不同客户的范围夹具、客户升权后原始 scope 保持、更多提醒失败点、5 MiB 字节上限、入队成功而确认失败后的补投。每次只让一个故障点可控,检查业务结果而不复刻实现内部调用顺序。

10. 练习与答案#

练习 A:四个自动测试文件全 passed,能宣布完整 Worker 上线验收完成吗#

动手练习查看参考答案

参考答案:不能。新增 Worker 集成已经复用正式处理器与事件监听器,证明消费、租约和特定恢复窗口;它仍不启动完整 Web;第 30 章另有正式生命周期的真实子进程测试,覆盖心跳与信号退出,容器守护仍需单独验收。还需页面接棒及部署验证,不能替代第六阶段镜像、迁移、健康与升级验收。

练习 B:WAITING、runAt 在明天、健康正常,需要点重试吗#

动手练习查看参考答案

参考答案:不需要。延迟任务应等待到期,且 retry 只接受 FAILED。先判断任务意图与时间,不能为让状态变绿手工改表。

练习 C:如何证明客户边界,而不是碰巧没有泄漏#

动手练习查看参考答案

参考答案:让其他客户和外团队的数据也匹配同一筛选,再断言只有当前客户数据被导出;同时测试他人下载被拒绝。数据集合边界和文件所有权是两份证据,缺一不可。

第五阶段最终链路:前端提交筛选 / 时限 → 接口校验身份、成员与版本 → 数据库原子保存意图与待办 → dispatcher 投递 ID 和轮次 → Worker 重查权限、提醒版本与执行租约 → 业务结果落库 → 页面轮询及授权下载。 每一段都应有对应证据,才能解释任务为什么等待、为什么失败、为什么正常跳过。

把知识变成自己的理解

完成本章练习,再继续下一站。

TEAMDESK HANDBOOK · 从一张工单,走进全栈开发

搜索教程

输入关键词,搜索全部章节。

阅读设置

外观
正文字号

从一张工单出发,读懂数据如何流转。

设置与进度仅保存在当前浏览器。