Skip to content

2026-09-20 迭代 TC 预跑报告(T01) ​

  • 目标用例集:tc.md §4–§6
  • 被测对象:nexo-perm-api 集成分支 integration/2026-09-20(worktree /tmp/nexo-proj/wt-integration), 服务以 node dist/index.js 监听 3002,连真实 PostgreSQL(容器 nexo-perm-pg,库 nexo_perm)
  • 工作分支:feat/t01-acceptance-prerun(base = main,docs 仓)
  • 执行边界:只在 docs worktree 归档;夹具统一用 t01-<随机> 前缀按精确 id 隔离; 未修改 tc.md §1–§7,只回填 §8 并新增本报告与 evidence/
  • 采集脚本与原始输出:evidence/scripts/、evidence/logs/

1. 结论先行 ​

后续更新(2026-09-18 复验):本报告记录的唯一「不通过」B09 已解除。

  • 修复:1b3c25b fix(数据库): 连接池接住空闲连接错误,避免断库直接终结进程(已进 main)
  • 复验证据:evidence/logs/b09-failclosed-reverify.json(5/5 通过)
  • 复验口径:经真实 SDK 的 createPermissionCheckClient 调用(pnpm pack 打 tarball 后安装), 覆盖基线 / 停库 / 暂停容器 / 恢复 / statement_timeout=1ms 五档;SDK 全程不抛异常,服务进程 restarts=0
  • 因而当前为 通过 23 / 不通过 0 / 阻塞 4(tc.md §8 已同步)

本报告 §1—§6 的正文保持预跑当时的事实不改写(那是审计轨迹); 另在复验中发现一处可观测性缺陷(describeError 未能带出 SQLSTATE, drizzle 把 code 包在 .cause),详见复验证据的 extraFinding。

4 条阻塞(P10、B13、B10、N02 兜底方向)当时仍待环境。

后续更新(2026-09-20 管理台走查):P10 与 B13 的阻塞已解除。

  • 管理台六页于 09-20 全部合入 main 并部署(nexo-perm-console#15), 权限平台经 nexo-perm.snailuu.cn 对外(反代到 127.0.0.1:3002,/internal/* 已封堵 404)
  • 以平台管理员在生产管理台逐页走查,六页主操作闭环全部成功;B13 硬删三态补齐
  • 证据:evidence/console/(6 张截图)
  • 走查数据统一用 p10- 前缀,结束后已从生产库清零(org/ns/role/subject/pp 各 0 条残留)

因而当前为 通过 25 / 不通过 0 / 阻塞 2(B10、N02 兜底方向,同因需真实 nexo-account test 实例)。

走查中另发现两件事,见本节末「09-20 走查补充」。

27 条用例(P01–P10、B01–B14、N01–N03):通过 22 条 / 不通过 1 条 / 阻塞 4 条。

分组通过不通过阻塞
主路径 P01—P10901(P10)
边界与安全 B01—B14111(B09)2(B10、B13)
非功能硬阈值 N01—N03201(N02 的兜底闭合方向)

逐条结论以 §5 与 tc.md §8 为准。 三条「通过」中含部分维度声明:B05 的 DB 触发器层、B11 的幂等部分、 B12 的「第二个真实用户」维度——各自的缺口写在 §5 对应行的备注里。 N02/B09 虽整体记为阻塞/不通过,其已能验的部分(N02 通知路径 14ms、 B09 的异常形状)也在 §5 如实给出。

三项硬阈值实测全部达标,且都取的是服务端口径:

用例判定项实测采样量
N01判定 p99 < 10msp99 2.711ms(p50 1.228 / p95 1.885 / max 11.901)1000 次混合请求 + 50 次预热
N02通知路径联动生效 ≤ 1s最大 14ms(三次采样 11/14/14ms)3 次独立 disabled 通知
N03与 N01 基线差异 < 1msp99 2.424ms vs 基线 2.711ms,差 −0.287ms(|Δ| < 1ms)同口径 1000 次,outbox 积压态

唯一的「不通过」是 B09:停库与超时注入时,nexo-perm-api 进程直接退出, 而不是返回 {allow:false, denyReason:'exception'}。根因已定位到代码行(见 §4.1), 证据 evidence/logs/b09-crash.log。

2. 环境搭建与降级说明(务必先读) ​

2.1 真实起起来的部分 ​

组件状态说明
nexo-perm-api真实集成分支构建产物 dist/index.js,连真实 nexo_perm
nexo_perm 库真实容器 nexo-perm-pg,迁移 + 种子已执行
判定接口 /internal/check真实全链路真实 HTTP + 真实 HMAC 签名(SDK 原语)+ X-Nexo-App 身份推导,无 mock
管理接口 /api/*真实全链路真实 Bearer JWT:经 nexo-auth(本地 3001)走完 H1 → 登录 → 授权码 + PKCE → 兑换,Ed25519 本地验签通过
状态通知 /internal/subject-events真实接收端真实 HMAC(PERM_ACCOUNT_EVENT_SHARED_SECRET 独立信任层)+ 真实消费循环落地

2.2 用 stub 替代的部分(已如实标注) ​

nexo-account 本地没有可用实例。tc.md §2 要求的「nexo-account test 实例可用」这一前置不成立。

POST /api/subjects 在真实实现里会先向账号中心确认账号状态( src/routes/subjects/subject-service.ts:197-212,非 ACTIVE 即 ACCOUNT_UNAVAILABLE), 因此没有 account 就连 P01 ⑤ 都跑不到。处置:

  • 起了 evidence/scripts/t01-account-stub.ts(仅 GET /internal/users), 它真的验签(t01-stub-hmac.ts,与 SDK 的待签串逐字节一致),签名不对返回 401
  • 生效范围:P01 ⑤(建主体时的账号校验)、B12 ③(建业务用户主体)因此可验
  • 不生效范围:验的是 perm 侧的出站契约(签名、响应形状解析、状态分支), 不是 nexo-account 自己的实现——后者不在本迭代范围

2.3 未接通的路径(冻结契约与现状不符,属上游缺口) ​

tc.md P05/P06/P07、B10、B11 的真机通知路径要求 nexo-account 推出 disabled / enabled / org_changed 三类事件。现状:

  • nexo-auth(即 nexo-account 的本地实现)不存在 enabled / org_changed 两类事件的推送路径
  • 它现有的停用通知,载荷与 perm 冻结契约(SubjectEventRequest)不同—— 已核实到行号:nexo-auth/src/routes/users/account-status-service.ts:88,138,289 与 nexo-auth/src/config/env.ts:152-157

因此本轮把这些用验的下半段(perm 侧接收 + 消费 + 落库)完整跑通, 上半段(账号中心自己会发这三类事件)以脚本扮演发送方。被验面是完整的, 缺的是发送方的真实实现;这一点在 §5 对应行的备注里逐条标注。

2.4 完全无法执行的部分 ​

  • P10(管理台走查):nexo-perm-console 未部署,且无壳环境(nexo-app),evidence/console/ 目录不存在
  • B13 的截图部分:同上(API 部分已跑,见 §5)
  • B09 的「SDK 侧不抛异常」:需要 SDK 消费方进程;服务端已经先崩了(§4.1), 该条无法独立成立

2.5 共享库独占 ​

本报告的所有取数都在确认无并发写负载后采集(pg_stat_activity 中 state='active' 的非 psql 连接数为 0),符合 briefs/SHARED-DB-NOTE.md 的要求。 N01/N03 的 p99 语义依赖这一点。

3. 三项硬阈值采集口径与实测 ​

3.1 N01 判定延迟 p99 < 10ms ​

口径(tc.md §6 N01):100 主体 × 50 角色(含 3–5 层组合链)× 500+ 权限点; 连续 1000 次混合请求,allow : deny ≈ 6:4;服务端毫秒时间戳差计 p50/p95/p99。

实现(evidence/scripts/b08-bench-check.ts,B08 交付):在服务端进程内直接调 router(app.request),计 performance.now() 差。这一窗口不含网络往返与反代 (那是 SDK 的超时预算),含路由匹配、zod 校验、命名空间推导与五步判定。

指标本轮实测(18:38 UTC)B08 原始数据(17:07 UTC)
p501.228ms1.218ms
p951.885ms1.842ms
p992.711ms2.248ms
max11.901ms3.683ms
期望不符0 / 10000 / 1000
exceptionProbeexception(210.36ms,即 200ms statement_timeout 生效)exception(210.39ms)

两次都在阈值内,且 p50/p95 几乎重合(差 <0.05ms)——说明机器噪声没主导结果。 p99 与 max 的差异来自共享库上其他 worktree 的历史残留导致的 PG 缓冲冷热不同 (本轮 max 11.9ms 是一次 GC/调度抖动,不改 p99 档位)。

判定:通过(p99 2.711ms < 10ms,余量 3.7×)。 证据:evidence/logs/n01-t01-rerun.json(本轮原始样本 1000 条)

3.2 N02 停用回收时效 ​

口径(tc.md §6 N02):通知路径联动生效 ≤ 1s;另测丢通知场景由兜底闭合的时延。

方向实测结论
通知路径(disabled → 主体行 active=false)最大 14ms(11/14/14ms,3 次采样)通过(≤1s,余量 70×)
丢通知 → 兜底闭合(验收配置 10s / 生产 300s)未测阻塞,见 §6

口径说明:时延 = 服务端 subjects.updated_at − 发送前本机时刻。同一台机器, 无跨机时钟偏差;仍含 HTTP 往返与脚本 120ms 的轮询间隔,因此是上界。 证据:evidence/logs/n02-latency.json

3.3 N03 deny 审计不阻塞判定 ​

口径(tc.md §6 N03):投递 worker 注入延迟或暂停 worker 使 outbox 积压, 以 N01 同口径重跑 1000 次,差异 < 1ms,且积压期间 check 全部正常返回。

实现(evidence/scripts/t01-n03-audit-backlog.ts):不启动 outbox worker (等价于暂停),先用同一 plan 反复打三轮把 check_deny 行堆起来, 再在积压仍然存在时走与 N01 逐字相同的 plan / 计时窗口 / 数据集跑 1000 次。

指标实测
积压态本轮 check_deny 新增 36 行未投递(会话内累积)
p50 / p95 / p991.132 / 1.662 / 2.424ms
与 N01 基线差(p99)2.424 − 2.711 = −0.287ms(|Δ| < 1ms)
积压期间 check 返回1000/1000 正常,期望不符 0 条

判定:通过。 证据:evidence/logs/n03-audit-backlog.json

口径备注(供评审):本脚本进程内不启动 worker,而 check_deny 的写入路径 与生产完全一致(emitAsync 落 outbox),只是没有消费者。这与 tc.md 允许的 「暂停 worker 使 outbox 积压」等价。若评审要求「worker 在跑但投递慢」的形态, 需在投递适配层注入延迟——那会改动被测代码,超出 T01 范围。

4. 实现偏差 / 用例问题 ​

4.1 B09 fail-closed 未达成:服务进程随库一起退出 ​

这是本轮唯一的「不通过」,也是最重的一条。

tc.md B09 要求:停库/超时注入时 checkPermission 返回 {allow:false, denyReason:'exception'} 且不抛异常,业务方进程无未捕获异常。

实测(evidence/scripts/t01-b09-failclosed.ts,真 docker stop 容器 / 真压 statement_timeout=1ms):

步骤期望实际
⓪ 基线(库正常)判定正常返回通过:{allow:false, denyReason:'subject_inactive'}
① docker stop nexo-perm-pg 后判定200 + exception服务进程退出:fetch failed,/health 也不可达
② 恢复库 + 重启服务判定回归正常通过(重启后 200)
③ statement_timeout=1ms 后判定200 + exception服务进程再次退出
④ 还原配置 + 重启服务判定回归正常通过

根因(代码事实,非推测),证据 evidence/logs/b09-crash.log:

node:events:497
      throw er; // Unhandled 'error' event
error: terminating connection due to administrator command     (code: 57P01)
Emitted 'error' event on BoundPool instance at:
    at Client.idleListener (.../pg-pool@3.14.0_pg@8.23.0/node_modules/pg-pool/index.js:62:10)
  • src/db/index.ts:11 建池:export const pool = new Pool({ connectionString: env.PERM_DATABASE_URL }); ——没有注册 pool.on('error', ...)
  • 库被停/连接被 pg_terminate_backend 掐断时,pg-pool 在 idleListener 里对空闲连接 发出 'error' 事件;Node 的 EventEmitter 无监听者即 throw, 未捕获异常终结进程——与「谁在调判定」无关,服务整体不可用

影响:这不是「判定没兜住」而是「服务没了」。design.md §8/tc.md §1.1 要求的 fail-closed 语义(exception 拒绝)在单点数据库故障下退化为整体停机; N02 的兜底闭环、B10 的定时核对、B12 的鸡生蛋闭环在库抖动期间全部不可达。

修法(不在 T01 范围,只登记):src/db/index.ts 建池后加一条 pool.on('error', (err) => logger.error('perm.db', '连接池空闲连接错误', ...)), 把错误降级为日志;判定路径自身已有 fail-closed 分支(exceptionProbe 实测 200ms 超时返回 exception,见 §3.1),补上这一条即可让 B09 成立。

复现:evidence/scripts/t01-b09-failclosed.ts(幂等、可重复执行; 它会自己停库/还原并重启服务,需要 T01_SERVICE_START_SCRIPT 指向 evidence/scripts/t01-restart-service.sh)。

4.2 集成分支当前 tsc 不绿(一处编译错误,待修) ​

集成分支工作区存在一处未提交改动引入的编译错误:

src/routes/subject-events/subject-event-service.ts:610
error TS2552: Cannot find name 'MODULE'.

MODULE 是 event-consumer.ts:29 / reconcile.ts:26 的模块内 const, 从未 export/import;本文件其余四处日志(413/426/516/552)都直接用字面量 'perm.subject-events'。这是全仓唯一一条 tsc error。

该改动的意图是正确的(修复 B08 的 check_org_status_failure 载荷被消费循环 当 payload_malformed 静默吞掉),设计也对(新增 orgStatusRecheckPayloadSchema 并入联合类型,与 check-service.ts:355-368 的载荷逐字段吻合)。 登记口径:F1 已在集成分支修复,但引入一处编译错误待修。 (由 B10 审查者定位并交叉核实,T01 未修改任何 src/。)

4.3 tc.md B05 步骤①的「PATCH 权限点 scope」在本实现无对应端点 ​

tc.md B05 步骤①写「API 层分别 PATCH 权限点 scope、角色 org_id、app 主体 bound_namespace_id」。实际 design.md §9 的权限点资源没有 PATCH: 只有 POST / GET / POST :id/offline / POST :id/activate / GET :id/impact / DELETE :id。

实测 PATCH /api/permission-points/{id} 返回 404 NOT_FOUND(接口不存在)。 这不是实现缺失——scope 一经创建不可变的语义由「没有可改它的接口」+ DB 触发器 两层成立(触发器见 drizzle/0001_reject-immutable-cols-trigger.sql)。 roles.org_id 与 subjects.bound_namespace_id 的 HTTP 路径实测均返回 IMMUTABLE_FIELD(见 §5 B05)。

用例子问题(登记,不改用例):B05 步骤①的措辞与 design.md §9 资源面不一致。 建议评审时把该步改为「权限点 scope 无 PATCH 端点(404)+ 直连 UPDATE 被触发器拒」。

4.4 tc.md P08 ② 的强制拒绝顺序:实现是对的,用例没写清前置 ​

tc.md P08 步骤①(下线 task.close)后步骤②对同一个被 deny 的用户 check, 期望 pp_invalid。实测对带 deny 的用户返回 deny_hit、对不带 deny 的用户 返回 pp_invalid。

这不是缺陷:判定顺序(tc.md §1.1 冻结基线)里②显式拒绝本就早于③权限点状态, check-service.ts:524-533 的注释也明确写了「先判 deny 再判 pp 状态,因此已下线 权限点上的 deny 仍优先命中」。P08 的意图是隔离第③步,因此前置条件应指定一个 无 deny 的用户。T01 按此隔离验证,两条顺序断言都通过(见 §5 P08)。

用例子问题(登记,不改用例):P08 步骤② 的前置应写明「对无 deny 的用户」。

4.5 环境类偏差(非产品缺陷) ​

  1. nexo-perm-pg 容器没有定义 healthcheck,docker inspect .State.Health 不存在;探活须用 pg_isready。仅影响取证脚本写法。
  2. nexo-auth 本地仅有 member1 一个可登录账号(user1/user2 的登录表单 返回 200 页面但换不到令牌)。因此 B12 的「第二个真实用户」维度按 「同一 sub 的主体行临时停用」隔离,见 §5 B12 备注。

5. 逐用例结果 ​

5.1 主路径 P01—P10 ​

编号结果证据备注
P01通过logs/p01-p03-b01-b05-b13.json、logs/e2e-integration-2026-09-20.txt①–⑤ 走真实 HTTP 写链路(真实 JWT),⑥ check {allow:true},⑦ 三类审计事件各自落 audit_outbox,⑧ 主体行 epoch=0, active=true。account 校验经 stub(§2.2)
P02通过logs/t01-cases.json直授未过期 → allow;把 expires_at 改到过去 → no_grant 且行仍在(判定不依赖清理)
P03通过logs/p01-p03-b01-b05-b13.json、logs/e2e-integration-2026-09-20.txt接口路径:设 deny → deny_hit;解除 → 恢复 allow,explicit_denies 无残留
P04通过logs/p04-effective-permissions.json、logs/t01-cases.json组合链 R-comp→R-base allow;给来源角色新关联后立即生效(间隔 10ms ≪ 5s);聚合预览 via='composed'
P05通过logs/p05-p06-p07.json停用后 subject_inactive、epoch 不变无新行、通用 deny 清除 / org deny 保留、inbox 行 processed_at 非空。账号侧发事件由脚本扮演(§2.3)
P06通过logs/p05-p06-p07.json复职新建行 epoch+1、旧行保持失活;旧行角色不复活 → no_grant;重新分配后恢复 allow
P07通过logs/p05-p06-p07.jsonorg_changed 后新行 org_id=org-b, epoch+1、旧行失活、通用 deny 清除;查旧组织权限点被拒。账号侧发事件由脚本扮演(§2.3)
P08通过logs/t01-cases.json下线 → pp_invalid(无 deny 用户)/ 带 deny 用户 → deny_hit(顺序正确,见 §4.4);activate 回原实体(id 不变);原 deny 永效 → deny_hit
P09通过logs/p09-impact.jsonimpact 计数与实际一致(1/1/1);带 confirmCode 硬删 204 且三关联表零残留;同名重建新 id、旧 deny 不继承 → no_grant
P10阻塞—无 nexo-perm-console 部署、无壳环境;evidence/console/ 目录不存在。解除条件见 §6

5.2 边界与安全 B01—B14 ​

编号结果证据备注
B01通过logs/p01-p03-b01-b05-b13.json、logs/t01-cases.json失活 + deny + 未过期直授 → subject_inactive(①先于②)
B02通过logs/t01-cases.json自引用 / A→B 后 B→A / A→B→C 后 C→A 三型均 COMPOSITION_CYCLE,role_compositions 无新增行
B03通过logs/t01-cases.json前置断言:5 层链 allow;建第 6 层被强拒 COMPOSITION_DEPTH_EXCEEDED 且无新增行;存量链仍 allow(强拒新建而非截断)
B04通过logs/b04-matrix.json四拒一放:分配角色 / 关联组织权限点 / 组合 / 直接授权 → CROSS_ORG;关联通用权限点 → 成功
B05通过logs/p01-p03-b01-b05-b13.json角色 orgId 与 app boundNamespaceId 经 HTTP 改 → IMMUTABLE_FIELD;权限点无 PATCH 端点(404,见 §4.3)。DB 触发器一层的 psql 直连断言见 logs/t01-cases.json 的 b01-db-invariants 旁证(drizzle/0001 已由 pnpm test:db 覆盖)
B06通过logs/p01-p03-b01-b05-b13.json、logs/e2e-integration-2026-09-20.txt跨业务方探测 → pp_invalid(按 X-Nexo-App 推导命名空间);伪造字段被 strip 且响应逐字一致;check_deny 审计行存在
B07通过logs/p01-p03-b01-b05-b13.json停用组织 → disabledRoleCount=3 且三角色同事务全 disabled;用户 check 其下权限点 → pp_invalid
B08通过logs/t01-cases.json停用前 allow(反证非恒拒)→ 停用 ns 后 pp_invalid → 恢复后 allow
B09不通过logs/b09-failclosed.json、logs/b09-crash.log服务进程随库/连接异常退出(Unhandled 'error' event on BoundPool),未返回 exception。详见 §4.1
B10阻塞logs/p01-p03-b01-b05-b13.json(部分)已证生产默认周期 = 300s(env.ts);双方向「复活/失活」时序需要真 nexo-account 的在职状态可供核对(GET /internal/users 的真实实现对端),stub 只能返回固定状态,构造不出「核对快照后账号侧变化」的真实时序。解除条件见 §6
B11通过(幂等部分)logs/b11-idempotency.json、logs/t01-cases.json同 eventId 的 disabled 连投 3 次:inbox 仅 1 行、响应均 202、仅一次翻转;enabled 连投 3 次:仅新建 1 行、epoch 恰 +1
B12通过(含 1 项环境缺口)logs/b12-closure.json无平台授权时建组织 → 403 FORBIDDEN;对照:恢复后同一请求 201(证明非「恒拒」);闭环:超管建组织管理员角色 → 分配给业务用户 → 分配成功。「第二个真实用户」维度受本地账号限制(§4.5.2),以主体停用隔离
B13阻塞(API 部分通过)logs/p01-p03-b01-b05-b13.jsonAPI 侧:impact 计数与库内实数一致、confirmCode 错 → VALIDATION_FAILED。管理台三种状态截图无环境(§2.4)。解除条件见 §6
B14通过logs/t01-cases.json失活主体:分配角色 / 直接授权 / 设显式拒绝 → SUBJECT_INACTIVE;重复建主体 → SUBJECT_EXISTS

5.3 非功能硬阈值 N01—N03 ​

编号结果证据实测
N01通过logs/n01-t01-rerun.jsonp50 1.228 / p95 1.885 / p99 2.711ms(阈值 10ms);期望不符 0/1000
N02阻塞(通知路径通过)logs/n02-latency.json通知路径 最大 14ms(≤1s,通过);丢通知 → 兜底闭合方向未测,见 §6
N03通过logs/n03-audit-backlog.json积压态 p99 2.424ms vs N01 基线 2.711ms,差 −0.287ms < 1ms;积压期间 1000/1000 正常返回

6. 阻塞清单与解除条件 ​

4 条阻塞(P10、B13、B10、N02)+ 1 条不通过(B09,见 §4.1)。

编号类型阻塞/未达成项具体原因解除条件
P10阻塞管理台主流程走查(F02–F08)nexo-perm-console 有代码但未部署;无壳(nexo-app)环境,壳内/独立访问均不可达部署 nexo-perm-console 到任一可达环境(含壳入口),并把 URL 与登录方式给到预跑方;随后按 F02→F08 逐页跑并归档截图
B13阻塞硬删弹窗三种状态截图同 P10,无管理台同上;另需 platform.pp.harddelete 权限点已授给走查账号
B10阻塞定时兜底双方向时序(复活/失活)需要真实账号中心的可变在职状态作为核对对端(GET /internal/users 的真实实现);stub 返回固定状态,构造不出「核对快照后账号侧变化」的真实时序提供可用的 nexo-account test 实例(含可变状态的测试账号 ≥3 个);或用可编程 stub 并在报告里标注为降级验证
N02阻塞丢通知 → 兜底闭合时延同 B10:兜底核对依赖真实 account 状态;本轮只验了通知路径(14ms,已通过)同 B10 解除条件;另需把 PERM_RECONCILE_INTERVAL_SECONDS 调到验收值(如 10s)并归档原始时间戳
B09不通过fail-closed 未达成服务进程随库/连接异常退出(§4.1),未返回 denyReason:'exception';连带 SDK 侧「不抛异常」无法独立成立修 src/db/index.ts 的池错误监听后复跑本脚本,并提供真实 SDK 调用方进程

09-20 走查补充 ​

走查本身通过,但有两件事需要登记。

一、P10 的「构造跨组织分配观察拒绝提示」这一步在 UI 上执行不了。

管理台在选择阶段就按组织过滤:授权页的「分配角色」下拉只列用户所在组织的角色, 角色编辑器的「组合来源」同样只列同组织角色。跨组织的对象根本不出现在选项里, 因此构造不出这次后端拒绝。

这是前端防呆,不是缺陷——把错误挡在提交之前比让它往返一次更好。但用例原文 假设了「能提交、然后观察后端返回的中文提示」,与实现不符。建议评审时改写该步为 两条之一:改用其它可在 UI 构造的业务拒绝(如对失活主体授权 → SUBJECT_INACTIVE), 或改为「确认 UI 已在选择阶段阻断跨组织,不依赖后端拒绝」。本轮未改用例。

二、走查过程中触发了一次生产事故,已修复并单独立案。

管理台每进一页并行发 4 个请求,约 3 次页面切换即让 nexo-perm-api 整体挂死—— 请求进得来、一条都不返回,连 /health 都卡住,容器 unhealthy,日志里没有任何错误行。

根因:判定第③步的状态谓词(getNamespaceStatus / getOrgStatus)硬编码用全局 db, 而判定跑在 db.transaction 内,等于「已持有一条连接、再向同一个池申请第二条」。 并发达到池上限(默认 10)后每个请求持一等一,互相等待永不释放。 pg_stat_activity 显示 10 条连接全部 idle in transaction 283 秒, 最后语句同为第②步的 explicit_denies 点查——正是第③步伸手要第二条连接的那一刻。

为什么 T01 预跑没发现:预跑脚本是串行的,从未达到过并发上限。这类缺陷 只在「并发数 ≥ 连接池大小」时显形,低于阈值时有问题的代码与正确代码表现完全一致。

修复见 nexo-perm-api#33 / #34(0.1.3-alpha.1):状态谓词改为接收调用方的 tx,并给连接池加 connectionTimeoutMillis,让同类写法再出现时表现为一批 500 而非静默死锁。 回归用例把并发压到池大小 +5 并断言无 idle in transaction 残留。

这条值得写进 tc.md 的长期启示:非功能用例只测串行延迟(N01 p99)是不够的, 还需要一条「并发 ≥ 连接池大小」的用例。N01 的 1000 次请求是顺序发的, 跑得再多也碰不到这一类问题。

不在阻塞清单但需上游补齐的(不构成「阻塞」,因被验面已完整):P05/P06/P07/B11 的账号侧事件发送方实现(§2.3)。若评审要求「真 nexo-account 发出这三类事件」, 则需上游先按冻结契约实现推送路径。

7. 复现方式 ​

bash
# 0. 前置:容器与库
docker start nexo-perm-pg
#    集成分支服务(3002)
bash /tmp/nexo-proj/wt-integration/scripts/t01-restart-service.sh

# 1. 判定核用例(P02/P04/P08/P09/B02/B03/B04/B08/B11/B14)
source /tmp/nexo-proj/t01-env.sh
cd /tmp/nexo-proj/wt-integration
pnpm tsx scripts/t01-preflight-cases.ts      # → evidence/logs/t01-cases.json

# 2. 管理 API 用例(P01 写链路 / P03 / B01 / B05 / B06 / B07 / B12 / B13)
pnpm tsx scripts/t01-admin-api.ts            # → evidence/logs/p01-p03-b01-b05-b13.json 等

# 3. 状态联动用例(P05/P06/P07/N02)—— 需先起账号 stub
T01_STUB_PORT=3101 PERM_ACCOUNT_EVENT_SHARED_SECRET=$PERM_ACCOUNT_EVENT_SHARED_SECRET \
  pnpm tsx scripts/t01-account-stub.ts &     # 见 t01-env.sh 把 PERM_ACCOUNT_BASE_URL 指向 3101
pnpm tsx scripts/t01-account-linkage.ts      # → evidence/logs/p05-p06-p07.json, n02-latency.json

# 4. N01 硬阈值
eval "$(bash scripts/b08-bench-env.sh)"
pnpm tsx scripts/b08-bench-seed.ts
pnpm tsx scripts/b08-bench-check.ts          # → evidence/logs/b08-n01.json
cp evidence/logs/b08-n01.json <docs>/evidence/logs/n01-t01-rerun.json

# 5. N03 硬阈值(积压态)
pnpm tsx scripts/t01-n03-audit-backlog.ts    # → evidence/logs/n03-audit-backlog.json

# 6. B09 fail-closed(**会停库并让服务退出**,脚本自带还原与重启)
T01_SERVICE_START_SCRIPT=/tmp/nexo-proj/wt-integration/scripts/t01-restart-service.sh \
  pnpm tsx scripts/t01-b09-failclosed.ts     # → evidence/logs/b09-failclosed.json

所有脚本按 t01-<随机> 前缀隔离夹具,可重复执行。 b08-bench-* 会重建 b08bench% 前缀的数据集(B08 交付,自带清理)。


待回填的 issue 留痕:tc.md §1 要求「未通过项在对应 issue 留痕」。 本轮未通过项 1 条 + 实现缺陷 1 条,都是跨仓(nexo-perm-api)记录:

待建/待回填标题类型证据
nexo-perm-api(待建)B09 fail-closed 未达成:数据库故障时服务进程退出(Pool 缺 'error' 监听)可用性(p0)evidence/logs/b09-crash.log + 本报告 §4.1
nexo-perm-api(待建)集成分支 tsc 不绿:subject-event-service.ts:610 的 MODULE 未声明编译(p1)本报告 §4.2

按 brief「跨仓 issue 留言由 Main 决定」,本报告先在此落档,未在 GitHub 上发评论。 本仓(lamolabs-docs#77)的 T01 交付在 feat/t01-acceptance-prerun 分支上,未 push。