facc04aa65
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交 (5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。 一、docs 结构整改(整改 #14) 根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入, 随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录 树并存于 docs/,共 791 文件、双分类体系冲突。 修复动作: - b2 同名异主题文件改名迁移保全 9 个 - C 类 39 个孤立文件按主题正确归类 - A/B1 类 222 个重复文件删除(新结构已有内容副本) - 9 个旧独有空目录删除 - 270 处内部引用按 verified 映射改写 - 整改记录 #14 登记于 04-运维文档/部署运维 结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。 残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。 二、compose 双目录对齐(消除踩坑 A) - docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist 改为 src/frontend-*/dist(h5 / agent / admin / terminal) - docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/ - 效果:本地 docker compose up 不再把根目录 stale dist 挂回, 与线上一致,分叉隐患消除(已 docker compose config 校验通过) 防复发铁律: - 重构须提交;仓库修复须 git stash -u 或先 commit - 新结构须 git add 并提交,避免再次 untracked 复活 - H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
31 KiB
31 KiB
测试用例 - 选项选择持久化
REQ 编号: REQ-通用-005 版本: v1.0 日期: 2026-07-29 作者: 严过关(QA) 状态: ✅ 编写完成(35 用例,待工程师静态审查 + AST 后给出最终结论) 关联文档:
- PRD:
docs/01-产品文档/00-产品规划/PRD-REQ-通用-005-选项选择持久化-v1.0.md- 技术方案:
docs/02-技术文档/技术架构/技术方案-REQ-通用-005-选项选择持久化-v1.0.md- 代码真相:
src/backend/app/api/ws.py+src/backend/app/api/messages.py+src/backend/app/utils/sensitive.py+src/backend/app/tasks/h5_ai_task.py+src/backend/app/services/ai_service.py+src/frontend-agent/src/components/chat/MessageBubble.vue+src/frontend-h5/src/stores/conversation.ts
一、用例汇总
| 类别 | 用例数 | 覆盖重点 | 关联 PRD |
|---|---|---|---|
| A. 功能验收(AC-01~AC-10) | 10 | PRD §5 验收标准 | §5 |
| B. 异常场景 | 8 | 历史 UUID 复用 / 多 worker 并发 / Dify 超时 / WS 失败 / 缺字段降级 / 跨卡同名 / 多题最新 / 不更新旧行 | §6 + 工程师报告 §八 |
| C. 敏感词边界 | 5 | 16 位 / 18 位 / 短值 / 中文边界 / 不动业务键 | §4.8 |
| D. H5 UUID 生命周期 | 3 | 首次生成 / 重试复用 / 主动重选 | §4.4 + §4.5 |
| E. 性能压测 | 2 | 20 次并发 / 1000 条 latest 计算 | 技术方案 §9 |
| F. Dify 集成 | 3 | 5 字段注入 / 普通文本无 inputs / 代理 fallback 不拼原值 | §4.6 + 技术方案 §5 |
| G. 转人工快照 | 2 | 每题最新 / 双处注入(tags + conversation_updated.data) | §4.7 + 技术方案 §6 |
| H. 回归兼容 | 2 | 普通文本链路 / ai_structured + recommend_event | 技术方案 §9 回归 |
| 总计 | 35 | - | - |
优先级分布:P0 = 23 条 / P1 = 9 条 / P2 = 3 条
二、功能测试用例
2.A 功能验收(直接对应 PRD §5 AC-01~AC-10)
TC-通用-005-001
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-001 |
| 用例标题 | 首次点选后 messages 表新增 1 行 option_select,6 字段完整 |
| 关联 AC | AC-01 |
| 前置条件 | 1) H5 已建立 WS 连接;2) AI 已发出 ai_structured 含 options;3) messages 表初始无 option_select |
| 测试步骤 | 1. 员工点击"网络中断"选项 2. H5 发送 6 字段 option_select3. 等待 200ms |
| 预期结果 | 1) messages 表新增 1 行:msg_type="option_select"、sender_type="employee";2) extra_data 字段含 option_value/selected_from_message_id/client_msg_id/question_id/option_id + option_label_original;3) content 为原始 label;4) GET /conversations/{id}/messages 返回该行 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_handle_option_select_persists_first_message |
| 静态审查锚点 | ws.py:374-385(构造 Message) + ws.py:365-373(extra_data 5+1 字段) |
TC-通用-005-002
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-002 |
| 用例标题 | 同一选项重选两次,形成两行 option_select |
| 关联 AC | AC-02 |
| 前置条件 | 1) 已完成 TC-001 的首次点选;2) 5 秒后再点同选项 |
| 测试步骤 | 1. 员工再次点击同选项(新 UUID) 2. 等待 200ms |
| 预期结果 | 1) messages 表累计 2 行 option_select,message_id 互不相同;2) 第一行不被覆盖;3) REST 历史按时间正序返回两行 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_two_reselects_persist_two_rows |
| 静态审查锚点 | ws.py:386(db.add) + 不存在 UPDATE 旧行的 SQL |
TC-通用-005-003
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-003 |
| 用例标题 | 坐席在线时收到标准 new_message 事件 |
| 关联 AC | AC-03 |
| 前置条件 | 1) 坐席 WebSocket 已连接;2) H5 发起点选 |
| 测试步骤 | 1. 抓取坐席 WS 收到的消息;2. 校验 payload |
| 预期结果 | 1) 收到 type=="new_message";2) data.msg_type=="option_select";3) data.message_id 与 DB 行一致;4) data.content 为 mask 后文本 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_ws_broadcasts_new_message_type |
| 静态审查锚点 | ws.py:407-421(broadcast payload) |
TC-通用-005-004
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-004 |
| 用例标题 | 坐席断线重连后,REST 历史可见相同 message_id |
| 关联 AC | AC-03 |
| 前置条件 | 1) 已完成 TC-001 的首次点选;2) 坐席 WS 断开;3) 重新调用 GET 历史接口 |
| 测试步骤 | 1. 调用 GET /api/conversations/{id}/messages;2. 在返回列表中查找该 option_select |
| 预期结果 | 1) 找到 1 条 msg_type=="option_select";2) message_id 与 TC-001 落库 ID 完全一致;3) content 为 mask 后文本 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_rest_history_returns_same_message_id |
| 静态审查锚点 | messages.py:158-173(REST mask 分支) |
TC-通用-005-005
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-005 |
| 用例标题 | 坐席 MessageBubble 渲染"✓ 选项"灰色徽标 |
| 关联 AC | AC-04 |
| 前置条件 | 1) 坐席前端加载会话;2) REST 返回 1 条 option_select |
| 测试步骤 | 1. 渲染 MessageBubble;2. 检查 DOM |
| 预期结果 | 1) 渲染 option-select-badge 节点;2) 文案为 ✓ {content};3) 默认不带 --latest 类(灰色) |
| 优先级 | P0 |
| 自动化建议 | ✅ MessageBubble.spec.ts → option_select renders badge |
| 静态审查锚点 | MessageBubble.vue:84-94(option_select 模板分支) |
TC-通用-005-006
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-006 |
| 用例标题 | 同 question_id 仅最新一条高亮,旧记录仍可见 |
| 关联 AC | AC-04 |
| 前置条件 | 1) 同一 question_id 有 2 条 option_select(不同 message_id) |
| 测试步骤 | 1. 渲染该 question 的所有 option_select 徽标;2. 检查 --latest 类归属 |
| 预期结果 | 1) 仅 1 个徽标带 option-select-badge--latest 与"最新"标签;2) 旧徽标仍显示但无高亮;3) 排序稳定(按 created_at + id 倒序) |
| 优先级 | P0 |
| 自动化建议 | ✅ MessageBubble.spec.ts → only latest highlighted |
| 静态审查锚点 | MessageBubble.vue:477-483(isLatestSelection) + 386-403(latestSelectionPerQuestion) |
TC-通用-005-007
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-007 |
| 用例标题 | H5 首次点选生成合法 UUID v4 |
| 关联 AC | AC-05 |
| 前置条件 | 1) H5 加载完成;2) pendingOptionSelect 为 null |
| 测试步骤 | 1. 员工首次点击选项;2. 抓取 WS 发送的 data.client_msg_id |
| 预期结果 | 1) client_msg_id 符合 UUID v4 格式(8-4-4-4-12,第 13 位为 4);2) 6 字段全部存在;3) selected_from_message_id 与 AI 消息 ID 一致 |
| 优先级 | P0 |
| 自动化建议 | ✅ conversation.spec.ts → first click generates UUID |
| 静态审查锚点 | conversation.ts:1567(safeRandomUUID 调用) + 1571-1579(6 字段 payload) |
TC-通用-005-008
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-008 |
| 用例标题 | 5 秒内同 client_msg_id 重发 3 次,仅落库/广播/Dify 各 1 次 |
| 关联 AC | AC-06 |
| 前置条件 | 1) H5 已首次点选;2) WS 偶发重连 |
| 测试步骤 | 1. 在 5 秒窗口内用同一 client_msg_id 重发 3 次;2. 统计落库次数、广播次数、Dify 调用次数 |
| 预期结果 | 1) messages 表仅 +1 行;2) ws_manager.broadcast 仅 1 次;3) process_h5_ai_reply 仅 1 次;4) 后续 2 次返回首次 message 不报错 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_idempotency_three_repeats |
| 静态审查锚点 | ws.py:316-336(5 秒窗口幂等查询 + return) + ws.py:300-313(advisory lock) |
TC-通用-005-009
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-009 |
| 用例标题 | Dify 收到 feedback_type=option_select 及题目/选项字段 |
| 关联 AC | AC-07 |
| 前置条件 | 1) H5 发起点选;2) Dify Workflow 已声明 5 个 input variables |
| 测试步骤 | 1. mock 拦截 Dify /v1/chat-messages 请求;2. 解析 payload.inputs |
| 预期结果 | 1) payload.inputs.feedback_type=="option_select";2) 字段含 question_id、option_id、option_value、option_label(mask 后);3) query 字段为 option_label 原值;4) 不影响现有 user/conversation_id |
| 优先级 | P0 |
| 自动化建议 | ✅ test_dify_inputs_contains_feedback_context |
| 静态审查锚点 | ws.py:433-439(feedback_context dict) + ai_service.py:279(inputs 透传) |
TC-通用-005-010
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-010 |
| 用例标题 | 员工选择后立即转人工,坐席上下文含各 question_id 最新选择 |
| 关联 AC | AC-08 |
| 前置条件 | 1) 员工已对 2 个不同 question_id 各选 1 次;2) Dify 返回 diagnosis_stage=escalating |
| 测试步骤 | 1. 等待 Dify 触发转人工;2. 坐席 WS 抓取 conversation_updated;3. 校验 data.selected_options |
| 预期结果 | 1) conversation_updated.data.selected_options 是 list;2) 每项含 question_id/option_id/label(message_id/selected_at);3) 每题仅 1 条;4) label 为 mask 后文本;5) conversation.tags.selected_options 同步写入 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_snapshot_each_question_latest |
| 静态审查锚点 | h5_ai_task.py:253-318(_build_selected_options_snapshot) + h5_ai_task.py:620-628(tags 注入) + h5_ai_task.py:743-755(conversation_updated.data 注入) |
TC-通用-005-011
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-011 |
| 用例标题 | 选项含账号/身份证时,中间四位为 ****,DB 原值不变 |
| 关联 AC | AC-09 |
| 前置条件 | 1) 选项 label 含 16 位数字串 |
| 测试步骤 | 1. H5 点选;2. 抓 WS 广播、REST 历史、Dify inputs、转人工快照;3. 直查 DB |
| 预期结果 | 1) WS/REST/Dify/快照中均为 前4****后N;2) DB messages.content 为原值;3) question_id/option_id/client_msg_id 不被 mask |
| 优先级 | P0 |
| 自动化建议 | ✅ test_mask_preserves_db_value |
| 静态审查锚点 | sensitive.py:38-66(mask_sensitive_text) + ws.py:397,415(mask 应用) + messages.py:165(mask 应用) |
TC-通用-005-012
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-012 |
| 用例标题 | 全链路:AI 发选项→员工点选→坐席实时 ✓→坐席重连→REST 历史仍可见,全程无重复记录 |
| 关联 AC | AC-10 |
| 前置条件 | 1) AI 已发 ai_structured;2) 坐席 WS 已连接 |
| 测试步骤 | 1. H5 点选;2. 坐席收到 new_message;3. 坐席 WS 断开 10s 后重连;4. 调用 REST 历史 |
| 预期结果 | 1) 实时收到 type=new_message,message_id=X;2) REST 返回含相同 message_id=X 的 option_select;3) DB 中该 message_id 仅 1 行;4) 整个流程无重复 INSERT、无重复广播 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_e2e_no_duplicate_across_ws_and_rest |
| 静态审查锚点 | ws.py:407(广播)+ messages.py:163(REST mask 分支) + ws.py:316-336(去重) |
2.B 异常场景
TC-通用-005-013
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-013 |
| 用例标题 | 历史 client_msg_id 复用(>5s)应被拒绝并不新增行 |
| 关联 AC | PRD §6 撤回/重选 + 工程师报告 §八 偏离项 #2 |
| 前置条件 | 1) 已完成首次点选 client_msg_id=A 已落库;2) 时间流逝 >5s |
| 测试步骤 | 1. 员工用同 client_msg_id=A 再次发起(模拟客户端异常复用);2. 等待 200ms |
| 预期结果 | 1) messages 表行数不变;2) 服务端日志告警"拒绝历史 client_msg_id 复用";3) 不广播、不调 Dify |
| 优先级 | P0 |
| 自动化建议 | ✅ test_historical_uuid_reuse_rejected |
| 静态审查锚点 | ws.py:339-356(hist_stmt 检查) |
TC-通用-005-014
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-014 |
| 用例标题 | 多 worker 并发同 UUID 20 次,仅 1 次落库/广播/Dify |
| 关联 AC | PRD §8 + 技术方案 §3.2 |
| 前置条件 | 1) 启动 ≥2 个 worker 模拟;2) 同一 client_msg_id |
| 测试步骤 | 1. 20 个协程同时调用 _handle_option_select;2. 完成后统计 |
| 预期结果 | 1) DB 仅 1 行;2) 广播 1 次;3) Dify 任务 1 次;4) 其余 19 次命中 5 秒窗口 return |
| 优先级 | P0 |
| 自动化建议 | ✅ test_concurrent_20_workers_single_uuid |
| 静态审查锚点 | ws.py:300-313(pg_advisory_xact_lock) |
TC-通用-005-015
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-015 |
| 用例标题 | Dify 30 秒超时,但 option_select 消息已落库,不反向删除 |
| 关联 AC | 工程师报告 §八 偏离项 #2/3 |
| 前置条件 | 1) H5 已点选;2) mock Dify 30 秒不返回 |
| 测试步骤 | 1. 等待 Dify 超时;2. 走关键词降级;3. 检查 messages 表 |
| 预期结果 | 1) messages 表 option_select 行保留;2) AI 回复走关键词降级(不影响 option_select 持久化);3) 后续 REST 仍可见该行 |
| 优先级 | P1 |
| 自动化建议 | ✅ test_dify_timeout_does_not_delete_option_select |
| 静态审查锚点 | h5_ai_task.py:1613-1639(超时分支) + ws.py:426-441(异步任务不阻塞 commit) |
TC-通用-005-016
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-016 |
| 用例标题 | WS 广播失败(ws_manager.broadcast 抛异常),消息已落库且可由 REST 重连读取 |
| 关联 AC | PRD §8 风险"广播失败" |
| 前置条件 | 1) mock ws_manager.broadcast 抛 RuntimeError |
| 测试步骤 | 1. H5 点选;2. 等待 200ms;3. 坐席调用 REST 历史 |
| 预期结果 | 1) messages 表有 option_select 行;2) WS 广播异常被吞掉(仅 warning);3) REST 历史可读 |
| 优先级 | P1 |
| 自动化建议 | ✅ test_ws_broadcast_failure_graceful |
| 静态审查锚点 | ws.py:422-423(try/except ws_err) |
TC-通用-005-017
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-017 |
| 用例标题 | 缺字段(question_id/option_id/sourceMessageId)降级为普通文本 |
| 关联 AC | 工程师报告 §八 偏离项 #2 |
| 前置条件 | 1) H5 sendOptionSelect 传入空 questionId="" |
| 测试步骤 | 1. 观察 H5 行为;2. 后端落库消息类型 |
| 预期结果 | 1) H5 走 sendMessage({content: optionLabel, msg_type: 'text'}) 降级;2) messages 新增 1 行 msg_type=text;3) 不产生 option_select 行 |
| 优先级 | P1 |
| 自动化建议 | ✅ conversation.spec.ts → missing question_id falls back to text |
| 静态审查锚点 | conversation.ts:1538-1544(降级分支) |
TC-通用-005-018
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-018 |
| 用例标题 | 跨卡同名 label 不串扰(依靠 question_id+option_id 归属) |
| 关联 AC | PRD §6 跨卡归属 |
| 前置条件 | 1) 会话有 2 张不同 ai_structured(question_id=A 与 question_id=B),均含 label="是" |
| 测试步骤 | 1. H5 在 A 卡选"是"(option_id=A_yes);2. H5 在 B 卡选"是"(option_id=B_yes);3. 落库 2 行 |
| 预期结果 | 1) 2 行均落库;2) latestSelectionPerQuestion 中 A→{label: "是", messageId: A 行 id},B→{label: "是", messageId: B 行 id};3) 不会互相覆盖 |
| 优先级 | P1 |
| 自动化建议 | ✅ MessageBubble.spec.ts → cross-card same label no interference |
| 静态审查锚点 | MessageBubble.vue:387-394(按 question_id 分组) |
TC-通用-005-019
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-019 |
| 用例标题 | 多 question_id 各自多次重选,最新选择仅高亮最新一条 |
| 关联 AC | PRD §6 + AC-04 |
| 前置条件 | 1) question_id=A 选了 3 次(A1/A2/A3);question_id=B 选了 2 次(B1/B2) |
| 测试步骤 | 1. 渲染所有 option_select;2. 检查 --latest 类归属 |
| 预期结果 | 1) A3 与 B2 各带 --latest;2) A1/A2/B1 无高亮;3) 排序按 (created_at desc, id desc) |
| 优先级 | P0 |
| 自动化建议 | ✅ MessageBubble.spec.ts → multi-question latest only |
| 静态审查锚点 | MessageBubble.vue:392-403(latest map 更新条件) |
TC-通用-005-020
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-020 |
| 用例标题 | 重选不更新旧行(数据库与 WS 均为追加语义) |
| 关联 AC | PRD §6 撤回/重选 + §7 非目标 #2 |
| 前置条件 | 1) 第一条 option_select 已落库(行 1) |
| 测试步骤 | 1. SQL:检查行 1 的 created_at、content、extra_data;2. 员工重选(行 2) |
| 预期结果 | 1) 行 1 内容与时间不变;2) 行 2 是新 INSERT;3) 没有 UPDATE 语句 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_reselect_never_updates_old_row(检查 SQL 日志无 UPDATE) |
| 静态审查锚点 | ws.py:374-385(仅 db.add + flush) + 全文无 Message.update |
2.C 敏感词边界
TC-通用-005-021
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-021 |
| 用例标题 | 16 位连续数字:保留前 4 + **** + 后 8 |
| 关联 AC | AC-09 |
| 前置条件 | 1) 单元测试 mask_sensitive_text |
| 测试步骤 | 1. 调用 mask_sensitive_text("6222123456789012") |
| 预期结果 | 返回 "6222****56789012" |
| 优先级 | P0 |
| 自动化建议 | ✅ test_mask_16_digit |
| 静态审查锚点 | sensitive.py:28(_MASK_PATTERN_16) + 57(替换) |
TC-通用-005-022
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-022 |
| 用例标题 | 18 位身份证(数字串)按通用规则:前 4 + **** + 后 10 |
| 关联 AC | AC-09 |
| 前置条件 | 单元测试 |
| 测试步骤 | 1. mask_sensitive_text("11010119900101001X")(含字母 X)2. mask_sensitive_text("110101199001010011")(纯数字) |
| 预期结果 | 1) 含 X 时不匹配纯数字正则,前 14 位中数字部分应 mask(如 "1101****0101001X");2) 纯数字 18 位按 4+4+10 切分,前 4 + **** + 后 10 |
| 优先级 | P1 |
| 自动化建议 | ✅ test_mask_18_idcard |
| 静态审查锚点 | sensitive.py:32(_MASK_PATTERN_GENERIC) |
TC-通用-005-023
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-023 |
| 用例标题 | 短敏感值(<4 位数字)全部替换为 * |
| 关联 AC | AC-09 |
| 前置条件 | 单元测试 |
| 测试步骤 | 1. mask_sensitive_text("6222") 2. mask_sensitive_text("123") 3. mask_sensitive_text("9") |
| 预期结果 | 1) "****";2) "***";3) "*" |
| 优先级 | P1 |
| 自动化建议 | ✅ test_mask_short_value |
| 静态审查锚点 | sensitive.py:35(_MASK_PATTERN_SHORT) |
TC-通用-005-024
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-024 |
| 用例标题 | 中文边界:数字前后为中文应正确切分 |
| 关联 AC | AC-09 |
| 前置条件 | 单元测试 |
| 测试步骤 | 1. mask_sensitive_text("账号62221234567中文") 2. mask_sensitive_text("余额是1234元") |
| 预期结果 | 1) "账号6222****4567中文"(11 位按通用规则:4+4+3);2) "余额是****元"(4 位按短规则但通用先匹配为 4+4+0... 实际为 "余额是****元") |
| 优先级 | P1 |
| 自动化建议 | ✅ test_mask_chinese_boundary |
| 静态审查锚点 | sensitive.py:32(前后 (?<!\d)/(?!\d) 数字边界断言) |
TC-通用-005-025
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-025 |
| 用例标题 | 业务键(question_id/option_id/client_msg_id)不被 mask |
| 关联 AC | AC-09 |
| 前置条件 | 单元测试 |
| 测试步骤 | 1. mask_sensitive_text("fault_type_network_down_12345678")(不应误伤)2. 验证 question_id="fault_type" 等保持原值(在 _build_selected_options_snapshot 输出中验证) |
| 预期结果 | 1) 含 _ 分隔的数字串不被破坏(按单词边界);2) snapshot 中 question_id 与 option_id 完整保留 |
| 优先级 | P1 |
| 自动化建议 | ✅ test_mask_skips_business_keys |
| 静态审查锚点 | h5_ai_task.py:305-314(snapshot 不调用 mask on qid/oid) |
2.D H5 UUID 生命周期
TC-通用-005-026
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-026 |
| 用例标题 | 首次点选:pendingOptionSelect 由 null → 含 client_msg_id |
| 关联 AC | AC-05 |
| 前置条件 | 1) pendingOptionSelect.value=null;2) H5 调用 sendOptionSelect |
| 测试步骤 | 1. 检查发送后 store 状态 |
| 预期结果 | 1) pendingOptionSelect.value !== null;2) clientMsgId 为合法 UUID v4;3) attempts=1;4) sentAt 为 Date.now() |
| 优先级 | P0 |
| 自动化建议 | ✅ conversation.spec.ts → first click sets pending |
| 静态审查锚点 | conversation.ts:1560-1589 |
TC-通用-005-027
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-027 |
| 用例标题 | 重试复用:失败重发使用同 client_msg_id(attempts+1) |
| 关联 AC | AC-05 + §4.4 |
| 前置条件 | 1) pendingOptionSelect.value 已存在;2) WS send 失败 |
| 测试步骤 | 1. 模拟 WS 未连接,重发同一选项;2. 抓 client_msg_id |
| 预期结果 | 1) 第二次发送复用同一 UUID;2) attempts=2;3) 服务端 5 秒窗口去重,DB 仅 1 行 |
| 优先级 | P0 |
| 自动化建议 | ✅ conversation.spec.ts → retry reuses UUID |
| 静态审查锚点 | conversation.ts:1561-1564(复用分支) |
TC-通用-005-028
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-028 |
| 用例标题 | 主动重选:同 question_id + 时间间隔 >1s 生成新 UUID |
| 关联 AC | AC-05 |
| 前置条件 | 1) pendingOptionSelect.value 已存在;2) 同 question_id 选项再次点击 |
| 测试步骤 | 1. mock Date.now() 偏移 2s;2. 调用 sendOptionSelect |
| 预期结果 | 1) 新 client_msg_id ≠ 上次;2) DB 累计 +1 行;3) 旧行不被覆盖 |
| 优先级 | P0 |
| 自动化建议 | ✅ conversation.spec.ts → user reselect generates new UUID |
| 静态审查锚点 | conversation.ts:1555-1568(isUserReselection 判断) |
2.E 性能压测
TC-通用-005-029
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-029 |
| 用例标题 | 20 个协程并发同 UUID:仅 1 次 INSERT / 1 次 broadcast / 1 次 Dify |
| 关联 AC | PRD §8 + 技术方案 §9 |
| 前置条件 | 1) 集成测试环境;2) mock ws_manager + Dify |
| 测试步骤 | 1. asyncio.gather(*[_handle_option_select(...) for _ in range(20)]);2. 统计 |
| 预期结果 | 1) DB 行数 += 1;2) broadcast 计数 = 1;3) Dify 任务计数 = 1;4) 其他 19 次命中 5 秒幂等 |
| 优先级 | P1 |
| 自动化建议 | ✅ test_concurrent_20_single_uuid |
| 静态审查锚点 | ws.py:300-336(lock + 5s 查询) |
TC-通用-005-030
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-030 |
| 用例标题 | 1000 条 option_select 的 latestSelectionPerQuestion 计算耗时 |
| 关联 AC | 技术方案 §9 |
| 前置条件 | 1) 构造 1000 条 mock;2) performance.now() 测时 |
| 测试步骤 | 1. 构造 1000 条 msg_type=option_select;2. 计算 latestSelectionPerQuestion;3. 100 次循环取平均 |
| 预期结果 | 平均耗时 < 10ms(典型 N=1000, Vue computed 单帧计算) |
| 优先级 | P2 |
| 自动化建议 | ✅ MessageBubble.spec.ts → 1000 rows performance |
| 静态审查锚点 | MessageBubble.vue:382-405(O(N) 单次扫描) |
2.F Dify 集成
TC-通用-005-031
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-031 |
| 用例标题 | option_select 触发:5 字段 inputs 完整透传 |
| 关联 AC | AC-07 |
| 前置条件 | 1) mock Dify /v1/chat-messages;2) H5 点选 |
| 测试步骤 | 1. 检查 payload.inputs 字段 |
| 预期结果 | 1) feedback_type=="option_select";2) question_id 与 H5 一致;3) option_id/option_value 一致;4) option_label 是 mask 后文本 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_dify_inputs_feedback_context |
| 静态审查锚点 | ws.py:433-439 + ai_service.py:279 |
TC-通用-005-032
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-032 |
| 用例标题 | 普通文本消息 inputs 仍为空 dict |
| 关联 AC | AC-07(普通文本链路不受影响) |
| 前置条件 | 1) H5 发送普通 text 消息 |
| 测试步骤 | 1. mock Dify;2. 检查 payload |
| 预期结果 | 1) payload.inputs == {};2) feedback_type 字段不存在;3) 仅 query / user / response_mode |
| 优先级 | P1 |
| 自动化建议 | ✅ test_dify_plain_text_empty_inputs |
| 静态审查锚点 | ai_service.py:279(inputs or {}) + h5_ai_task.py:1609(普通文本传 None) |
TC-通用-005-033
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-033 |
| 用例标题 | 代理 fallback(dify2openai):feedback_context 放入 metadata,不拼原值 |
| 关联 AC | 工程师报告 §八 偏离项 #5 |
| 前置条件 | 1) mock 原生 API 失败;2) 走 dify2openai 路径 |
| 测试步骤 | 1. 检查代理 payload |
| 预期结果 | 1) payload.metadata.feedback_context 含 5 字段;2) payload.messages[0].content 不含 option 原值(仅原 query 内容);3) 不破坏 OpenAI 兼容层 |
| 优先级 | P1 |
| 自动化建议 | ✅ test_proxy_fallback_metadata |
| 静态审查锚点 | ai_service.py:476-479(metadata 注入) |
2.G 转人工快照
TC-通用-005-034
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-034 |
| 用例标题 | 转人工快照:每 question_id 仅 1 条最新选择,按 (created_at desc, id desc) 取 |
| 关联 AC | AC-08 |
| 前置条件 | 1) 同一 question_id 选 3 次;2) Dify 返回 escalating |
| 测试步骤 | 1. 触发转人工;2. 校验 snapshot |
| 预期结果 | 1) 该 question_id 仅 1 条;2) message_id 为最后一次的 ID;3) selected_at 准确;4) label 已 mask |
| 优先级 | P0 |
| 自动化建议 | ✅ test_snapshot_latest_per_question |
| 静态审查锚点 | h5_ai_task.py:277-298(row_number window function) |
TC-通用-005-035
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-035 |
| 用例标题 | 转人工快照同时注入:conversation.tags.selected_options + conversation_updated.data.selected_options |
| 关联 AC | PRD §4.7 + 技术方案 §6 |
| 前置条件 | 1) Dify 触发 escalating |
| 测试步骤 | 1. 检查 conversation.tags;2. 抓取坐席 WS 的 conversation_updated |
| 预期结果 | 1) conversation.tags.selected_options 写入;2) conversation.tags.selected_options_updated_at ISO 时间存在;3) conversation_updated.data.selected_options 含相同条目;4) 两处结构一致 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_snapshot_dual_inject |
| 静态审查锚点 | h5_ai_task.py:620-628(tags)+ h5_ai_task.py:743-755(data 注入) |
2.H 回归兼容
TC-通用-005-036
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-036 |
| 用例标题 | 普通文本消息链路不受 option_select 改造影响 |
| 关联 AC | 技术方案 §9 回归 |
| 前置条件 | 1) H5 发送普通 text |
| 测试步骤 | 1. 检查落库消息类型;2. 检查 Dify 调用;3. 检查 WS 广播 |
| 预期结果 | 1) messages.msg_type=="text";2) Dify inputs 为空;3) WS 仍广播 type=new_message 含 text 消息 |
| 优先级 | P0 |
| 自动化建议 | ✅ test_plain_text_chain_unaffected |
| 静态审查锚点 | h5_ai_task.py:1609(普通文本 None)+ messages.py:163(仅 option_select 分支 mask) |
TC-通用-005-037
| 字段 | 内容 |
|---|---|
| 用例编号 | TC-通用-005-037 |
| 用例标题 | ai_structured 推荐卡 + recommend_event 兼容(不删除旧字段) |
| 关联 AC | PRD §7 非目标 #6 + 技术方案 §9 回归 |
| 前置条件 | 1) Dify 返回 action+options;2) 旧客户端与新客户端并存 |
| 测试步骤 | 1. 检查 ai_reply 推送;2. 检查 dynamic_recommend;3. 检查 new_message 广播 |
| 预期结果 | 1) extra_data.action 仍含旧字段;2) extra_data.options 仍含旧字段;3) 不因 option_select 改造而丢失 |
| 优先级 | P1 |
| 自动化建议 | ✅ test_recommend_event_compat |
| 静态审查锚点 | h5_ai_task.py:557-590(extra_data.action/options 构造未改) |
三、覆盖矩阵
| PRD/技术方案要点 | 覆盖用例 |
|---|---|
| PRD §4.1 落库 schema | TC-001, TC-002, TC-011 |
| PRD §4.2 WS 标准广播 | TC-003, TC-012, TC-016 |
| PRD §4.3 坐席渲染 | TC-005, TC-006, TC-018, TC-019 |
| PRD §4.4 H5 字段 | TC-007, TC-026~TC-028 |
| PRD §4.5 幂等 | TC-008, TC-013, TC-014, TC-029 |
| PRD §4.6 Dify inputs | TC-009, TC-031~TC-033 |
| PRD §4.7 转人工快照 | TC-010, TC-034, TC-035 |
| PRD §4.8 敏感词 mask | TC-011, TC-021~TC-025 |
| PRD §5 AC-01~AC-10 | TC-001~TC-012(含 AC-10 E2E) |
| PRD §6 边界场景 | TC-013~TC-020 |
| PRD §8 风险 | TC-014, TC-016, TC-029 |
四、自动化落库建议(pytest/vitest 目录)
| 文件 | 覆盖 |
|---|---|
src/backend/tests/test_option_select.py |
TC-001 |
src/backend/tests/test_sensitive.py |
TC-021~TC-025 |
src/backend/tests/test_dify_inputs.py |
TC-031~TC-033 |
src/frontend-h5/src/stores/conversation.spec.ts |
TC-007, TC-017, TC-026~TC-028 |
src/frontend-agent/src/components/chat/MessageBubble.spec.ts |
TC-005 |
注:自动化测试代码尚未在仓库中提交(
Glob未发现test_option_select.py与test_sensitive.py),需工程师在 T02/T03 验收前补齐。