Skip to content

2026-09-27 迭代 — 验收用例(TC) ​

依据 PRD 原文 与 技术方案。PRD 定义产品行为,技术方案定义实现契约; 本文只把两者转成可重复执行、可留证据的判定条件,不另行扩大范围。 本文与 PRD、技术方案一同评审;预跑与证据归档由 T01(lamolabs-docs#87) 执行, 预跑完成后在 §10 回填结果与证据链接。

任务追踪:2026-09-27 父任务 lamolabs-docs#86 · 组织 Project

本批只产出本文档,不预跑。 预跑于 09-26 单独执行,结果与证据回填 §10。

1. 执行规则 ​

  • 编号含义:G<门禁号>-<用例号> = 发布门禁用例(与 lamolabs-docs#87 的「门禁映射」表一一对应); S = PRD §8 点名、但父任务门禁表未单列门禁的补充覆盖用例;N = 非功能硬阈值;MTG = 上会用例子集。 编号一经发布不得复用或重排,后续任务的「测试追踪」按本编号引用。
  • 分层:§4 为底线用例(门禁 #1—#5、#7,对应设计 §15 的 B01–B09 + B12);§5 为可顺延用例 (门禁 #6、#8、#9,对应 B10 / B11 / B13 / B15 / B16)。顺延则门禁 #6、#8、#9 转下期,底线用例不得顺延。 每条用例头部显式标注分层,§1.2 汇总表同口径。
  • 会上只执行 §3 的 9 条,每条判定时间不超过 3 分钟;其余用例在 2026-09-26 预跑阶段完成,会上只复核证据。
  • 一条用例的全部判定条件均满足才算通过;部分满足、无证据或只能口头说明都记为不通过。
  • 因环境、资产或评审结论缺失而无法执行时记为阻塞,不允许把"未测"写成"通过"。
  • 空集通过必须显式标注:当某条用例的被测面客观上为空集(如某服务根本不存在该类接口)时, 记为**空集通过(vacuous pass)**而非普通"通过",并在证据与 §10 备注里写明「被测面为空的原因」。 空集通过与真实通过在一张结果表里长得一模一样,不标注会让后来者误以为某个接口真被加固过。
  • 离线可重复:每条用例自建所需数据、自行清理,不依赖上一条用例的残留状态。 自建实体统一用 tc-<用例编号小写>-<6 位随机串> 前缀命名,用例结束按前缀删除并断言零残留; 种子模板 tpl_release_prod 由发布步骤种入(design §16),用例只读引用、不建不改。
  • 三路用例固定同一调用方身份:门禁 #1 的三次创建必须使用同一个 HMAC keyId(namespace 从已认证身份推导, 换调用方会让逐字段比对失败)。
  • 并发用例脚本化:S01 由脚本发起并发请求,不靠人工手点。
  • 证据不接受截图代替日志:接口类用例归档请求/响应 JSON,UI 类用例归档截图或录屏(截图只作 UI 用例的证据)。
  • 涉及时间的用例以服务端时间为准;毫秒级阈值(N01—N03)用服务端毫秒时间戳差计算,不用人工秒表。
  • 三项硬阈值必须实测取数,不得估算(取数口径见 §7);数据由预跑阶段采集,会上只复核证据。
  • 错误码断言必须比对 error.code 具体值(design §10.1),只看 HTTP 状态不算通过。

1.1 本期冻结的验收基线 ​

项目验收值
模板绑定三路统一存 template_snapshot(NOT NULL);template_id / resource_type 降级为溯源字段;source_type ∈ {resource_type, template_id, inline}
三路允许差异字段仅三个:source_type / template_id / resource_type。template_config 已被 template_snapshot 取代,不再作为比对维度(design §7.1)
装配管线分叉点只有 resolveTemplate() 一处分叉;分叉点之后不得出现任何 source_type 判断(code review 按此检查)
第一阶段(创建)只预填 fill_by=system 字段;审批单 pending;不实例化节点
第二阶段(提交)merged = { ...defaults, ...payload, ...submitterInput },整值替换槽位;校验不过即整体回滚,审批单停留 pending
节点状态枚举pending / active / in_progress / approved / rejected / cancelled
审批人状态枚举pending / approved / rejected / cancelled
审批单状态枚举pending / in_progress / approved / rejected / withdrawn / cancelled / auto_approved / auto_rejected(后两者本期无代码路径产生)
strategy 枚举single / countersign / any(无 multi_level)
会签 modeall / majority / ratio;ratio 必须带 threshold ∈ (0,1],判据为 通过比例 ≥ threshold
判定优先级veto_users 一票否决优先于一切 mode 判定;cancelled 审批人不计入分母
或签终态任一通过 → 节点 approved 且其余 pending 置 cancelled;全员拒绝 → 节点 rejected;单人拒绝不终止
条件 operator== != > >= < <= in not_in 共八种;跨类型比较判不命中;槽位缺失判不命中并走 next.default
幂等重放判断先于终态检查与"是否当前节点"检查;同一审批人对同一节点同一决策不产生第二条 ApprovalAction
并发整单一把锁(approval_orders 行级 FOR UPDATE),同一单的并发操作串行化
append-onlyapproval_actions / audit_logs 的 UPDATE / DELETE 一律被触发器拒绝,报 APPEND_ONLY_VIOLATION(design §7.3)
快照冻结approval_orders.template_snapshot / source_type 变更被触发器拒绝,报 IMMUTABLE_FIELD
节点与审批人上限APPROVAL_MAX_NODES_PER_ORDER=20、APPROVAL_MAX_APPROVERS_PER_NODE=20;列表分页上限 APPROVAL_LIST_PAGE_SIZE_MAX=100
鉴权 fail-closedperm-api 不可达时管理接口 PERM_UNAVAILABLE(503)拒绝;审批操作(submit / actions / withdraw)不受影响
权限点(命名空间 approval)approval.template.manage / approval.template.read / approval.resource-type.manage / approval.order.read.all / approval.audit.read
读写矩阵editable 唯一实现在后端:system 恒 false;submitter 字段仅提交人且 status=pending 为 true;approver 字段仅本人待处理时为 true(design §10.4)
硬阈值审批单创建 API p99 < 1s;审批操作 API p99 < 1s;待办列表与「我发起的」列表首屏 p99 < 2s

1.2 用例编号与门禁映射 ​

门禁门禁要点主责任务用例分层
#1三种发起方式产出结构一致的审批单B04G1-01—G1-03底线
#2tpl_release_prod 两条分支各跑通B06G2-01—G2-03底线
#3会签三 mode + 一票否决B08G3-01—G3-04底线
#4或签两条终态B08G4-01—G4-02底线
#5槽位校验失败不产生半实例化审批单B05G5-01—G5-03底线
#6审计完整性B11G6-01—G6-02可顺延
#7门户主流程全程不碰 APIF03 / F02G7-01—G7-05底线
#8零硬编码管理员B13G8-01—G8-04可顺延
#9存量接入债还清B15 / B16G9-01—G9-03可顺延
#1009-26 预跑归档 + 09-27 验收会全通过本任务G10-01—G10-02底线
—PRD §8 点名补充(状态机并发 / 幂等 / 撤回与取消 / 读写矩阵)B09 / B10 / F02S01—S03(读写矩阵并入 G7-02)S01、S02 底线;S03 可顺延
—三项硬阈值实测本任务N01—N03底线

2. 环境前置 ​

  • nexo-approval-api test 实例已部署并监听 3003;nexo_approval 库迁移(drizzle 生成 + 3 个手工触发器迁移 + 2 个部分唯一索引)与初始化脚本已执行。

  • 只读 psql 访问 nexo_approval(断言 approval_orders / approval_nodes / approval_node_approvers / approval_actions / approval_comments / audit_logs / approval_templates / resource_types)。

  • nexo-approval-console 已部署并经壳(nexo-app)侧边栏入口可达;独立访问方式亦可登录。

  • nexo-perm-api(3002)可用,命名空间 approval 与 5 个权限点、角色「审批平台管理员」已由初始化脚本幂等建好; nexo-perm-api#35 的角色成员反查 internal 接口可用(供角色审批人展开)。

  • nexo-account(仓库 nexo-auth,3001)与 nexo-im-api(3000)test 实例可用(供门禁 #9 与登录链路)。

  • 门禁 #9 的 SDK 版本前置(易漏,务必先核,且必须核到分支而非工作区):鉴权客户端 createPermissionCheckClient 只存在于 @lamolabs/nexo-backend-sdk 0.3.0-alpha.1(v0.1.0 / v0.2.0 无 src/permission-check/,v0.3.0-alpha.1 才有);更早版本的 dist/index.d.ts 只有 errors / internal-auth / jwks / logger / mask,无任何 permission-check 导出。 各仓基线分支(git show <ref>:package.json)实测如下:

    仓库基线分支锁定版本是否需升级原因
    nexo-auth(B15)0.1.0(main / dev 及对应 origin 全为 0.1.0)需升级要为 8 条管理接口挂判定,必须能编译鉴权客户端
    nexo-im-api(B16)0.1.0(origin/main / origin/dev 均为 0.1.0)需升级B16 交付的是鉴权机制(中间件 + 客户端 + 权限点定义),机制本身要编译,故仍需升版
    nexo-perm-api0.3.0-alpha.1已就绪—
    nexo-approval-api0.3.0-alpha.1已就绪—

    两侧升级的语义不同,别混为一谈:B15 升版是为了给端点加门;B16 升版是为了交付机制本身—— 它把中间件挂到 0 条路由上(这是正确结果,见 G9-01 被测面表)。 所以核验 B16 时的判据不是「有没有升 SDK」(升了是对的),而是 「中间件有没有被挂到任何自助端点上」:grep -rn authz src/routes/index.ts src/routes/*/routes.ts src/routes/*/index.ts 应当零命中。若命中 /users / /conversations* / /ws 等任一条,即为往消息链路加耦合, 按净回归处理——这正是 nexo-im-api#85 明令禁止的。

    预跑提示:package.json 的版本因分支而异。核验一律用 git show <基线分支>:package.json,不要读工作区——工作区可能已 checkout 到特性分支并带着未提交改动, 会得出与基线相反的结论(本条曾据此把 nexo-im-api 基线误读为已升到 0.3.0-alpha.1,实际其 origin/main / origin/dev 均为 0.1.0,是 B16 特性分支的工作区升了版)。

  • @lamolabs/* 走 GitHub Packages 私有 registry(各仓 .npmrc 声明 @lamolabs:registry=https://npm.pkg.github.com), 需要 read:packages 权限的 token。新建仓库必须在 GitHub 仓库 Secrets 里配 GH_PACKAGES_TOKEN (CI 用 echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_PACKAGES_TOKEN }}" >> .npmrc 注入), 否则 pnpm install 直接 ERR_PNPM_FETCH_401。本用例的预跑依赖各仓可构建产物, 任一仓的该 Secret 缺失都会让 09-26 预跑无法开始——预跑前逐仓核对。

    写法提醒:VitePress 按 Vue 模板编译,行内代码里的美元符号加双花括号会被当作插值求值: 轻则构建时抛 TypeError 并静默吞掉该段正文(构建仍打印 complete,页面却缺内容), 重则直接 Error parsing JavaScript expression 中断构建。凡需在正文里写出字面插值写法的, 用 <span v-pre> 包裹或放进围栏代码块——本行上方的 CI 命令即按此处理。

  • 种子模板 tpl_release_prod 已由发布步骤种入(design §16),用例只读引用、不建不改。实测(B13 的 seed 实现): namespace = nexo-release,名称 tpl_release_prod,同时种入绑定它的资源类型 release(owner = nexo-release); 幂等靠 (namespace, name) 与 type_key 唯一约束,已存在即跳过且不覆盖。

    注意区分两个 namespace:模板的 nexo-release 是模板归属;审批单的 namespace 由调用方 HMAC 身份推导, 二者是不同的字段。门禁 #1 只要求三次创建的 namespace 彼此相等(同一身份),不要求等于 nexo-release。

    种子的审批人默认值刻意全为 type: 'user'(app_tl=u_101、sre_1=u_201、sre_2=u_202、oncall_lead=u_301), 而非 design §11 示例里的 { type: 'role', id: '7' }——若照抄示例,门禁 #2 的每次提交都会依赖 perm-api 里 恰好存在 id=7 的角色,引擎用例就变成了跨平台联调用例。故角色展开(G5-03)必须自建模板, 不能靠种子模板。

  • 调用方身份:内部通道 HMAC 凭据就绪(keyId 对应 app 主体,namespace 由此推导);门禁 #1 全程复用同一 keyId。

  • 账号矩阵:1 个发起方用户、≥4 个审批人(其中 ≥1 人经 perm-api 角色展开得到)、1 个平台管理员(持全部 approval.* 权限点)、 1 个无任何 approval.* 权限的普通用户。 种子模板自带的 4 个审批人标识必须可用:u_101(node_tl)、u_201 / u_202(node_sre 会签)、 u_301(node_emergency)——它们由种子写死在 defaults 里,用例不建这些账号, 只要求 test 环境能以其身份登录门户(门禁 #2 / #3 的端到端提交与审批依赖此)。

  • 工具:psql(只读断言)、浏览器 DevTools / HAR、并发脚本、压测脚本、服务端结构化日志检索。

  • 造数与压测脚本归档 evidence/scripts/,结构化取数(JSON)落 evidence/logs/,UI 走查截图与录屏落 evidence/ui/。

  • 敏感信息脱敏:HMAC 密钥不入库,Bearer Token 只留前 8 位,业务 payload 中的真实标识用测试值。

3. 会上用例子集(固定 9 条) ​

编号覆盖用例操作摘要判定阈值
MTG-01G1-01同一模板、同一调用方身份分别走 resource_type / template_id / template_config 创建template_snapshot 三路 deep-equal;业务字段逐列相等;仅三个溯源字段允许不同
MTG-02G2-01用 tpl_release_prod 以 is_urgent=true 提交节点序列 node_tl → node_emergency → node_sre,node_emergency 落行
MTG-03G2-02同一模板以 is_urgent=false 提交节点序列 node_tl → node_sre,node_emergency 零行
MTG-04G3-023 人会签 majority 节点两人通过第 2 人通过即节点 approved,第 3 人未被要求处理
MTG-05G4-01或签节点一人通过节点 approved,其余 pending 全部 cancelled
MTG-06G5-01提交时必填字段缺失 / 审批人解析为空两次均 400 具体错误码;审批单仍 pending;节点表零行
MTG-07G7-01从详情页链接进入 → 登录回跳 → 补充提交 → 审批 → 完成全程浏览器内完成,录屏中无任何 API 工具调用
MTG-08G7-02三角色视角分别打开同一审批单详情页九格 editable 与 design §10.4 完全一致
MTG-09S01脚本对同一会签节点两人同时通过节点只流转一次;approval_actions 恰 2 条;下一节点 seq 唯一

N01—N03 三项硬阈值在会上只复核预跑证据,不现场重测;证据缺失时该条记阻塞。

4. 底线用例(门禁 #1—#5、#7) ​

主责任务 B01–B09 + B12(design §15)。本节用例不得顺延;顺延即门禁失败。

4.1 门禁 #1 — 三路发起产出结构一致的审批单(B04) ​

G1-01 三路创建结构一致(同一模板 + 同一调用方身份) ​

  • 分层:底线(门禁 #1 · B04)

  • 前置条件:种子模板 tpl_release_prod 可用;同一 HMAC keyId 可调 /internal/*; 用例自建一个绑定该模板的 resource_type(type_key = tc-g1-01-<rand>); 可只读查询 approval_orders / approval_nodes / approval_node_approvers。

  • 操作步骤: ① 三路共用同一份 payload(含 app_name / version / is_urgent / resource_url)与同一 submittedBy, 分别调 POST /internal/orders:resourceType = tc-g1-01-<rand>、templateId = <tpl_release_prod 主键>、 templateConfig = <从 GET /api/templates 取到的该模板四份配置原样>; ② 对三个 orderId 分别只读查询 approval_orders 全列,逐列比对; ③ 三路各以同一份 submitterInput 调 POST /api/orders/:id/submit,再次比对 form_data / resolved_slots; ④ 比对三单的 approval_nodes(seq / node_key / strategy / countersign_rule)与 approval_node_approvers(approver_type / approver_id / from_role_id)。

  • 预期结果:三次创建均 201 且 status = pending、detailUrl 形如 ${APPROVAL_PORTAL_BASE_URL}/orders/:id; 逐列比对结果:

    列期望
    template_snapshot三路 deep-equal
    namespace相等(同一 HMAC 身份推导)
    status相等
    payload相等
    form_data相等(system 预填结果)
    resource_url相等
    submitted_by相等
    resolved_slots提交后相等(创建时为 NULL)
    source_type允许不同(依次为 resource_type / template_id / inline)
    template_id允许不同;方式三(inline)必为 NULL(design §7.2 列注释);方式一/二为所解析到的模板主键(实测核对)
    resource_type允许不同;方式二/三必为 NULL(design §7.2 列注释);方式一为所注册的 type_key
    id / created_at / updated_at允许不同(自增与时间戳)

    允许不同的仅三个溯源字段(source_type / template_id / resource_type)加上自增与时间戳列; template_config 已被 template_snapshot 取代,不作为比对维度(design §7.1)。 步骤③后三单的 form_data / resolved_slots 仍相等,且 approval_nodes 的 (seq, node_key, strategy, countersign_rule) 三元组序列逐项相等、 approval_node_approvers 集合逐项相等。

  • 证据采集:三次创建与三次提交的请求/响应 JSON(evidence/logs/g1-01-three-paths.json); 三单 approval_orders 全列的 psql 断言输出;节点与审批人表的对比输出; 代码检索结果(src/ 中 source_type 的出现位置仅限 resolveTemplate 与其后的落库赋值,无装配分支)。

  • 对应:PRD §8 第一条 · FR-4 / FR-5 · design §7.1 / §8.1 · 上会 MTG-01

G1-02 三路入参互斥与内联模板不落库 ​

  • 分层:底线(门禁 #1 · B04)
  • 前置条件:同 G1-01;记录用例开始前 approval_templates 的行数。
  • 操作步骤:① 同时传 resourceType + templateId;② 同时传 templateId + templateConfig;③ 三者都传; ④ 三者都不传;⑤ 单独传 templateConfig(合法内联配置);⑥ 断言 approval_templates 行数。
  • 预期结果:①—④ 均 400 VALIDATION_FAILED,零新审批单、零新节点、零新审计行; ⑤ 201,source_type = 'inline'、template_id IS NULL、resource_type IS NULL、template_snapshot 等于所传配置; ⑥ 模板表行数与用例开始时一致(内联模板不持久化为可复用模板); 另断言内联路径同样走 FR-2 全量校验:传一份违反 C01/C07 的内联配置应 400 TEMPLATE_INVALID 且 detail.issues[] 带 path(PRD §10 Q5)。
  • 证据采集:五次请求/响应 JSON;approval_templates 前后计数断言;非法内联配置的 TEMPLATE_INVALID 响应(含 issues[])。
  • 对应:PRD FR-4 / §10 Q5 · design §8.1 / §10.1

G1-03 第一阶段只预填、不建节点 ​

  • 分层:底线(门禁 #1 · B04)
  • 前置条件:同 G1-01;tpl_release_prod 的 form_schema 同时含 fill_by=system 与 fill_by=submitter 字段。
  • 操作步骤:① 以 resourceType 路创建,payload 只带 system 字段值并额外塞一个 submitter 字段值; ② 查 approval_orders 该行;③ 查该 order_id 下的 approval_nodes 与 approval_node_approvers 行数。
  • 预期结果:status = pending;form_data 只含 fill_by=system 字段的值(submitter 字段被忽略); resolved_slots IS NULL;submitted_at IS NULL;节点表与审批人表零行; 此时 GET /internal/orders/:id/status 返回 currentNode: null(未实例化节点)。
  • 证据采集:创建请求/响应 JSON;该行全列 psql 断言;节点与审批人表计数为 0 的输出;status 接口响应。
  • 对应:PRD FR-5 · design §8.1 · 上会并入 MTG-01

4.2 门禁 #2 — tpl_release_prod 两条分支(B06) ​

G2-01 is_urgent = true 走紧急分支 ​

  • 分层:底线(门禁 #2 · B06)
  • 前置条件:tpl_release_prod 的 flow_config 含 static_nodes(node_tl → node_sre)、 dynamic_nodes(node_emergency → node_sre),node_tl.next.if 的条件为 is_urgent == true; 槽位 app_tl / oncall_lead / sre_1 / sre_2 均可解析。
  • 操作步骤:① 以 payload.is_urgent = true 创建并提交;② 按 seq 升序查 approval_nodes。
  • 预期结果:节点序列为 seq=1 node_tl → seq=2 node_emergency → seq=3 node_sre; node_emergency 有行且 strategy = single;node_sre 为 countersign 且 countersign_rule.mode = 'all'; 首节点 status = active 且 started_at 非空,其余 pending;审批单 status = in_progress、submitted_at 非空。
  • 证据采集:创建与提交的请求/响应 JSON;approval_nodes 按 seq 的输出(evidence/logs/g2-branches.json)。
  • 对应:PRD §8 第二条 · FR-7 · design §8.3 · 上会 MTG-02

G2-02 is_urgent = false 不走紧急分支 ​

  • 分层:底线(门禁 #2 · B06)
  • 前置条件:同 G2-01。
  • 操作步骤:① 以 payload.is_urgent = false 创建并提交;② 按 seq 升序查 approval_nodes; ③ 按 node_key = 'node_emergency' 精确查询。
  • 预期结果:节点序列为 seq=1 node_tl → seq=2 node_sre;node_emergency 零行(dynamic_nodes 仅在条件命中时实例化); 其余断言同 G2-01。
  • 证据采集:创建与提交的请求/响应 JSON;approval_nodes 输出;node_emergency 计数为 0 的断言。
  • 对应:PRD §8 第二条 · FR-7 · design §8.3 · 上会 MTG-03

G2-03 八种 operator 求值矩阵与缺失槽位回落 ​

  • 分层:底线(门禁 #2 · B06)

  • 前置条件:flow 模块为纯函数(无 DB 无 ctx),可直接以脚本调用;另可自建模板 tc-g2-03-<rand>。

  • 操作步骤: ① 纯函数矩阵:脚本对 evalCondition({slot, operator, value}) 逐行输入下表,记录返回值; ② 端到端:自建模板,next.if 分别使用 in 与 not_in 命中两个不同 dynamic_nodes,各走一单; ③ 边界:槽位缺失、两侧类型不同(number 对 ISO8601 串)各跑一行。

    operator槽位值value期望
    ==truetrue命中
    =="1"1不命中(跨类型严格相等)
    !="prod""staging"命中
    >32命中
    >=22命中
    <"2026-09-01T00:00:00Z""2026-09-26T00:00:00Z"命中(ISO8601 日期比较)
    <="2026-09-26T00:00:00Z""2026-09-26T00:00:00Z"命中
    in"staging"["staging","production"]命中
    not_in"dev"["staging","production"]命中
    >3"2026-09-26T00:00:00Z"不命中(跨类型)
    ==槽位缺失true不命中
  • 预期结果:纯函数矩阵逐行与期望一致;端到端两单各自只实例化被命中的那个 dynamic_node(另一个零行); 槽位缺失时该条件判为不命中并走 next.default(PRD FR-6 允许条件引用的槽位缺失); 条件求值顺序按 if 数组顺序短路,命中即返回 then。

  • 证据采集:矩阵输入/输出的 JSON(evidence/logs/g2-03-operators.json);端到端两单的节点序列断言; 自建模板与两单数据的清理断言。

  • 对应:PRD FR-6 / FR-7 · design §8.3 · §11 C09

4.3 门禁 #3 — 会签三 mode 与一票否决(B08) ​

G3-01 会签 all:全部通过才通过 ​

  • 分层:底线(门禁 #3 · B08)
  • 前置条件:可自建含 strategy = countersign、countersign_rule.mode = 'all' 且 3 名审批人的模板 (tc-g3-01-<rand>);3 名审批人账号可登录门户。
  • 操作步骤:① 走一单到该节点 active;② 第 1 人通过;③ 第 2 人通过;④ 查节点与审批单状态; ⑤ 第 3 人通过;⑥ 再查节点与审批单状态;另起一单重复 ①②,第 2 人改为拒绝。
  • 预期结果:② 后节点 in_progress(非终态);③ 后仍 in_progress(all 需等全员); ⑤ 后节点 approved、审批单流转到下一节点或 approved; 拒绝分支:任一 rejected → 节点立即 rejected,其余 pending 审批人置 cancelled,后续 pending 节点全部 cancelled,审批单 rejected。
  • 证据采集:每一步的请求/响应 JSON;approval_nodes 与 approval_node_approvers 的状态流转输出 (evidence/logs/g3-01-all.json)。
  • 对应:PRD §8 第三条 · FR-9 · design §9.1

G3-02 会签 majority:3 人 2 通过即决 ​

  • 分层:底线(门禁 #3 · B08)
  • 前置条件:自建模板 tc-g3-02-<rand>,节点 mode = 'majority'、3 名审批人。
  • 操作步骤:① 走一单到该节点 active;② 第 1 人通过(查状态);③ 第 2 人通过(查状态); ④ 查第 3 人的 approval_node_approvers.status;⑤ 另起一单,前两人均拒绝(查状态)。
  • 预期结果:② 后节点 in_progress(1 票不足半数);③ 后节点立即 approved——approved 数 > 总数/2 即决,无需等第 3 人; ④ 第 3 人状态为 cancelled(节点已终态);⑤ 拒绝数达 ≥ 总数/2 时提前终止为 rejected,不再等待第 3 人。
  • 证据采集:各步请求/响应 JSON;节点与审批人状态输出(evidence/logs/g3-02-majority.json)。
  • 对应:PRD §8 第三条 · FR-9 · design §9.1 · 上会 MTG-04

G3-03 会签 ratio(threshold = 0.66):等全员处理完再判 ​

  • 分层:底线(门禁 #3 · B08)
  • 前置条件:自建模板 tc-g3-03-<rand>,节点 mode = 'ratio'、threshold = 0.66、3 名审批人。
  • 操作步骤:① 走一单到该节点 active;② 两人通过;③ 查节点状态;④ 第 3 人通过;⑤ 查节点状态; ⑥ 另起一单,两人通过、第 3 人拒绝(比例 2/3 ≈ 0.667)。
  • 预期结果:③ 节点仍 in_progress(ratio 必须等全员处理完,仍有 pending → pending); ④ 后节点 approved;⑥ 同样 approved(2/3 ≥ 0.66,判据为比例 ≥ threshold,PRD §10 Q4 不做取整策略); 另断言 threshold ∉ (0,1] 的模板被 TEMPLATE_INVALID 拒绝(design §11 C08)。
  • 证据采集:各步请求/响应 JSON;节点状态输出;非法 threshold 的模板校验响应(evidence/logs/g3-03-ratio.json)。
  • 对应:PRD §8 第三条 / §10 Q4 · FR-9 · design §9.1 / §11 C08

G3-04 veto_users 一票否决优先于一切 mode ​

  • 分层:底线(门禁 #3 · B08)
  • 前置条件:自建模板 tc-g3-04-<rand>,节点 mode = 'all'、3 名审批人,veto_users 含其中 1 人。
  • 操作步骤:① 走一单到该节点 active;② 其余 2 人先通过;③ veto_users 中的人拒绝;④ 查节点与审批单状态; ⑤ 另建模板,把 veto_users 指向不在该节点审批人内的用户,尝试提交。
  • 预期结果:③ 后节点立即 rejected(否决优先于 all 的"全部通过"判定),审批单 rejected、completed_at 非空; ④ 其余 pending 审批人置 cancelled; ⑤ 提交被拒 400 VETO_NOT_APPROVER,审批单停留 pending、节点表零行(展开后否决者必须是本节点审批人的子集)。
  • 证据采集:各步请求/响应 JSON;节点与审批人状态输出;VETO_NOT_APPROVER 响应与零节点断言 (evidence/logs/g3-04-veto.json)。
  • 对应:PRD §8 第三条 · FR-9 · design §8.4 / §9.1 / §10.1

4.4 门禁 #4 — 或签两条终态(B08) ​

G4-01 任一通过 → 节点 approved,其余 cancelled ​

  • 分层:底线(门禁 #4 · B08)
  • 前置条件:自建模板 tc-g4-01-<rand>,节点 strategy = 'any'、3 名审批人。
  • 操作步骤:① 走一单到该节点 active;② 第 1 人通过;③ 查节点与全部 3 名审批人的状态;④ 查审批单状态与 completed_at。
  • 预期结果:节点 approved;通过者 approved,其余 2 人全部 cancelled(不是 pending); 审批单流转到下一节点(存在时)或 approved(无下一节点时 completed_at 非空); 此后对已 cancelled 的审批人再提交操作,返回 409 NODE_STALE(页面已过期)。
  • 证据采集:各步请求/响应 JSON;节点与审批人状态输出;NODE_STALE 响应(evidence/logs/g4-any.json)。
  • 对应:PRD §8 第四条 / §4.5 · FR-9 · design §9.1 / §9.3 · 上会 MTG-05

G4-02 单人拒绝不终止;全员拒绝 → 节点 rejected ​

  • 分层:底线(门禁 #4 · B08)
  • 前置条件:同 G4-01。
  • 操作步骤:① 走一单到该节点 active;② 第 1 人拒绝;③ 查节点状态;④ 第 2 人拒绝;⑤ 查节点状态; ⑥ 第 3 人拒绝;⑦ 查节点、审批人、审批单与后续节点状态。
  • 预期结果:③ 与 ⑤ 节点仍 in_progress(单人拒绝不终止或签,这是 PRD §4.5 补死的规则); ⑦ 节点 rejected,其余 pending 审批人置 cancelled,后续全部 pending 节点置 cancelled, 审批单 rejected、completed_at 非空。
  • 证据采集:各步请求/响应 JSON;节点/审批人/审批单状态输出(evidence/logs/g4-any.json)。
  • 对应:PRD §8 第四条 / §4.5 · FR-9 · design §9.1 / §9.2

4.5 门禁 #5 — 槽位校验失败不留半成品(B05) ​

G5-01 提交校验三类失败:必填缺失 / 槽位未解析 / 审批人为空 ​

  • 分层:底线(门禁 #5 · B05)
  • 前置条件:自建模板 tc-g5-01-<rand>(含 required 字段、引用槽位 approver_1); 另备一份模板使某节点审批人槽位解析后为空(perm-api 角色反查返回空成员)。
  • 操作步骤: ① 创建审批单后,submit 时故意不传某个 required 字段; ② submit 时 defaults 与 payload 均不含 approver_1; ③ 用「角色反查返回空成员」的模板走一单并提交; ④ 每次失败后查 approval_orders / approval_nodes / approval_node_approvers / approval_actions / audit_logs。
  • 预期结果:① 400 REQUIRED_FIELD_MISSING 且 detail.fields[] 列出缺失字段 key; ② 400 SLOT_MISSING 且 detail.slots[] 列出未解析槽位名; ③ 400 APPROVER_EMPTY 且 detail.nodeKey 指明节点; ④ 三次失败后审批单 status 仍为 pending、submitted_at IS NULL、resolved_slots IS NULL, 节点表与审批人表零行,approval_actions 零行(无 submit 记录),audit_logs 无 submit_approval 行—— 失败路径整体回滚,不留半实例化审批单; 失败后修正入参再次 submit 可成功(证明失败未污染审批单)。
  • 证据采集:三类失败请求/响应 JSON;四张表的计数断言(evidence/logs/g5-01-validation.json); 修正后成功提交的响应。
  • 对应:PRD §8 第五条 · FR-6 · design §8.2 / §10.1 · 上会 MTG-06

G5-02 defaults 回落与条件引用槽位缺失走 default ​

  • 分层:底线(门禁 #5 · B05)
  • 前置条件:自建模板 tc-g5-02-<rand>,defaults 提供 app_tl;next.if 条件引用槽位 is_urgent。
  • 操作步骤:① 提交时 payload 与 submitterInput 均不含 app_tl,查 resolved_slots 与审批人快照; ② 提交时 payload 与 defaults 均不含 is_urgent,查节点序列; ③ 合并优先级验证:同一槽位在 defaults / payload / submitterInput 三处给不同值。
  • 预期结果:① app_tl 由 defaults 回落解析成功,resolved_slots 留档为回落值,节点审批人快照为展开后的用户; ② 条件引用槽位缺失时不报错,该条件判为不命中并走 next.default(不产生 SLOT_MISSING); ③ 生效值为 submitterInput > payload > defaults。
  • 证据采集:三次提交的请求/响应 JSON;resolved_slots 与节点序列断言(evidence/logs/g5-02-defaults.json)。
  • 对应:PRD FR-6 · design §8.2

G5-03 perm-api 不可达时回滚,不降级为空审批人 ​

  • 分层:底线(门禁 #5 · B05)
  • 前置条件:自建模板(tc-g5-03-<rand>),其节点审批人为 type: 'role',展开依赖 perm-api 角色反查; 不得使用种子模板 tpl_release_prod——它的 defaults 刻意全为 type: 'user'(见 §2), 用它验不出角色展开路径;perm-api 可临时停掉或改为不可达地址。
  • 操作步骤:① 停掉 perm-api(或临时指向不可达地址);② 提交该审批单;③ 查四张表;④ 恢复 perm-api 后重新提交。
  • 预期结果:② 返回 503 PERM_UNAVAILABLE(fail-closed,不降级为空审批人); ③ 审批单仍 pending、节点表零行、无 approval_actions 行;④ 恢复后同一审批单可成功提交。
  • 证据采集:失败与成功两次响应 JSON;四张表计数断言;服务端错误日志(脱敏) (evidence/logs/g5-03-perm-unavailable.json)。
  • 对应:PRD FR-8 · design §8.4 / §10.1 / §17

4.6 门禁 #7 — 门户主流程全程不碰 API(F03 / F02) ​

G7-01 收到链接 → 登录回跳 → 补充提交 → 审批 → 完成 ​

  • 分层:底线(门禁 #7 · F03)
  • 前置条件:nexo-approval-console 已部署并经壳入口可达;发起方与审批人均有可用测试账号; 审批门户未登录(清空站点存储);已存在一张 pending 审批单(由用例自建,payload.resource_url 指向可访问的业务页)。
  • 操作步骤:① 以发起方身份把 detailUrl 复制到浏览器新标签直接打开(模拟"业务方把链接放进自己的页面"); ② 完成登录(授权码流程),确认登录后回跳到原路由 /orders/:id 而非首页; ③ 在详情页填写 fill_by=submitter 字段并点提交; ④ 以审批人身份打开同一链接,点通过(必要时走完多个节点); ⑤ 回到业务方 resource_url 页面确认审批完成。
  • 预期结果:全程在浏览器内完成,不使用 curl / Postman / 任何 API 工具; ② 回跳后直接落在详情页(F01 的 redirect 参数生效),链接不因未登录而断链; ③ 提交后详情页状态变为 in_progress,节点区出现当前节点与审批人; ④ 每一步操作后详情页即时反映节点状态、时间线与审批意见; ⑤ 审批单终态为 approved,timeline 按时间顺序完整呈现 submit → approve 记录。
  • 证据采集:全程录屏(evidence/ui/g7-01-portal-closed-loop.mp4 或分步截图序列); 每一步的关键页面截图;浏览器 HAR 仅作辅助(不得替代录屏)。
  • 对应:PRD §8 第八条 · FR-16 · design §14.3 · 上会 MTG-07

G7-02 三角色读写矩阵九格(fill_by × 角色视角) ​

  • 分层:底线(门禁 #7 · F02/F03)

  • 前置条件:一张 pending 审批单(提交人未提交)与一张 in_progress 审批单; 三个账号:提交人、当前待处理审批人、持 approval.order.read.all 的观察者。

  • 操作步骤:① 以三个账号分别调 GET /api/orders/:id,逐字段记录 editable; ② 以三个账号分别打开同一详情页,记录每个字段的可编辑态; ③ 在 in_progress 单上重复 ①②,并把审批单推到终态后再做一次观察者视角。

    fillByviewerRole = submitter 且 status = pendingviewerRole = approver 且本人待处理其余一切情况
    systemfalsefalsefalse
    submittertruefalsefalse
    approverfalsetruefalse
  • 预期结果:九格的 editable 与上表逐格一致;前端不自行推断,只消费后端返回的 editable; 同一格在接口与页面两侧结论一致(后端单测 + 前端走查双侧覆盖,design §14.2); 终态单上一切字段 editable = false;观察者视角无任何可写字段。

  • 证据采集:三账号 × 三种状态下的详情响应 JSON(evidence/logs/g7-02-editable-matrix.json); 九格页面截图(evidence/ui/g7-02-*.png)。

  • 对应:PRD §8 · FR-15 · design §10.4 / §14.2 · 上会 MTG-08

G7-03 只读字段 DOM 改写后被提交前过滤 ​

  • 分层:底线(门禁 #7 · F02)
  • 前置条件:一张 pending 审批单,同时含 fill_by=submitter 与 fill_by=system 字段。
  • 操作步骤:① 在详情页用 DevTools 移除某个 fill_by=system 字段的 readonly / disabled 并改写其值; ② 点击提交;③ 查 approval_orders.form_data 与该次提交请求的 payload。
  • 预期结果:提交请求的 payload 只含 editable === true 的字段;被改写的 system 字段未进入 form_data (前端按 editable 过滤,design §14.2);后端即便收到该字段也不采纳(后端是正确性底线); 页面在提交后回显的值等于服务端值而非被改写值。
  • 证据采集:改写前后 DOM 截图;提交请求 payload(HAR 或 DevTools 复制); form_data 的 psql 断言(evidence/logs/g7-03-readonly-filter.json)。
  • 对应:PRD FR-15 · design §14.2

G7-04 待办与我发起的列表 + 未处理计数 ​

  • 分层:底线(门禁 #7 · F03)
  • 前置条件:当前审批人待办 ≥ 1 条;发起方「我发起的」≥ 1 条;另建一批 tc-g7-04-<rand> 前缀数据用于分页与筛选。
  • 操作步骤:① 打开「我的待审批」页,按状态筛选、翻页;② 打开「我发起的」页,按状态筛选、翻页; ③ 记录未处理计数(红点);④ 处理掉一条待办后刷新两页与计数。
  • 预期结果:两个列表均只返回与当前登录身份相关的数据(他人数据不出现); 分页参数生效且 limit 超过 APPROVAL_LIST_PAGE_SIZE_MAX=100 被拒或截断到上限; 状态筛选生效;未处理计数与待办列表中 pending 条数一致;④ 处理后计数减一、待办列表即时刷新。
  • 证据采集:两个列表的接口响应 JSON(含 total)与页面截图;GET /api/orders/pending/count 响应 (evidence/logs/g7-04-lists.json、evidence/ui/g7-04-*.png)。
  • 对应:PRD FR-17 · design §10.3 · F04 / F05

G7-05 未知字段类型前端降级不白屏 ​

  • 分层:底线(门禁 #7 · F02)
  • 前置条件:可自建一份 form_schema 含 type 为未知值的模板(模拟"模板来自更新版本的后端")。
  • 操作步骤:① 用该模板建单并打开详情页;② 观察该字段的渲染与页面其余部分。
  • 预期结果:未知类型字段渲染为只读文本 + 提示,不白屏、不抛前端异常;页面其余字段与节点区正常渲染; 该字段 editable = false。
  • 证据采集:页面截图与浏览器控制台(无未捕获异常);详情响应 JSON(evidence/ui/g7-05-*.png)。
  • 对应:PRD FR-15 · design §14.1

4.7 底线补充(PRD §8 点名、父任务门禁表未单列门禁) ​

S01 并发:同一会签节点两人同时通过,节点只流转一次 ​

  • 分层:底线(PRD §8 点名 · B09)
  • 前置条件:自建模板 tc-s01-<rand>,单节点 countersign(mode = 'all')或两节点串联; 两名审批人均为当前节点待处理人;并发脚本就绪(evidence/scripts/s01-concurrent-approve.ts)。
  • 操作步骤:① 走一单到该节点 active;② 脚本在同一毫秒窗口内由两名审批人同时调 POST /api/orders/:id/actions (action = 'approve',各自带自己的 nodeId),重复 ≥20 轮; ③ 每轮记录两个响应的状态码与节点 seq;④ 查 approval_nodes(UNIQUE (order_id, seq))与 approval_actions。
  • 预期结果:每轮节点只流转一次——不存在同一 seq 出现两行、也不存在跳节点; approval_actions 中每轮恰 2 条(两名审批人各一条),无重复决策行; 两响应的语义为「一个生效 + 一个按重放或 NODE_STALE 收敛」,不出现两个请求都触发流转; 20 轮全部满足(整单一把锁 FOR UPDATE 串行化的直接结果,design §9.2)。
  • 证据采集:脚本与原始输出(每轮两响应的状态码与耗时、evidence/logs/s01-concurrency.json); approval_nodes / approval_actions 断言输出。
  • 对应:PRD §6 幂等与并发 · FR-10 · design §9.2 · 上会 MTG-09

S02 幂等:重复提交同一操作不产生第二条 ApprovalAction ​

  • 分层:底线(PRD §8 点名 · B09)
  • 前置条件:单节点或两节点审批单;同一审批人账号。
  • 操作步骤:① 审批人对当前节点通过一次,记录响应; ② 用完全相同的请求体(同 nodeId、同 action)连发 3 次; ③ 对最后一个节点的通过请求在审批单已 approved 后再重放一次; ④ 查 approval_actions。
  • 预期结果:② 三次均 200 且返回当前详情(重放语义),不写任何新记录; ③ 终态单的重放同样 200(重放判断先于终态检查,design §9.2),不是 409 ORDER_FINALIZED; ④ approval_actions 中 (node_id, actor, action) 对该决策恰一行; 另断言「同一 nodeId 的不同审批人各自操作」不被误判为重放。
  • 证据采集:四次请求/响应 JSON;approval_actions 计数与部分唯一索引断言 (evidence/logs/s02-idempotency.json)。
  • 对应:PRD §6 幂等 · FR-10 · design §4.2 / §9.2 / §10.1

5. 可顺延用例(门禁 #6、#8、#9) ​

主责任务 B10 / B11 / B13 / B15 / B16(design §15)。允许顺延至验收会前一晚; 顺延则对应门禁转下期,本文档同步把该组标记为「下期」。

5.1 门禁 #6 — 审计完整性(B11) ​

G6-01 一单走完后 action / audit 条数与操作数一致 ​

  • 分层:可顺延(门禁 #6 · B11)
  • 前置条件:自建模板 tc-g6-01-<rand>,两节点串联(首节点 single,次节点 countersign all 两人); 记录该单创建前的 approval_actions 与 audit_logs 基线计数。
  • 操作步骤:① 创建 → 提交 → 第 1 节点通过 → 第 2 节点两人通过(另起一单改为拒绝); ② 全程记录每一次状态类操作(不含评论);③ 按 order_id 查两张表;④ 另发一条评论。
  • 预期结果:通过路径 approval_actions 条数 = 操作数(submit 1 + approve 3 = 4 条), 每条 (node_id, actor, action) 唯一、created_at 单调; audit_logs 中该单相关行覆盖 create_order / submit_approval / 每个 approve / reject,条数与操作数一致; ④ 评论只落 approval_comments,不写 approval_actions、不写 audit_logs 的动作码(design §12 / §7.2 表 7 注释); 拒绝路径:reject 同样一条 approval_actions + 一条 audit_logs。
  • 证据采集:全流程请求/响应 JSON;两张表按 order_id 的 psql 输出与计数比对 (evidence/logs/g6-01-audit.json)。
  • 对应:PRD §8 第七条 · FR-12 · design §7.2 / §12

G6-02 append-only 与快照冻结的 DB 触发器兜底 ​

  • 分层:可顺延(门禁 #6 · B11)

  • 前置条件:只读 psql 之外,验收环境允许执行受控的直连写语句(仅用于触发拒绝,不落任何数据); 已有一张完成态审批单。

  • 操作步骤:① 直连执行 UPDATE audit_logs SET detail = '{}'::jsonb WHERE id = <任一 id>; ② 直连执行 DELETE FROM approval_actions WHERE id = <任一 id>; ③ 直连执行 UPDATE approval_orders SET template_snapshot = '{}'::jsonb WHERE id = <任一 id>; ④ 直连执行 UPDATE approval_orders SET source_type = 'inline' WHERE id = <任一 id>; ⑤ 检索接口面:对 approval_actions / audit_logs 尝试 PATCH / PUT / DELETE 路由; ⑥ 反证(触发器是否为承重件):在单事务内 BEGIN → DROP TRIGGER trg_actions_append_only ON approval_actions → 重跑一次①的同类 UPDATE → ROLLBACK(Postgres 的 DDL 可回滚,不留任何残留)→ 事务外再跑一次同类 UPDATE。

  • 预期结果(两半强度不同,不得并列为同等证据):

    半判定强度
    触发器半(①—④)① ② 报 APPEND_ONLY_VIOLATION,③ ④ 报 IMMUTABLE_FIELD;四者均两表行数与内容零变化真实通过——这是门禁 #6 的承重断言。它证明绕过应用层直连也改不动,即"写了 update 路径也会被库拒"(design §4.1 的原话)
    路由半(⑤)接口面不存在更新与删除路径(404 NOT_FOUND),全仓检索无对应写语句空集通过——approval_actions / audit_logs 按设计就没有 PATCH/PUT/DELETE 路由,该断言恒成立,无法区分"被加固过"与"从来没有过这条路径";它恰好就是 design §4.1 明确宣告不充分的那一层("不是'代码里没写 update 路径'")

    ⑥ 反证:DROP TRIGGER 后同一 UPDATE 成功执行(证明拒绝确实来自触发器、断言是承重的,不是恒真); ROLLBACK 后同一 UPDATE 再次被拒(触发器恢复、无残留);报告里给出 DROP TRIGGER 前后的成功/失败对照。

  • 证据采集:四条 psql 语句的报错原文(evidence/logs/g6-02-append-only.txt); 拒绝前后两张表的行数与 md5 校验;⑥ 的 DROP TRIGGER 前后对照输出与 ROLLBACK 后的残留断言; 路由检索结果;代码检索结果。 §10 该行须分半记录:触发器半 = 通过,路由半 = 空集通过,不得合并成一条"通过"。

  • 对应:PRD §8 第七条 · FR-12 · design §4.1 / §7.3 / §10.1

5.2 门禁 #8 — 零硬编码管理员(B13) ​

G8-01 全仓检索无 requireAdmin、无管理员白名单 ​

  • 分层:可顺延(门禁 #8 · B13)
  • 前置条件:nexo-approval-api 主分支工作区干净。
  • 操作步骤:① 在 nexo-approval-api 全仓(含 src/ / drizzle/ / 脚本 / 配置)检索 requireAdmin、 isAdmin、ADMIN_USERS、ADMIN_IDS、adminIds、superAdmin、SUPERADMIN、白名单; ② 检索硬编码用户标识常量(形如 u_admin / admin@ 的字面量); ③ 检索 X-Nexo-* 之外的自定义特权头; ④ 确认权限点常量的唯一真源:5 个 approval.* code 的字面量在仓内只允许出现在权限点定义文件一处, 中间件、业务模块与初始化脚本一律引常量(拼错一个 code 的表现是「接口对所有人 403」或 「初始化时多建了一个没人用的点」,两者都不报错)。
  • 预期结果:① 零命中(PERM_SUPERADMIN_USER_ID 一类仅存在于初始化脚本与 perm-api 侧的引导常量不算命中, 但需在报告中列出并说明用途);② 审批平台内零硬编码用户标识;③ 无绕过 perm-api 的特权通道; 初始化脚本只做幂等的权限点与角色建立,不授予任何用户特权——只建角色、不授人(design §13); ④ 权限点字面量仅一处定义,重复即不通过。
  • 证据采集:检索命令与命中结果(evidence/logs/g8-01-grep.txt);初始化脚本片段引用。
  • 对应:PRD §8 第八条 · FR-14 · design §13

G8-02 无权限管理接口被拒,有权限放行 ​

  • 分层:可顺延(门禁 #8 · B13)
  • 挂载依赖(必读):本条测的是真实端点,需 B02 / B03 / B12 把 authorizeCaller 挂到各自的 模块 index.ts 上之后才能执行——B13 的交付面是「可被挂载的中间件 + 客户端 + 权限点注册」, 挂载由各业务模块自己做(B13 的任务契约禁止其改 src/routes/index.ts)。 中间件行为本身由 B13 的演示性挂载集成测试覆盖(测试文件内起一条 /api/_demo/* 挂上中间件, 断言 403+denyReason / 503 / 放行三态),该证据可在模块挂载落地前先引用; 但真端点级别的本用例须等挂载完成,未挂载时记为「阻塞」而非「失败」。
  • 前置条件:1 个持合法 Bearer JWT 但无任何 approval.* 权限点的普通用户;1 个平台管理员账号; 上述管理路由已完成挂载。
  • 操作步骤:① 以普通用户调 POST /api/templates、PATCH /api/templates/:id、POST /api/resource-types、 GET /api/audit-logs、POST /api/templates/validate、GET /api/orders/:id(非参与人的单); ② 以平台管理员调同一组接口;③ 以普通用户调自己的 GET /api/orders/pending 与自己的审批单详情。
  • 预期结果:① 均 403 FORBIDDEN 且带 denyReason(不是 500、不是静默放行); ② 全部按权限点判定通过(approval.template.manage / approval.resource-type.manage / approval.audit.read / approval.template.read); ③ 自己的审批单与列表不走权限点、天然可见(design §13「不走 perm-api 的两类接口」); 「合法 JWT 即放行」在审批平台不成立(无权限的合法 JWT 被拒即为反证)。 ④ 中间件语义断言(按 authorizeCaller 实现核对,勿只断言 403):一次请求并发判完全部 5 个点, 缺哪个点就报哪个点的 denyReason——须验证三种原因可区分: 主体失活 → subject_inactive;被显式拒绝 → deny_hit;压根没授过 → no_grant。 三者处置动作完全不同,压成一句「无权限」即不算通过。 ⑤ denyReason 原样透传 SDK 取值,不自行造枚举;且被拒时日志里的人身份标识已脱敏, 掩码格式为前 8 字符 + ***(即 id.slice(0,8) + '***'), 日志中出现完整 external_id 即为不通过。

    断言掩码时按「前 8 字符 + 三个星号」比对,不要按尖括号占位符 <前 8 位>*** 的字面形状断言—— 后者只是本文档的说明写法,不是实现输出。

  • 证据采集:全部请求/响应 JSON(evidence/logs/g8-02-authz.json)。
  • 对应:PRD §8 第八条 · FR-14 · design §13 / §10.1

G8-03 perm-api 停掉时管理接口拒绝、审批操作正常 ​

  • 分层:可顺延(门禁 #8 · B13)
  • 挂载依赖(必读):同 G8-02——管理接口一侧须待 B02 / B03 / B12 完成 authorizeCaller 挂载; 未挂载时记为「阻塞」。但审批操作一侧(步骤③)不依赖挂载,可先单独执行—— 它恰恰是要证明「审批链路不经过 perm-api」,与挂载与否无关。
  • 前置条件:一张 in_progress 审批单,当前节点审批人待处理;perm-api 可临时停掉(或指向不可达地址); 管理路由已完成挂载(仅 ② 需要)。
  • 操作步骤:① 停掉 perm-api;② 调管理接口(POST /api/templates、GET /api/audit-logs); ③ 调审批操作(POST /api/orders/:id/submit 于另一张 pending 单、POST /api/orders/:id/actions、 POST /api/orders/:id/withdraw);④ 恢复 perm-api 后重跑 ②。
  • 预期结果:② 均 503 PERM_UNAVAILABLE(fail-closed); 必须是 503 而非 403——实现里「问不到」先于「有没有」判定:perm-api 不可达时 5 个点会同时返回 denyReason: 'exception',若先走「有没有」分支,用户会看到 403「你没权限」而事实是权限平台挂了, 会照着提示去申请一个自己本就持有的权限。故断言 503 + PERM_UNAVAILABLE,断言到 403 即为不通过; 边界(易漏,必测):5 个点中任意一个是 exception 即整体 503, 即使本端点要求的那一个点恰好返回 allow: true 也不得放行——权限平台不可达时不许任何管理接口凭半份答案通过。 构造方式:让 perm-api 对其中 4 个点正常响应、对另 1 个点超时(或先全量正常取得 allow,再停服重放并断言仍 503); 若实现只在「被要求的点」为 exception 时才 503,本条即为不通过; ③ 审批操作全部正常(提交、通过/拒绝、撤回均按业务规则生效,状态正确流转)—— 这是把审批链路与权限平台解耦的直接收益(design §13 / §17); ④ 恢复后管理接口回归正常(反证 ② 不是恒拒)。
  • 证据采集:停服前后各接口的请求/响应 JSON;服务端错误日志(脱敏); 审批单状态流转的 psql 断言(evidence/logs/g8-03-perm-down.json)。
  • 对应:PRD §8 第八条 · FR-14 · design §13 / §17

G8-04 详情越权:非参与人须持 approval.order.read.all ​

  • 分层:可顺延(门禁 #8 · B13)
  • 前置条件:一张与测试账号无关的审批单;一个无 approval.order.read.all 的普通用户;一个持该权限的观察者。
  • 操作步骤:① 以无关普通用户按 ID 遍历 GET /api/orders/:id; ② 以持 approval.order.read.all 的账号调同一接口; ③ 以该单的提交人与任一节点审批人(含已处理、待处理、未轮到)分别调用。
  • 预期结果:① 403 FORBIDDEN(不设这条,任意登录用户按 ID 遍历就能读全部审批单 payload); ② 200 且 viewerRole = 'observer';③ 参与人均 200,viewerRole 分别为 submitter / approver; viewerRole 由后端判定,前端不推断(design §10.4)。
  • 证据采集:全部响应 JSON(evidence/logs/g8-04-detail-scope.json)。
  • 对应:PRD FR-14 · design §10.4 / §13

5.3 门禁 #9 — 存量接入债还清(B15 / B16) ​

G9-01 管理接口不再「合法 JWT 即放行」 ​

  • 分层:可顺延(门禁 #9 · B15 / B16)

  • 前置条件:两个服务的 test 实例已部署接入 SDK 鉴权客户端; SDK 依赖已升到 0.3.0-alpha.1(nexo-auth / nexo-im-api 当前锁在 0.1.0,该版本无 createPermissionCheckClient,不升级则本条无法执行,见 §2);准备 1 个持合法 Bearer JWT 但无对应管理权限的账号, 与 1 个持权限的账号。

  • 被测面(两侧不同,实测确认后固化,勿照抄对方):

    服务管理侧接口是否需加判定
    nexo-account(B15)POST /api/users(开户)、GET /api/users(花名册)、GET /api/users/{id}(详情)、PATCH /api/users/{id}(改资料)、POST /api/users/{id}/tickets(重发激活/重置链接)、POST /api/users/{id}/disable、POST /api/users/{id}/enable、POST /api/users/batch-import(CSV 导入)是,共 8 条
    nexo-im-api(B16)不存在独立管理侧接口。全部 Bearer 面均为「本人/本团队」自服务:/me、/users(通讯录,普通用户页面)、/conversations*、/messages/{id}/recall(仅本人发送)、/rtc/devices(不含调用方自己)、/ws否,交付机制本身、挂 0 条路由,见下方判定

    im-api 的口径必须写清:/users 是通讯录(nexo-im-pc 的 users() 直接消费),不是管理接口; 会话、消息、撤回、/ws 一律自服务且被 issue #85 的实现约束明确禁止改造。给其中任何一条加 perm-api 判定 都会打断通讯录或消息链路。因此 B16 的交付面是鉴权机制 + 回归证明,不是「给某组端点加门」: 机制(中间件 / 客户端 / 权限点定义)必须存在且有测试覆盖,但挂载点数为 0。 核验判据是挂载点而非「有没有写中间件」——写了但没挂是正确形态, 挂了任一条自助端点才是缺陷(grep -rn authz src/routes/index.ts src/routes/*/routes.ts src/routes/*/index.ts 应零命中)。

  • 操作步骤:① 以无权限账号调用 nexo-account 上表 8 条管理接口; ② 以持权限账号调用同一组接口;③ 以无权限账号调用 nexo-im-api 全部 Bearer 面,断言行为与改造前一致(不加判定); ④ 断言两侧的拒绝语义与 SDK 契约一致。

  • 预期结果:① nexo-account 8 条管理接口均 403 FORBIDDEN(附 denyReason)——该侧「合法 JWT 即放行」不再成立; ② 持权限账号全部成功(反证 ① 不是恒拒); ③ nexo-im-api 全部 Bearer 面不引入 perm-api 判定(无新增 403、无新增延迟), perm-api 不可达时这些接口照常工作——im-api 侧的偿还判定是「管道就位且不误伤自服务面」,不是「新增拒绝」; ④ 两侧均经 nexo-backend-sdk 鉴权客户端;nexo-account 侧 perm-api 不可达时 fail-closed(PERM_UNAVAILABLE)。 ⑤ 边界:nexo-account 的用户自助接口(/api/users/me)与认证流程本身(/login /token /authorize JWKS /internal/*) 不在判定面内,改造前后行为一致(见 G9-02)。 ⑥ 本条 im-api 侧记为「空集通过」,不记为普通「通过」:该服务不存在管理侧接口,故"管理接口不再合法 JWT 即放行" 这一判定在 im-api 侧无被测对象——被满足的是「不新增耦合」而非「接口被加固」。 预跑时必须在 evidence/logs/g9-01-im-api.json 与 §10 该行备注里写明「被测面为空的原因」, 禁止把空集通过与 nexo-account 的真实通过合并成一行结论(两者在结果表里长得一样,不标注会被误读为 「im-api 也有一个管理接口被加固了」)。

  • 证据采集:两侧请求/响应 JSON(evidence/logs/g9-01-account.json、g9-01-im-api.json); 两侧代码检索结果(无 requireAdmin / 无白名单常量);nexo-im-api 全量路由清单与逐条「是否加判定」的判定表。

  • 对应:PRD §8 第九条 · FR-19 · nexo-auth#83 · nexo-im-api#85 · design §16 第 4 条

G9-02 登录链路回归无行为变化 ​

  • 分层:可顺延(门禁 #9 · B15)
  • 前置条件:改造前的登录链路基线证据(0920 / 0913 归档)可取;nexo-account 与 IM 已就绪。
  • 操作步骤:① 走完整登录链路:H1 派生 → 登录 → 授权码 + PKCE → 兑换令牌 → JWKS 本地验签; ② 用签发令牌访问 IM 与审批门户;③ 断言凭据与令牌载荷形状。
  • 预期结果:链路各步响应形状与改造前一致;令牌可被资源服务本地验签通过;令牌载荷字段(sub / exp / iss)无变化; 登录、刷新、登出行为与改造前逐项一致——管理接口的鉴权改造不得影响登录链路。
  • 证据采集:链路各步请求/响应 JSON(脱敏);改造前后对照表(evidence/logs/g9-02-login-regression.json)。
  • 对应:PRD §8 第九条 · FR-19

G9-03 消息链路回归无行为变化 ​

  • 分层:可顺延(门禁 #9 · B16)
  • 前置条件:≥2 个测试账号可登录 IM 桌面端或网页端;改造前的消息链路基线可取;perm-api 可临时停掉。
  • 操作步骤:① 建立 WebSocket 连接;② 会话列表与历史消息拉取;③ 发送与接收文本消息;④ 撤回、未读计数、presence; ⑤ 断言按需拉取用户资料路径(GET /internal/users?ids=... 与通讯录 /users); ⑥ 停掉 perm-api,重复 ①②③④。
  • 预期结果:连接建立与重连正常;会话列表、历史消息、收发消息、撤回、未读数、presence 与改造前一致; 按需拉取用户资料与通讯录行为不变; ⑥ perm-api 停掉时消息收发与 WebSocket 全部正常(nexo-im-api#85 的验收标准点名项)—— 消息链路不因权限平台故障而中断,也不被其拖慢;改造前后逐项对照无行为变化。
  • 证据采集:关键接口响应 JSON 与 WebSocket 帧记录;改造前后对照表;perm-api 停机期间的消息链路证据 (evidence/logs/g9-03-im-regression.json)。
  • 对应:PRD §8 第九条 · FR-19 · nexo-im-api#85

5.4 顺延补充(PRD §8 点名、父任务门禁表未单列门禁) ​

S03 撤回与业务方取消两条终态区分 ​

  • 分层:可顺延(PRD §8 点名 · B10)
  • 前置条件:两张 in_progress 审批单(多节点串联,当前节点非首节点),一张由发起方撤回、一张由业务方取消; HMAC 内部通道可用(取消走 /internal/*)。
  • 操作步骤:① 发起方调 POST /api/orders/:id/withdraw;② 业务方以 HMAC 身份调 POST /internal/orders/:id/cancel; ③ 分别查审批单、当前节点、后续节点、全部审批人的状态;④ 对两张终态单重复调用撤回与取消。
  • 预期结果:① 审批单 withdrawn、② 审批单 cancelled——两条终态严格区分(PRD §4.6); 两者均执行同一段级联逻辑:当前节点与后续全部节点置 cancelled,未处理审批人置 cancelled; approval_actions 分别落 withdraw / cancel 各一条(node_id 为空,design §7.2); ④ 终态单的 cancel 幂等返回当前状态、withdraw 返回 409 ORDER_FINALIZED; 另断言「非发起方调 withdraw」被拒 403 FORBIDDEN。
  • 证据采集:四次请求/响应 JSON;节点与审批人状态输出(evidence/logs/s03-withdraw-cancel.json)。
  • 对应:PRD §8 第六条 · FR-10 / FR-11 · design §9.2 / §10.2 / §10.3

6. 门禁 #10 — 预跑归档就绪 ​

G10-01 证据目录齐备且与用例编号一一对应 ​

  • 分层:底线(门禁 #10 · 本任务)
  • 前置条件:09-26 预跑已执行完毕。
  • 操作步骤:① 列出 evidence/scripts/、evidence/logs/、evidence/ui/ 全部文件; ② 与 §1.2 用例清单逐条对账;③ 检查脱敏。
  • 预期结果:§4—§6 的每条用例都有对应证据文件(命名含用例编号); 接口类用例的证据是请求/响应 JSON,UI 类用例的证据是截图或录屏; evidence/scripts/ 中的脚本可重复执行(幂等、自建自清); 无 HMAC 密钥、无完整 Token、无真实业务数据泄漏; tc.md §10 已回填每条用例的结果、证据链接与阻塞项解除条件。
  • 证据采集:evidence/ 目录树输出与对账表。
  • 对应:门禁 #10 · 本文档 §9 / §10

G10-02 09-27 验收会上会用例全部通过 ​

  • 分层:底线(门禁 #10 · 本任务)
  • 前置条件:09-26 预跑通过;test 环境在验收会当日保持可用;§3 的 9 条已准备就绪。
  • 操作步骤:按 §3 顺序执行 9 条上会用例,逐条现场判定;N01—N03 复核预跑证据。
  • 预期结果:9 条零失败;三项硬阈值证据齐备且达标; 预跑中标记为阻塞的用例已在会上给出解除结论或明确转入下期(不得静默跳过)。
  • 证据采集:会上记录(时间、执行人、结论);现场产生的请求/响应 JSON 与截图。
  • 对应:门禁 #10

7. 三项硬阈值取数口径(N01—N03,实测不估算) ​

本节三条必须实测取数,不得估算;数据由 09-26 预跑阶段采集,会上只复核证据,不现场重测。 采集脚本与原始样本随证据归档(evidence/scripts/、evidence/logs/)。 计时一律取服务端毫秒时间戳差(handler 内 performance.now()),不采客户端秒表; 列表首屏额外采集浏览器侧 performance 口径作为补充,两者都报。 采集前须确认共享库无并发写负载(pg_stat_activity 中 state='active' 的非 psql 连接数为 0)。

N01 审批单创建 API p99 < 1s ​

  • 取数口径:POST /internal/orders 服务端耗时;三路各采样 ≥200 次(合计 ≥600 次), 数据集固定(自建模板 + 自建 resource_type + 固定 payload),预热 50 次不计入; 报告中位数 / p50 / p95 / p99 / max,并分路报告(resource_type / template_id / inline)。
  • 阈值:p99 < 1s(PRD §6)。
  • 证据采集:压测脚本与原始样本 JSON(evidence/logs/n01-create-p99.json)。
  • 对应:PRD §6 · design §17

N02 审批操作 API p99 < 1s ​

  • 取数口径:POST /api/orders/:id/actions 服务端耗时;为每个样本预置一张待处理单(避免样本间相互影响), 采样 ≥200 次,覆盖 approve 与 reject 两类;报告中位数 / p50 / p95 / p99 / max。 该窗口包含整单锁获取、判定、approval_actions 与 audit_logs 写入。
  • 阈值:p99 < 1s(PRD §6)。
  • 证据采集:压测脚本与原始样本 JSON(evidence/logs/n02-action-p99.json)。
  • 对应:PRD §6 · design §9.2 / §17

N03 两个列表首屏 p99 < 2s ​

  • 取数口径:GET /api/orders/pending 与 GET /api/orders/submitted 各采样 ≥200 次(第一页,limit = 20); 数据集:单个测试用户 ≥500 条待办 + ≥200 条我发起的(脚本构造,前缀 tc-n03-<rand>,采完清理); 同时记录两组口径:① 接口服务端耗时;② 浏览器侧首屏(导航开始 → 列表渲染完成,performance 打点)。 报告中位数 / p50 / p95 / p99 / max,并断言查询走专用索引(idx_approver_pending / idx_orders_submitter),无 JSONB 扫描。
  • 阈值:两个列表首屏 p99 < 2s(PRD §6)。
  • 证据采集:压测脚本与原始样本 JSON;EXPLAIN 输出(evidence/logs/n03-lists-p99.json)。
  • 对应:PRD §6 · design §7.2 / §17
  • 附加观测(不计入门禁三项):详情页首屏 < 2s(PRD §6)随 G7-01 录屏顺带记录首屏打点。

8. 需求追踪矩阵 ​

需求覆盖用例
FR-1 模板管理G1-02、G8-02
FR-2 模板配置强校验G1-02、G3-03(threshold 边界)
FR-3 资源类型注册与绑定G1-01、G8-02
FR-4 三种发起方式同构装配G1-01、G1-02
FR-5 第一阶段槽位预填G1-01、G1-03
FR-6 第二阶段槽位解析与校验G2-03、G5-01、G5-02
FR-7 条件分支求值与节点实例化G2-01、G2-02、G2-03
FR-8 角色审批人解析与快照G1-01、G3-04、G5-03
FR-9 判定引擎三策略G3-01—G3-04、G4-01、G4-02
FR-10 审批操作与三层状态机G4-01、S01、S02、S03
FR-11 业务方取消S03
FR-12 审计日志 append-onlyG6-01、G6-02
FR-13 查询与列表G7-04、G8-04
FR-14 平台自身接口鉴权G8-01—G8-04
FR-15 动态表单渲染器与读写矩阵G7-02、G7-03、G7-05
FR-16 审批详情页G7-01、G7-02
FR-17 待办与我发起的G7-04
FR-19 存量服务接入 SDK 鉴权G9-01—G9-03
非功能:性能硬阈值N01—N03
非功能:幂等S02
非功能:并发(整单一把锁)S01
非功能:解耦(perm-api 停机)G8-03、G5-03

9. 证据目录约定 ​

  • evidence/scripts/:造数、并发(S01)、压测(N01—N03)、校验矩阵(G2-03)脚本,可重复执行、自建自清
  • evidence/logs/:结构化取数(JSON / 纯文本),文件名按用例编号(如 g1-01-three-paths.json、n01-create-p99.json)
  • evidence/ui/:UI 类用例的截图与录屏(如 g7-01-portal-closed-loop.mp4、g7-02-submitter.png)
  • 每条用例的证据文件命名必须包含用例编号,与 §1.2 一一对应(G10-01 按此对账)
  • 敏感信息脱敏:HMAC 密钥不入库,Bearer Token 只留前 8 位,业务 payload 用测试值
  • 截图只作 UI 类用例的证据;接口类用例一律以请求/响应 JSON 为准,不接受截图代替

10. 预跑结果(T01 于 09-26 回填) ​

预跑报告见 tc-evidence-preflight.md(预跑时产出)。本节在预跑完成后回填逐条结论、证据链接与阻塞项解除条件; 预跑前保持空白,不得预先填写结论。

编号分层结果证据备注
G1-01底线通过(装配分叉点代码检索(tc.md §4.1 的证据采集项)、创建阶段 + 提交阶段(三路逐字段比对、节点与审批人表比对、装配分叉点))g1-01-assembly-branch.json、g1-01-three-paths.json—
G1-02底线通过g1-02-mutual-exclusion.json—
G1-03底线通过g1-01-three-paths.json—
G2-01底线通过(端到端节点序列落库(is_urgent=true))g2-branches.json—
G2-02底线通过(端到端节点序列落库(is_urgent=false,node_emergency 零行))g2-branches.json—
G2-03底线通过(步骤①八种 operator 矩阵 + 短路顺序(纯函数)、步骤①矩阵 + 步骤②③端到端两单)g2-03-operators.json—
G3-01底线通过(判定逻辑(穷举 64 组合)、端到端状态流转(提交物化 + 逐步审批))g3-g4-decision-engine.json—
G3-02底线通过(判定逻辑(穷举 64 组合))g3-g4-decision-engine.json—
G3-03底线通过(判定逻辑(穷举 64 组合;C08 threshold 校验未验)、端到端 ratio 状态流转)g3-g4-decision-engine.json—
G3-04底线通过(判定逻辑(否决优先于一切 mode)、端到端否决(其余人先通过、否决者拒绝 → 立即 rejected))g3-g4-decision-engine.json—
G4-01底线通过(判定逻辑(穷举 64 组合))g3-g4-decision-engine.json—
G4-02底线通过(判定逻辑(穷举 64 组合))g3-g4-decision-engine.json—
G5-01底线通过(仅步骤③审批人为空(APPROVER_EMPTY)、步骤①②③三条校验(错误码 + detail 载荷)+ 端到端四表零行回滚)g5-slot-validation.json—
G5-02底线通过(纯函数层(resolveSlots 三层优先级 / 整值替换 / 类型保留)、端到端 resolved_slots 落库与 defaults 回落)g5-slot-validation.json—
G5-03底线通过(perm-api 不可达 → PERM_UNAVAILABLE(fail-closed)、perm-api 不可达 fail-closed + 端到端四表零行)g5-slot-validation.json—
G7-01底线阻塞(UI 全链路)blocker-console-absent.json阻塞:nexo-approval-console 仓库为空(三个分支各仅 README)
G7-02底线阻塞(UI 侧、UI 侧(页面截图 / 九格页面走查));通过(接口侧九格 editable 矩阵)blocker-console-absent.json、g7-02-editable-matrix.json阻塞:nexo-approval-console 仓库为空
G7-03底线阻塞(UI 侧(DOM 改写后被过滤))blocker-console-absent.json阻塞:nexo-approval-console 仓库为空
G7-04底线阻塞(UI 侧(列表页与红点))blocker-console-absent.json阻塞:nexo-approval-console 仓库为空(接口侧已通过)
G7-05底线阻塞(UI 侧(未知字段类型降级))blocker-console-absent.json阻塞:nexo-approval-console 仓库为空
S01底线通过(20 轮并发(前置状态由真实提交链路产生))s01-concurrency.json—
S02底线通过(幂等重放(前置状态由真实提交链路产生))s02-idempotency.json—
G6-01可顺延通过(审计完整性(整条动作序列含 submit 均由生产代码写入))g6-01-audit.json—
G6-02可顺延通过(触发器半(①②③④ + ⑥ 反证):直连 UPDATE/DELETE 被库拒绝);空集通过(路由半(⑤):无 PATCH/PUT/DELETE 路由,断言恒成立,不构成证据)g6-02-append-only.json空集通过:路由半(⑤):无 PATCH/PUT/DELETE 路由,断言恒成立,不构成证据(须写明原因,勿与真实通过合并)
G8-01可顺延通过(全仓静态检索(零硬编码管理员 / 权限点字面量唯一真源 / 初始化脚本不授人))g8-01-grep.json—
G8-02可顺延通过(真端点 403+denyReason / 管理员放行 / 三种 denyReason 可区分 / 掩码格式)g8-02-authz.json—
G8-03可顺延通过(管理接口 503 非 403 / 边界半(进程内直测)/ 审批链路解耦(含真实审批执行)/ 恢复后回归)g8-03-perm-down.json—
G8-04可顺延通过(非参与人 403 / 观察者 observer / 提交人 submitter / 节点审批人 approver)g8-04-detail-scope.json—
G9-01可顺延通过(nexo-account 侧(B15):8 条管理接口无权限 403 / 有权限放行);空集通过(nexo-im-api 侧(B16):被测面为空——该服务不存在管理侧接口(会话管理已迁至认证中心),故「管理接口不再合法 JWT 即放行」无被测对象;被满足的是「不新增耦合」而非「接口被加固」)g9-01-account.json、g9-01-im-api.json空集通过:nexo-im-api 侧(B16):被测面为空——该服务不存在管理侧接口(会话管理已迁至认证中心),故「管理接口不再合法 JWT 即放行」无被测对象;被满足的是「不新增耦合」而非「接口被加固」(须写明原因,勿与真实通过合并)
G9-02可顺延通过(登录链路真实链路(H1 派生 → 登录 → 授权码 + PKCE → 换票 → JWKS 本地验签))g9-02-login-regression.json—
G9-03可顺延通过(消息链路回归(静态半段 + 动态半段;含 perm-api 不可达时的对照))g9-03-im-regression.json—
S03可顺延通过(两条终态级联(前置状态由真实提交链路产生))s03-withdraw-cancel.json—
N01底线通过(三路各 200 次采样(客户端观测墙钟;平台不暴露服务端计时,见文件头口径说明))n01-create-p99.json—
N02底线通过(200 次采样(approve/reject 各 100);前置单由真实提交链路产生,被测窗口为整单锁+判定+两表写入)n02-action-p99.json—
N03底线通过(两列表各 200 次采样 + EXPLAIN 索引断言(走专用索引、无 JSONB 扫描))n03-lists-p99.json—
G10-01底线通过(证据目录齐备、命名含编号、格式合规、脱敏、脚本可重复执行)g10-01-evidence-inventory.json—
G10-02底线阻塞(会上执行 9 条上会用例)g10-01-evidence-inventory.json阻塞:MTG-07 / MTG-08 依赖的门户不存在(nexo-approval-console 仓库为空);MTG-01…MTG-06、MTG-09 的前置已具备