18 · 第三阶段验收:把协作、草稿和链接串成闭环#
第三阶段的完成标准,不只是页面上多了一个负责人和一个回复框,而是:责任能正确分配,沟通能可靠保存,未提交输入能恢复,浏览条件能分享,而且第二阶段的权限边界仍然有效。
以下步骤供你在本地执行。阅读站的构建、搜索和链接检查,与业务数据库集成验证是两种不同的检查,不能互相代替。
1. 准备测试环境和角色#
沿用 第二阶段验收 的独立账号与浏览器会话:甲团队管理员 A、客服 C、客户 D 和 E,以及乙团队管理员 B。普通标签页通常共享 Cookie,不能当成不同身份。
在甲创建几张未关闭工单,至少让 D、E 各有一张,本章主线选择 D 的工单。C 若目前只属于甲,可另建自己的练习团队,供最后的切换实验使用。先熟悉当前团队与用户,再开始下面的协作故事。使用自己的测试数据,避免误操作真实工单。
2. 用一条故事验收四项能力#
| 步骤 | 操作 | 预期与观察点 |
|---|---|---|
| 1 | C 筛选未分配并打开 D 的工单 | URL 有对应条件,数据只在甲范围 |
| 2 | 点击分配给我 | 仅改变待保存选择,不应立即假装写库 |
| 3 | 保存分配 | 详情与列表刷新,版本推进 |
| 4 | C 输入回复,切到别处再返回 | 当前工单草稿仍在 |
| 5 | 刷新同一标签页 | 挂载后恢复草稿,输入恢复后可用 |
| 6 | 发送回复 | 数据库保存一条,成功后清草稿,回复页回 1 |
| 7 | D 在自己的工单补充情况 | 作者身份与角色正确,换行按文本展示 |
| 8 | 在列表调整搜索、筛选、页码 | 可复制、刷新、前进后退恢复条件 |
| 9 | 从已有列表行进详情,再返回 | 保留那次列表查询参数 |
| 10 | 切换团队、再主动退出 | 不串用业务数据;退出清当前账号草稿 |
记住两个实现细节:创建成功后的导航只保留 team;回复分页不在 URL 中。不要把它们误报成工作台 URL 恢复机制失效。
3. 专门验证失败边界#
| 场景 | 预期 | 负责的边界 |
|---|---|---|
| 将客户 / 其他团队成员作为负责人 | 拒绝 | 服务重新验证候选资格 |
| 用旧 version 分配 | CONFLICT |
条件更新与版本 |
| 客户读或回复同团队他人工单 | NOT_FOUND |
资源归属 |
| 无资格团队请求回复 | FORBIDDEN |
成员关系 |
| 新编号给关闭工单发回复 | 拒绝 | 工单业务状态 |
| 同编号、同内容并发回复 | 同一记录,只落一条 | 锁、去重查询与唯一约束 |
| 同编号改内容 | CONFLICT |
发送意图不能被替换 |
| 被分配人降为客户或移出团队 | 自动清空负责人并递增版本 | 成员变更事务 |
page=1.5、超大 pageSize、非法枚举 |
parser 回落;HTTP 输入仍有 Zod 限制 | URL 与接口输入边界 |
故障演示中,普通“网络离线”可能让请求根本没到服务端,不能证明“数据库已提交但响应丢失”。幂等行为优先用可控的集成调用验证,避免靠碰巧断网得出结论。
4. 无需数据库的测试先验证什么#
tests/drafts.test.ts 同时包含草稿和 URL parser 的测试:
pnpm exec vitest run tests/drafts.test.ts草稿用注入的内存 storage 验证账号、团队、工单隔离与恢复;还验证形状损坏的存储不会被恢复。parser 验证非法数字与枚举返回 null,以及合法参数能解析。
这不是完整的 React hydration 测试,也没有真正驱动浏览器后退、nuqs history 或隐私模式存储异常。标题里写了“回落默认”不代表断言已经直接覆盖所有 hook 行为;要看测试具体检查了什么。
5. 真实数据库测试覆盖哪些链路#
tests/collaboration.integration.test.ts 通过 appRouter.createCaller 执行真实 Prisma 操作,覆盖分配资格、筛选隔离、回复权限、并发去重、关闭限制、成员变更联动。
先在开发 Compose 中准备独立测试库,已有数据库时跳过创建:
pnpm db:up
# 仅首次创建;已有 teamdesk_test 时跳过下一行
docker compose exec -T postgres createdb -U teamdesk teamdesk_test
DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_test pnpm db:migrate
pnpm db:generate
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_test \
pnpm exec vitest run tests/collaboration.integration.test.ts当前创建操作会在数据库保存提醒任务,但这组协作测试不要求 Redis 在线。不要让开发 Worker 连接测试库抢先执行任务。
测试只清理自己创建的数据;仍要明确指向测试环境。没有设置 TEST_DATABASE_URL 时,该文件会跳过,不能把“执行命令未报错”当成数据库场景已经通过。
createCaller 注入明确用户上下文,测试 tRPC、服务与数据库,不经过浏览器、Cookie 传输和页面组件。第二阶段的认证验证与本阶段的 UI 验收仍各有用途。
6. 当前没有单独验证的细节怎样记录#
第三阶段的协作测试验证同编号并发只落一条;第四阶段的 记录测试 还执行了无变化分配和同编号回复重试,检查历史与版本序列,见 第 21 章。因此不能笼统说“当前仓库还没测无变化分配”。
仍可加强的窗口是:“先成功回复 → 关闭 → 相同编号重试”“浏览器刷新后复用同一 requestId”“前进后退后控件与查询输入同步”。这些行为可从相关源码推导,但已有内存 store、parser 和服务调用测试没有自动组成真实浏览器链路。写预期时从业务合同出发,不直接调用实现的规则生成答案。
本次教程只说明这些边界,没有借写教程顺便修改业务实现或宣称新增了这些测试。
7. 按四种状态定位问题#
| 现象 | 先检查 | 可能原因 |
|---|---|---|
| 下拉显示我,数据库仍未分配 | 是否点击保存、请求结果 | 选择 state 与已保存数据不同 |
| 保存分配后冲突 | 最新详情 version | 其他回复、分配或成员联动推进版本 |
| 同样文字出现两次 | 两次请求编号是否相同 | 编辑或重建编号形成新意图 |
| 回复已发但输入还在 | mutation 成功回调、当前草稿 key | 结果未被客户端确认或 key 不一致 |
| 刷新后短暂不能编辑 | hydrated 与恢复流程 | 防止新输入被旧草稿覆盖 |
| 换工单出现另一份草稿 | user/team/ticket key | 客户端条目范围错误 |
| 退出后仍能看到旧私有内容 | Query 清理与完整导航 | 客户端状态未清干净 |
| URL 变了却没有全页加载 | shallow 与 Query 请求 | 当前按客户端筛选更新是正常设计 |
| 后退后搜索框文字未恢复 | q、Input.Search 的 key/defaultValue | 控件显示与 URL 状态要同步 |
| 搜索描述找不到工单 | ticketWhere 的 title 条件 | 当前只搜索标题 |
浏览器 Network 看输入与结果,React / store 状态看未提交内容,Prisma Studio 看最终持久数据。不要为了让页面“看起来更新”而同时手工维护多份工单数组,掩盖事实来源不一致。
8. 三道练习与参考答案#
练习 A:分配给我为什么有两个意思#
解释详情按钮“分配给我”和列表 assignment=mine 的区别。
动手练习查看参考答案
参考答案:详情按钮准备将负责人改为当前人的 Membership,保存后才写数据库;列表参数只是请求“当前负责人是我”的可见工单。修改业务数据与改变读取条件不能混为一个动作。
练习 B:把回复存 Zustand 后直接显示,算发送成功吗#
用户输入已经在 sessionStorage 中,页面也能展示,能否不请求服务端就提示已发送?
动手练习查看参考答案
参考答案:不能。这仍是本地草稿,其他成员看不到,服务端也没有验证归属和作者、保存回复或更新工单。应由接口事务成功返回后再清草稿并展示成功结果。
练习 C:URL 改成别人的 team 能绕过权限吗#
链接格式合法、nuqs 也能解析,是否意味着能读该团队工单?
动手练习查看参考答案
参考答案:不能。URL parser 处理字符串形状和缺省,服务端仍根据会话查询成员关系,再加团队及客户归属条件。链接不是授权凭证,客户端状态管理不负责授予服务端权限。
9. 第三阶段完成标准#
你应能解释并验证:用户选择责任人 → 服务检查团队资格和版本 → 数据库保存分配 → 双方回复经授权与去重落库 → 未发送内容留在独立草稿 → 筛选条件写到 URL → Query 按当前条件读取并刷新。
当前不是即时聊天、富文本或离线自动发送系统;草稿没有跨设备同步,链接也不固定数据快照。接着进入 第 19 章:操作记录,学习业务历史与事务验证,再进入后台提醒、导出和部署。