| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- """第二阶段异常场景目录(任务书 §12-§15)。
- 纯数据 + 应用/恢复指令:副作用由 app.py 的 fault_apply / fault_recover 执行,
- 本模块不连接数据库、不改任何状态,保证场景定义可单测、可审计。
- kind 语义:
- - auto 模拟器一键预置(apply 指令由模拟器执行)
- - guide 需测试人员在 Ai-DOP 侧人工操作(页面展示指引,模拟器只控制第三方一侧)
- 被 Ai-DOP 正确拒绝属于测试通过,不属于工具失败。
- """
- from __future__ import annotations
- # 场景报告八列(任务书 §15)
- FAULT_REPORT_FIELDS: list[tuple[str, str]] = [
- ("scenarioId", "场景"),
- ("simBehavior", "模拟端行为"),
- ("expected", "预期结果"),
- ("actualResponse", "实际响应"),
- ("aidopFlowStatus", "Ai-DOP 流水状态"),
- ("targetChanged", "目标数据是否变化"),
- ("pass", "是否符合预期"),
- ("note", "备注"),
- ]
- # ---------------------------------------------------------------------------
- # N1 方式甲 DB_SYNC(10 场景)
- # ---------------------------------------------------------------------------
- N1_SCENARIOS: list[dict] = [
- {"id": "N1-01", "channel": "DB_SYNC", "kind": "guide", "title": "数据库地址/端口错误",
- "sim": "测试人员在 Ai-DOP 侧把来源连接串改为未监听端口(如 3307)。模拟器不修改 Ai-DOP 配置。",
- "expect": "Ai-DOP 测连或同步失败,提示连接被拒绝/超时。",
- "recover": "把 Ai-DOP 侧连接串改回正确地址。"},
- {"id": "N1-02", "channel": "DB_SYNC", "kind": "guide", "title": "数据库用户名/密码错误",
- "sim": "测试人员在 Ai-DOP 侧填写错误的测试账号/密码(如 sim_user/wrong)。",
- "expect": "认证失败(Access denied),同步任务失败。",
- "recover": "改回正确测试账号。"},
- {"id": "N1-03", "channel": "DB_SYNC", "kind": "guide", "title": "无表权限",
- "sim": "测试人员在模拟库上创建受限账号(仅 SELECT 部分表或完全无权限),Ai-DOP 侧配置该账号。",
- "expect": "查询被拒绝(SELECT command denied)。",
- "recover": "Ai-DOP 侧换回有权限账号。"},
- {"id": "N1-04", "channel": "DB_SYNC", "kind": "auto", "title": "源表不存在",
- "sim": "模拟器删除源表(DROP TABLE)。测试人员在 Ai-DOP 侧保持配置不变并触发同步。",
- "expect": "明确报表不存在(Table ... doesn't exist),同步失败不静默。",
- "recover": "点击恢复:按目录样例重建表并装载样例行。",
- "apply": {"type": "db_drop_table"}},
- {"id": "N1-05", "channel": "DB_SYNC", "kind": "guide", "title": "字段不存在(错误增量列)",
- "sim": "测试人员在 Ai-DOP 侧把增量列配置改为不存在的列名(如 sourceUpdatedAt2)。",
- "expect": "明确报列不存在(Unknown column),同步失败。",
- "recover": "改回正确增量列名。"},
- {"id": "N1-06", "channel": "DB_SYNC", "kind": "auto", "title": "空业务键",
- "sim": "模拟器装载 bizKey 为 NULL 的行(额外插入,不清空原数据)。",
- "expect": "被拒绝或隔离,不静默覆盖正常数据。",
- "recover": "点击恢复:删除空键行并重建样例行。",
- "apply": {"type": "db_load_null_bizkey"}},
- {"id": "N1-07", "channel": "DB_SYNC", "kind": "auto", "title": "重复业务键",
- "sim": "模拟器重建无唯一键的表并装载两条相同 bizKey 的行。",
- "expect": "行为可解释(Ai-DOP 侧去重/告警),不产生脏数据。",
- "recover": "点击恢复:重建带唯一键的表并装载样例行。",
- "apply": {"type": "db_load_duplicate_bizkey"}},
- {"id": "N1-08", "channel": "DB_SYNC", "kind": "auto", "title": "增量列不推进",
- "sim": "模拟器更新一行数据但保持增量列时间不变(advance_increment=false)。",
- "expect": "该行不被误判为新增量;下一次正常推进后可同步。",
- "recover": "点击恢复:重建样例行。",
- "apply": {"type": "db_update_no_advance"}},
- {"id": "N1-09", "channel": "DB_SYNC", "kind": "auto", "title": "慢查询",
- "sim": "模拟器创建受控延迟视图 v_slow_{table}(每行 SLEEP 0.5 秒),并提示测试人员在 Ai-DOP 侧把 source_table_name 改为该视图名。",
- "expect": "Ai-DOP 读取耗时显著变长,超时行为可解释。",
- "recover": "Ai-DOP 侧改回原表名;点击恢复删除视图。",
- "apply": {"type": "db_slow_view", "sleepSeconds": 0.5}},
- {"id": "N1-10", "channel": "DB_SYNC", "kind": "guide", "title": "连接中断",
- "sim": "测试人员在同步过程中停止模拟库服务(或断开网络),Ai-DOP 侧触发同步。",
- "expect": "失败不记成功,重连后状态可恢复。",
- "recover": "重新启动模拟库服务。"},
- ]
- # ---------------------------------------------------------------------------
- # N2 方式乙 API_PULL(16 场景)
- # ---------------------------------------------------------------------------
- N2_SCENARIOS: list[dict] = [
- {"id": "N2-01", "channel": "API_PULL", "kind": "auto", "title": "401 未授权",
- "sim": "Mock API 返回 401(force_status=401)。",
- "expect": "Ai-DOP 拉取失败,流水记录失败状态,不写入数据。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"force_status": 401}}},
- {"id": "N2-02", "channel": "API_PULL", "kind": "auto", "title": "403 禁止",
- "sim": "Mock API 返回 403(force_status=403)。",
- "expect": "Ai-DOP 拉取失败,流水记录失败状态。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"force_status": 403}}},
- {"id": "N2-03", "channel": "API_PULL", "kind": "auto", "title": "404 路径不存在",
- "sim": "Mock API 返回 404(force_status=404)。",
- "expect": "Ai-DOP 拉取失败,明确提示路径不存在。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"force_status": 404}}},
- {"id": "N2-04", "channel": "API_PULL", "kind": "auto", "title": "429 限流",
- "sim": "Mock API 返回 429(force_status=429)。",
- "expect": "Ai-DOP 拉取失败或按限流策略重试。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"force_status": 429}}},
- {"id": "N2-05", "channel": "API_PULL", "kind": "auto", "title": "500 服务器错误",
- "sim": "Mock API 返回 500(force_status=500)。",
- "expect": "Ai-DOP 拉取失败,错误信息明确。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"force_status": 500}}},
- {"id": "N2-06", "channel": "API_PULL", "kind": "auto", "title": "503 服务不可用",
- "sim": "Mock API 返回 503(force_status=503)。",
- "expect": "Ai-DOP 拉取失败,错误信息明确。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"force_status": 503}}},
- {"id": "N2-07", "channel": "API_PULL", "kind": "auto", "title": "固定延迟",
- "sim": "Mock API 每次响应前延迟 3 秒(delay_seconds=3)。",
- "expect": "Ai-DOP 响应耗时约 3 秒,请求成功。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"delay_seconds": 3}}},
- {"id": "N2-08", "channel": "API_PULL", "kind": "auto", "title": "超时",
- "sim": "Mock API 睡眠 120 秒(timeout_seconds=120),超过 Ai-DOP 读取超时。",
- "expect": "Ai-DOP 侧超时终止,流水记录失败。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"timeout_seconds": 120}}},
- {"id": "N2-09", "channel": "API_PULL", "kind": "auto", "title": "空响应",
- "sim": "Mock API 返回 200 空 body(empty_response=true)。",
- "expect": "Ai-DOP 解析失败并明确报错,不写入数据。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"empty_response": True}}},
- {"id": "N2-10", "channel": "API_PULL", "kind": "auto", "title": "非法 JSON",
- "sim": "Mock API 返回截断的非法 JSON 文本(invalid_json=true)。",
- "expect": "Ai-DOP 解析失败并明确报错。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"invalid_json": True}}},
- {"id": "N2-11", "channel": "API_PULL", "kind": "auto", "title": "错误 Content-Type",
- "sim": "Mock API 返回 text/html 的 Content-Type(content_type=text/html)。",
- "expect": "Ai-DOP 按 Content-Type 校验失败或仍按 JSON 解析,行为可解释。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"content_type": "text/html"}}},
- {"id": "N2-12", "channel": "API_PULL", "kind": "auto", "title": "错误 response path",
- "sim": "Mock API 把数据挂到不存在的路径 data.notExist(response_path=data.notExist)。",
- "expect": "Ai-DOP 取不到列表,明确报错。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"response_path": "data.notExist"}}},
- {"id": "N2-13", "channel": "API_PULL", "kind": "auto", "title": "缺 bizKey",
- "sim": "Mock API 挂载去掉 bizKey 字段的覆盖行(/api/item)。",
- "expect": "Ai-DOP 去重键缺失,报错或隔离,不静默写入。",
- "recover": "恢复默认(清除覆盖行)。", "apply": {"type": "mock_api_override", "mode": "missing_bizkey"}},
- {"id": "N2-14", "channel": "API_PULL", "kind": "auto", "title": "重复 bizKey",
- "sim": "Mock API 挂载两条相同 bizKey 的覆盖行(/api/item)。",
- "expect": "Ai-DOP 去重行为可解释,不产生脏数据。",
- "recover": "恢复默认(清除覆盖行)。", "apply": {"type": "mock_api_override", "mode": "duplicate_bizkey"}},
- {"id": "N2-15", "channel": "API_PULL", "kind": "auto", "title": "cursor 不推进",
- "sim": "Mock API 无视 cursor 返回全部行(ignore_cursor=true)。",
- "expect": "Ai-DOP 每次拿到同样数据,去重后无新增,行为可解释。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"ignore_cursor": True}}},
- {"id": "N2-16", "channel": "API_PULL", "kind": "auto", "title": "cursor 乱序",
- "sim": "Mock API 不按 bizKey 排序返回(no_sort=true)。",
- "expect": "Ai-DOP 分页去重行为可解释,不产生脏数据。",
- "recover": "恢复默认。", "apply": {"type": "mock_api", "settings": {"no_sort": True}}},
- ]
- # ---------------------------------------------------------------------------
- # N3 方式丙 API_INBOUND(17 场景,任务书 §14 原文)
- # ---------------------------------------------------------------------------
- N3_SCENARIOS: list[dict] = [
- {"id": "N3-01", "channel": "API_INBOUND", "kind": "auto", "title": "缺 AccessKey",
- "sim": "推数请求不发送 X-Access-Key 头。",
- "expect": "Ai-DOP 拒绝(401/403 授权缺失),不产生流水数据。",
- "recover": "无需恢复(无状态注入)。", "apply": {"type": "inbound_case", "case": "missing-access-key"}},
- {"id": "N3-02", "channel": "API_INBOUND", "kind": "auto", "title": "错误 AccessKey",
- "sim": "发送未授权的 AccessKey(SIM-UNKNOWN-ACCESS-KEY,签名自洽)。",
- "expect": "Ai-DOP 拒绝(授权缺失/未知 AccessKey)。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "wrong-access-key"}},
- {"id": "N3-03", "channel": "API_INBOUND", "kind": "auto", "title": "错误 Secret/签名",
- "sim": "用错误 Secret 计算签名(bad_signature=true)。",
- "expect": "Ai-DOP 验签失败(401/403)。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "bad-signature"}},
- {"id": "N3-04", "channel": "API_INBOUND", "kind": "auto", "title": "body 摘要不一致",
- "sim": "发送后篡改 body(X-Content-SHA256 与实际不符)。",
- "expect": "Ai-DOP 摘要校验失败,明确拒绝。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "tamper-body"}},
- {"id": "N3-05", "channel": "API_INBOUND", "kind": "auto", "title": "缺幂等键",
- "sim": "请求不发送 Idempotency-Key 头。",
- "expect": "Ai-DOP 拒绝(幂等键必填)。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "missing-idem"}},
- {"id": "N3-06", "channel": "API_INBOUND", "kind": "auto", "title": "过期 timestamp",
- "sim": "X-Timestamp 设为 1 小时前。",
- "expect": "Ai-DOP 拒绝(时间戳超出允许窗口)。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "expired-ts"}},
- {"id": "N3-07", "channel": "API_INBOUND", "kind": "auto", "title": "nonce replay",
- "sim": "相同 nonce 连发两个请求。",
- "expect": "第二个被拒绝(防重放)。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "nonce-replay"}},
- {"id": "N3-08", "channel": "API_INBOUND", "kind": "auto", "title": "同幂等键同 body",
- "sim": "相同 Idempotency-Key 与 body 连发两次。",
- "expect": "第二次命中幂等返回相同结果,不重复落库。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "same-key-replay"}},
- {"id": "N3-09", "channel": "API_INBOUND", "kind": "auto", "title": "同幂等键不同 body",
- "sim": "相同 Idempotency-Key 但 body 不同。",
- "expect": "Ai-DOP 拒绝(幂等键冲突)。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "same-key-diff-body"}},
- {"id": "N3-10", "channel": "API_INBOUND", "kind": "auto", "title": "缺必填字段",
- "sim": "推数行删除契约必填字段(如 MDM_ITEM 的 Descr)。",
- "expect": "Ai-DOP 拒绝或隔离该行(映射/校验失败)。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "missing-required-field"}},
- {"id": "N3-11", "channel": "API_INBOUND", "kind": "auto", "title": "字段类型错误",
- "sim": "推数行把必填字段替换为结构敌对值(嵌套对象,实测比数字更能逼近类型校验边界)。",
- "expect": "Ai-DOP 拒绝或隔离该行(类型校验失败);若宽容转换则在报告中记录实证。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "wrong-field-type"}},
- {"id": "N3-12", "channel": "API_INBOUND", "kind": "auto", "title": "部分行拒绝",
- "sim": "push 信封多行中一行缺必填字段。",
- "expect": "坏行被拒绝,好行正常入站(或整体拒绝),行为可解释。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "partial-reject"}},
- {"id": "N3-13", "channel": "API_INBOUND", "kind": "auto", "title": "stale 数据",
- "sim": "推数行时间字段退到 2000 年(快照场景下模拟旧数据)。",
- "expect": "Ai-DOP 拒绝过期数据或按 stale 策略处理。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "stale"}},
- {"id": "N3-14", "channel": "API_INBOUND", "kind": "auto", "title": "未授权 entityCode",
- "sim": "推送到不存在/未授权的实体码 SIM_UNKNOWN_ENTITY_XX。",
- "expect": "Ai-DOP 返回 404 实体未注册(或 403 未授权)。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "unknown-entity"}},
- {"id": "N3-15", "channel": "API_INBOUND", "kind": "auto", "title": "未启用 entity",
- "sim": "测试人员先在 Ai-DOP 侧关闭某实体 inbound 开关,模拟器再向该实体推送(默认 S5_INVENTORY_TXN,可填其他已注册实体码)。",
- "expect": "Ai-DOP 拒绝(实体未启用)。",
- "recover": "Ai-DOP 侧重新启用该实体。",
- "apply": {"type": "inbound_case", "case": "disabled-entity"}},
- {"id": "N3-16", "channel": "API_INBOUND", "kind": "auto", "title": "bulk 部分失败",
- "sim": "bulk 端点 ndjson 多行中一行缺必填字段。",
- "expect": "Ai-DOP 返回逐行结果,坏行计数准确。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "bulk-partial-failure"}},
- {"id": "N3-17", "channel": "API_INBOUND", "kind": "auto", "title": "snapshot commit 失败",
- "sim": "提交不存在的 snapshotId(SNAP-NOT-EXIST-0001)。",
- "expect": "Ai-DOP 拒绝 commit,明确报错。",
- "recover": "无需恢复。", "apply": {"type": "inbound_case", "case": "snapshot-commit-failure"}},
- ]
- FAULT_SCENARIOS: list[dict] = N1_SCENARIOS + N2_SCENARIOS + N3_SCENARIOS
- FAULT_CHANNEL_LABELS = {
- "DB_SYNC": "方式甲 DB_SYNC(数据库同步)",
- "API_PULL": "方式乙 API_PULL(接口拉取)",
- "API_INBOUND": "方式丙 API_INBOUND(接口推送)",
- }
- def scenario_counts() -> dict:
- counts = {"DB_SYNC": 0, "API_PULL": 0, "API_INBOUND": 0, "total": 0}
- for s in FAULT_SCENARIOS:
- counts[s["channel"]] += 1
- counts["total"] += 1
- return counts
- def find_scenario(scenario_id: str) -> dict | None:
- sid = scenario_id.upper()
- return next((s for s in FAULT_SCENARIOS if s["id"].upper() == sid), None)
|