外观
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-accounttest 实例)。走查中另发现两件事,见本节末「09-20 走查补充」。
27 条用例(P01–P10、B01–B14、N01–N03):通过 22 条 / 不通过 1 条 / 阻塞 4 条。
| 分组 | 通过 | 不通过 | 阻塞 |
|---|---|---|---|
主路径 P01—P10 | 9 | 0 | 1(P10) |
边界与安全 B01—B14 | 11 | 1(B09) | 2(B10、B13) |
非功能硬阈值 N01—N03 | 2 | 0 | 1(N02 的兜底闭合方向) |
逐条结论以 §5 与 tc.md §8 为准。 三条「通过」中含部分维度声明:
B05的 DB 触发器层、B11的幂等部分、B12的「第二个真实用户」维度——各自的缺口写在 §5 对应行的备注里。N02/B09虽整体记为阻塞/不通过,其已能验的部分(N02通知路径 14ms、B09的异常形状)也在 §5 如实给出。
三项硬阈值实测全部达标,且都取的是服务端口径:
| 用例 | 判定项 | 实测 | 采样量 |
|---|---|---|---|
N01 | 判定 p99 < 10ms | p99 2.711ms(p50 1.228 / p95 1.885 / max 11.901) | 1000 次混合请求 + 50 次预热 |
N02 | 通知路径联动生效 ≤ 1s | 最大 14ms(三次采样 11/14/14ms) | 3 次独立 disabled 通知 |
N03 | 与 N01 基线差异 < 1ms | p99 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) |
|---|---|---|
| p50 | 1.228ms | 1.218ms |
| p95 | 1.885ms | 1.842ms |
| p99 | 2.711ms | 2.248ms |
| max | 11.901ms | 3.683ms |
| 期望不符 | 0 / 1000 | 0 / 1000 |
exceptionProbe | exception(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 / p99 | 1.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 环境类偏差(非产品缺陷)
nexo-perm-pg容器没有定义 healthcheck,docker inspect .State.Health不存在;探活须用pg_isready。仅影响取证脚本写法。nexo-auth本地仅有member1一个可登录账号(user1/user2的登录表单 返回 200 页面但换不到令牌)。因此B12的「第二个真实用户」维度按 「同一 sub 的主体行临时停用」隔离,见 §5B12备注。
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.json | org_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.json | impact 计数与实际一致(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.json | API 侧: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.json | p50 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。