Files
inkwatch/tests/run-all.sh
T
jkooe cc0aaffffd fix(pairing): 配对码改由设备本地生成、随连接上报 —— 零鉴权取码端点下线为 410(M4″)
## 修的是什么

`POST /api/pairing/code {device_id}` 原先**零鉴权地生成码并在响应体里返回**:

```
POST /api/pairing/code  {"device_id":"<任何存在的设备>"}
→ 200 {"code":"483920","expires_at":...}
```

只要知道一个存在的 `device_id`,**任何人不登录、不持有任何凭据**,就能拿到该设备
的配对码,再去查看端输入,换到该设备的查看授权。比认领洞更浅:认领洞还要先登录。

根因不是"端点忘了加鉴权",是**码的来源错了**。服务端凭什么相信"请求方就是那台
设备"?`device_id` 是可猜的公开标识;`device_token` 只发给已归属设备(新设备会
成环,M4′ 里已经推演过)。唯一不可伪造的是**那条已建立的连接本身**。

## 改成什么

码由设备本地生成(`client/lib/host/pairing_code.dart`),随 `agent:hello` 的
`payload.claim_code` 上报,网关在这一刻登记为配对码(`PairingStore.registerDeviceCode`),
**断连即删**(`forgetDeviceCode`),TTL 300s 只是崩溃兜底。

于是码的语义变成真话:**"设备此刻在这条连接上活着"**。屏幕上那个码因此永远是服务端
认可的那一个,不存在"屏幕显示 A、服务端认 B"。

**配对码与认领码是同一个字符串**:用户只有一个六格输入框的心智,两条链路各发一个码
只会让人念错。

`POST /api/pairing/code` 保留为 **410 `E_ENDPOINT_GONE`** 而不是删掉:404 会让人以为
路径拼错或版本不对,410 明确说"它被有意移除了,码从哪来"。

## 客户端

`pairing_api.issueCode` 改为**幂等只读**:返回单例里的当前码,不发网络。别把它改成
"每次调用都换一个码" —— 换了却不同时重发 hello,屏幕就会显示一个服务端从未登记过的
值,用户念它必然失败,而且看起来像服务端坏了。

## 顺带补上 M4′ 在客户端的缺口

上一轮 M4′ 只做了服务端:`grep claim_code client/lib` 命中 **0**。也就是服务端能力建好
了、测试全绿,而**真实客户端从不上报码** —— 生产上认领只能走 `isNew` 路径。同一个
"必须有真实产生方"的坑在 M5 里刚踩过一次(表恒空)。本次 `link.dart` 的 hello 已带
`claim_code`,两条链路同时有了产生方。

## ⚠️ 一个比原洞更大的发现:84 项配对回归网跑的是 9 天前的镜像

`tests/pairing-security-test.js` 默认打 `http://127.0.0.1:4000`,那是 docker-compose
的实例。实测 `pmon-business-1` / `pmon-gateway-1` 镜像构建于 **2026-10-30**,而本轮
改动在 **2026-11-08** —— 早 9 天。所以那些"84 项全绿"只证明旧镜像行为没变,对仓库里的
代码一个字都没说。

所以本套件(`pairing-code-origin-test.js`)**自起源码实例**(business + gateway,
随机端口,源码直跑),并在 `run-all.sh` 里那一段加了大字警告:在其余套件改成自起
之前,**不要把它们的绿当作本仓代码的回归证据**。

## 测试

`tests/pairing-code-origin-test.js` **16 项全绿**,已接入 `run-all.sh`。16 条里 13 条是
"拿不到"型(410 / 无码返回 / 猜码被拒 / 跨设备被拒 / 断连失效),3 条是"该拿到的人
拿得到"(hello 登记、verify 成功、一次性消费)—— 只验前者的话,一个把配对整体做死的
实现也能全绿。

客户端:`pairing_card_test` 6/6、全量 **595 通过 / 2 失败**(两条仍是 `libpmon_core.dylib`
未编译)。两条断言按行为变更改写而不是删除:
- "发码失败不静默" → 改为"**一次请求都不发**"(失败路径已不存在,但新风险更大:
  任何一次发出的取码请求都意味着码又能被第三方索取);
- "服务端返回 123456" → 改为"卡片显示的码必须与上报给服务端的是同一个"。

第一次跑本套件时 E 段红了两条,原因是我自己的夹具没关掉前一条连接 —— 网关判定
"这台设备还有别的活连接",正确行为就是不删码。已修,并在测试里写清这个坑。

回归:认领 57/0、埋点 29/0、权限 60/0、schema 116/0。
2026-11-08 19:57:57 +08:00

351 lines
24 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 墨观 全量验证入口
#
# 前置:
# - Redis @ 6379、PostgreSQL @ 5432(表结构已由 deploy/init.sql 建好)
# - 网关 @ 3000、业务服务 @ 4000(本脚本不会自动拉起,见下方提示)
# - cargo(跑 Rust 单测)+ 能 import ctypes 的 python3(跑 FFI 冒烟)
# cargo 若装在 `$HOME/.cargo/bin`,本脚本会自己补进 PATH;仍找不到就标
# **SKIP**("环境缺 cargo"),绝不记成 FAIL —— 详见文件中部 CARGO_BIN 的注释。
# 如需指定解释器:PMON_PYTHON=/path/to/python3 bash run-all.sh
#
# 无上述依赖时,离线套件(不需要网关/业务进程的那些,含 Rust 单测与 FFI 冒烟)仍会完整执行:
# 结构性守卫 57 / 镜像覆盖性 49 / 上行 msgpack 52 / seq 去重 28 / 指标字段一致性 116
# 协议契约 34 / 编解码 64 / Rust 单测 61 / FFI 冒烟 79
# 实例配置接线 56 / SMTP 传输层 92(自带假 SMTP 服务器 + openssl 现场签证书)
# 权限矩阵 60(第 8 段是档位字段接线台账的双向差集 + 对外投影) / 用户生命周期 68 / 管理端 80 / 安装向导 97(后三者自起临时实例,
# 只需 PG+Redis;安装向导还会建 4 个临时库)
# 档位/历史/审计档位 97(M2/M3/M7:/api/plans 三集同集 + 负向对照、升档申请全链路、
# 历史软截断、审计先角色后档位、retention_days 随实例配置变;自起临时实例,
# 并在 plan_requests 缺失时执行 deploy/migrate.sql 里那一段真实 DDL)
# 认领在场证明 57(M4′:自报 owner_id 不落归属 / 6 位码随 hello / 双桶限流 / 审计条件化;
# 自建临时库 + **独占 Redis db 14**,跑完 FLUSHDB —— 失败计数键不含随机后缀,不隔离会被上一次运行污染)
# 配对码来源 16(M4″:零鉴权取码端点已下线为 410 / 码只能由设备随 hello 上报 /
# 断连即删 / 码劫持到别的设备被拒;**源码直跑自起 business+gateway**)
# 埋点白名单与有界 29(M5:未知名丢弃计 dropped / 超 20 条截断 / 写库失败仍 202 /
# **表里真的长出了行**;匿名允许、废令牌 401、body.account_id 自报被忽略)
# 客户端 Dart 测试 598(查看端逻辑+界面、配对卡、账号与角色、单位自适应、
# 格式化唯一出口、配置变更、视觉令牌护栏、机型采集、
# 连接域、中继驱动、采样调度、历史、成员卡…)
# ⚠️ 上述计数以**实跑**为准:静态数 test( 会漏掉 for 循环内动态生成的用例。
# 改完测试请同步这里(历史教训:标签写 416 而实跑已 598,数字长期漂移)。
#
# 配对码安全套件分两档:离线契约 23 项永远跑;端到端 84 项需业务服务在 :4000。
# 账号体系 75 项(含历史回溯 14、归属缓存 9)与安全开关 41 项需要业务服务 + PG。
# 设备归并端到端 31 项需要业务服务 + PG(会自清测试数据)。
# 安全开关 41 项会就地拉起三个临时进程(严格 :4011 / 网关 :3011 / 配额关闭对照 :4012),跑完即停:
# [E] 段守「全新设备首次接入」(认领即注册,防死锁回归);
# [F] 段守「指标/心跳通道不得绕过设备令牌」(防只守 hello 的漏洞回归)。
# 查看端端到端 21 项需业务服务 + dart + 已构建的 Rust 核心。
# 安装器干净目录 38 项(E10a)需 dart + Rust 核心产物 + 采集端可执行文件;
# 全程在临时 HOME 内跑,不碰真实家目录;缺依赖时明确跳过并返回成功。
# CLI 进程退出码 11 项需 dart + Rust 核心产物:守"失败语义真的到达进程边界"
# (本 SDK 上 main 的返回值不设退出码,只有 exitCode=/exit() 生效)。
# 中继路由端到端 14 项需网关;中继客户端端到端 6 项还需 dart(无需 Rust 产物)。
# 另外三个**纯离线**套件(不起服务,任何机器可跑):
# 中继 30(四道闸 + 真传不落盘 + 慢传不误删 + 限速定时器不叠加)、
# 会话索引 50(O(1) 寻址与 sessions 表同生共死 + 角色翻转摘索引)、
# 登录限流兜底有界 22(防"限流器自己变成泄漏点"+ 喷洒不冲掉热点 key)。
# 落库失败不丢批 41(E2):用注入 repo 跑,无需服务;末尾的 /health 校验
# 需要 PG + Redis,起不来则明确跳过(不影响其余断言)。
#
# 用法:bash tests/run-all.sh
set -uo pipefail
cd "$(dirname "$0")"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
TOTAL_PASS=0; TOTAL_FAIL=0; TOTAL_SKIP=0
# ── cargo 的可执行性:**环境缺失不许冒充"测试失败"** ──────────────────
# 实测(2026-10-27):`$HOME/.cargo/bin` 不在 PATH 时(非登录 shell / CI / cron),
# 下面那条 Rust 套件会打印 `line 52: cargo: command not found`,然后被 run()
# 记成一次 FAIL —— 汇总显示「30 通过 / 2 失败」,报错形式是 **"Rust 核心单测失败"**。
# 于是所有人都会去查 Rust 代码,而真因只是 PATH。这正是"让自己的环境问题冒充
# 被测对象的问题",与被修过的"护栏假装在工作"是同一族。
# 故:① 主动补 PATH;② 仍找不到就明确 SKIP,**不计入失败**。
if [ -z "${CARGO_BIN:-}" ]; then
if [ -x "$HOME/.cargo/bin/cargo" ]; then
export PATH="$HOME/.cargo/bin:$PATH"
CARGO_BIN="$HOME/.cargo/bin/cargo"
elif command -v cargo >/dev/null 2>&1; then
CARGO_BIN="$(command -v cargo)"
else
CARGO_BIN=""
fi
fi
run() {
local name="$1"; shift
echo ""
echo "────────────────────────────────────────────────"
echo " $name"
echo "────────────────────────────────────────────────"
if "$@"; then
TOTAL_PASS=$((TOTAL_PASS+1))
echo -e " ${GREEN}◉ PASS${NC} $name"
else
TOTAL_FAIL=$((TOTAL_FAIL+1))
echo -e " ${RED}◉ FAIL${NC} $name"
fi
}
# 明确的"没跑"——与 PASS/FAIL 都不同。为什么必须单列:把它算成 FAIL,人会去查
# 被测对象;算成 PASS,覆盖缺口就被藏起来了。两者都是说谎。
skip() {
local name="$1"; local why="${2:-环境不具备}"
echo ""
echo "────────────────────────────────────────────────"
echo " $name"
echo "────────────────────────────────────────────────"
echo -e " ${YELLOW}⚠ ${why}${NC}"
TOTAL_SKIP=$((TOTAL_SKIP+1))
echo -e " ${YELLOW}◉ SKIP${NC} $name"
}
# 依赖探测
if ! curl -sf http://127.0.0.1:3000/health >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ 网关未在 :3000 运行,跳过依赖网关的测试。${NC}"
echo " 启动命令:"
echo " cd server/gateway && PORT=3000 \\"
echo " REDIS_URL=redis://127.0.0.1:6379 \\"
echo " DATABASE_URL=postgres://pmon:pmon_dev_only@127.0.0.1:5432/pmon \\"
echo " node --expose-gc --max-old-space-size=256 index.js"
echo ""
GW_UP=0
else
GW_UP=1
fi
if ! curl -sf http://127.0.0.1:4000/health >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ 业务服务未在 :4000 运行,端到端测试的授权步骤会失败。${NC}"
echo " cd server/business && PORT=4000 \\"
echo " REDIS_URL=redis://127.0.0.1:6379 \\"
echo " DATABASE_URL=postgres://pmon:pmon_pass@127.0.0.1:5432/pmon node index.js"
BIZ_UP=0
else
BIZ_UP=1
fi
# 不依赖运行服务的测试:总是跑
# 结构性守卫放最前:它检查的是「其余套件的护栏是否真的在执行」,
# 一旦它红,后面所有绿灯的含义都要打折。
run "结构性守卫(57 项)" node guard-structure-test.js
run "镜像构建覆盖性(49 项)" node dockerfile-coverage-test.js
run "上行 msgpack 链路(52 项)" node uplink-msgpack-test.js
run "seq 会话去重(28 项)" node seq-session-dedup-test.js
run "会话索引与首帧标记(50 项)" node session-index-test.js
run "登录限流兜底有界(22 项)" node login-throttle-test.js
run "中继四道闸与真传(30 项)" node relay-test.js
run "指标字段一致性(116 项)" node schema-consistency-test.js
run "协议契约(34 项)" node protocol-test.js
run "编解码与体积(64 项)" node codec-test.js
# 配对安全的离线契约段:不上任何服务也能拦住"把码比对删掉但留着测试"的退化改动
run "配对安全·源码契约(23 项)" node pairing-security-test.js --offline-only
# 内网端点校验:堵的是「客户端上报任意地址 → 查看端去连」的 SSRF 面。
# (P2 起含连接候选校验:host 只收私网、srflx 只收公网,同一类信任边界)
# 故用例重点在**不合法值必须被拦**,而不在合法值能过。
run "内网端点校验(41 项)" node lan-endpoint-test.js
# 隐私扫描:提交/推送前拦一道。工作树 + **全部提交**都扫 ——
# 只改工作树是不够的,`git log -p` 照样能把脱敏前的原文翻出来。
run "隐私扫描(工作树+全历史)" node privacy-scan.js
# 设备身份归并判据(纯函数):判"两条记录是不是同一台机"的唯一真相源。
# 判错的代价是把两台设备的授权与历史搅在一起且不可逆,故离线穷举推演。
run "设备身份判据(23 项)" node device-identity-test.js
# 落库失败不许静默丢整批(E2):旧实现一次失败就丢掉整批(最多 5000 帧),
# 不留计数、不重试、/health 全绿。本套件用注入 repo 验证有限重试槽、毒丸隔离、
# 丢弃计数与告警节奏;末尾再起一个真网关确认 /health 的 storage_failed 可读。
run "落库失败不丢批(41 项)" node storage-failure-test.js
# ★ 权限矩阵(纯函数,离线可跑):本仓库第 15 条"单一真相源"。
# 它出错的形态**不是抛异常**,而是"某一条组合悄悄放行" —— 只有把
# 13 个能力 × 3 个角色全枚举才发现得了。另含"授权 ≠ 夺权"(ownerOnly)
# 与三条失败关闭纪律。跨语言那一半在 schema-consistency 第 14 段。
# 第 8 段是"档位字段接线台账"的双向差集(防下一个白写的卖点字段)
# 与对外投影过滤(unwired 字段不得进 `/api/plans`),含 10 组**负向对照**
# —— 护栏本身必须被证明会红。
run "权限矩阵与失败关闭(60 项)" node permissions-test.js
# ★ 用户机制全生命周期:注册/验证、会话踢人、改密、找回(**防枚举**)、
# 邀请与三级角色、转移归属、配额真的拦、注销冷静期、审计。
# **自己拉起临时实例**(随机空闲端口 + 自己的 outbox 路径 + 放宽的限流额度),
# 跑完即停 —— 因此不依赖 :4000 那个业务服务,只要 PG + Redis。
# 为什么这么设计(三个坑,根因同一个"被测环境不归测试管"):
# · 写死 :4010 会撞上次遗留的旧进程 → 测的是改动前的代码;
# · outbox 路径两处各算一份 → 永远找不到令牌,整段假红;
# · 注册限流 20 次/小时被反复重跑耗光 → 429 假红。
# 缺 PG/Redis 时脚本自己打印 [skip] 并退出 0(不冒充通过,也不记失败)。
run "用户生命周期(68 项)" node user-lifecycle-test.js
# ★ v2 · M2/M3/M7:档位目录投影「三集同集」(响应键 = 台账非 unwired 键 =
# OpenAPI Plan.properties,含 5 组负向对照——种一个假字段就得红)、升档申请
# 全链路(201 → 409 → 审批生效 → 幂等不重复写审计 → 驳回必须给理由)、
# 历史软截断(free 请求 30d 得 7d + truncated,bucket_sec 跟**生效**窗口)、
# 审计「先角色后档位」(同一 free 账号:自己的设备 403 E_PLAN_REQUIRED,
# 别人的设备 404 —— 两条并置才证明顺序)、retention_days 随实例配置变。
# 同样自起临时实例(随机端口 + 自己的 ADMIN_TOKEN),只需 PG+Redis。
run "档位与历史截断(99 项)" node plan-history-tier-test.js
# ★ 埋点(M5):未知名整条丢弃并计 dropped、超 20 条截断、写库失败仍 202。
# 最要紧的一条是 A7「表里真的长出了行」—— 没有它,前面每条都能在一个
# "什么都没落库"的实现上全绿(端点行为对、表恒空,每层单看都对)。
# 每条"应当丢弃"的断言都配了"应当受理"的正控制,防"全都丢"也全绿。
run "埋点白名单与有界(29 项)" node events-test.js
# ★ 云端管理端:**全站作用域**的接口(能列全部账号、导出全部邮箱、封任何人的号),
# 唯一的边界就是那道口令。故重点不是"功能能用",而是四种"没配好"都不放行:
# 没设口令 / 口令太短(<32) / 口令错 / 口令走 query 而非 header —— 前两种一律 404
# (不对外宣布它存在),后两种 401 且不回显片段。
# 另一半是"不许漏出来的东西":响应里不得出现 secret_hash、完整 token_hash;
# CSV 里以 = + - @ 开头的用户字段必须加前导单引号(否则 Excel 打开即执行公式)。
# 同样自己起 9 个隔离实例(每个形态一个进程),跑完即停。
run "管理端鉴权与数据(80 项)" node admin-api-test.js
# ★ 安装向导:「这台实例归谁管」只能确定一次,而 Web 向导的行业经典缺陷正是
# 「谁先访问谁就是管理员」。故每段都在验证**某一条闸真的关着**:
# 没设引导令牌 / 令牌短于 32 → 一律 404(不是 401);未初始化 → 业务端点 503;
# 令牌连错达上限 → 连**正确**令牌也 429;成功后向导永久消失、**进程重启也不复活**;
# 两个 complete 同时进临界区 → 只一个 200(用行锁把并发做成必然,不靠运气);
# 存量库升级 → 不断服,且已存在的邮箱只提权、**不改口令**。
# 它会 CREATE/DROP 四个临时库并各自跑一遍真实的 init.sql/migrate.sql ——
# 顺带把「全新库首次部署」这条路径也测了(历史上它出过建表顺序的坑)。
# 需要 PG 的 CREATEDB 权限;缺依赖时自己打印 [skip] 并退出 0。
run "安装向导与实例配置(126 项)" node setup-test.js
# 静态守门:每个 instance_config 字段**必须有人读**(界面文件不算消费)、
# 每个 env 覆盖名都在 .env.example 与两份编排里。
# 起因是 2026-10-30:mail_mode / mail_from / public_url / audit_retention_days /
# free_device_limit 五个字段落了库、界面能改,代码里却一行都没读。
run "实例配置接线(56 项)" node instance-config-wiring-test.js
# 发信传输层:自带假 SMTP 服务器(含 STARTTLS/465 隐式 TLS,证书用 openssl 现场生成),
# 断言的是"服务器真收到了什么"——信封、报文头、点填充、AUTH 在明文还是密文上发。
# 起因:mail_mode=smtp 以前是 `return delivered:false`,只测返回值的话随便返回
# true 也能全绿。含 QQ/163 的服务商预设(465 隐式 TLS、From 必须等于账号、授权码提示)。
run "SMTP 传输层(92 项)" node smtp-test.js
# 真实投递:需要真凭据(QQ/163 的授权码),没配就 [skip] 退出 0 ——
# 日常回归不该因为"这台机器没配邮箱"变红。要跑:设 SMTP_HOST/USER/PASS 再执行。
run "SMTP 真实投递(需凭据)" node smtp-live-test.js
# Rust 侧:单测 + 真实加载编译产物的 FFI 冒烟
if [ -n "$CARGO_BIN" ]; then
run "Rust 核心单测(61 项)" "$CARGO_BIN" test --release --manifest-path ../rust-core/Cargo.toml
else
skip "Rust 核心单测(61 项)" "环境缺 cargo(\$HOME/.cargo/bin 不在 PATH?)——这是环境问题,不是 Rust 代码的问题"
fi
run "FFI 冒烟(79 项)" ${PMON_PYTHON:-python3} ffi-smoke.py
# 安装器干净目录 e2e(E10a):「首次安装」是只在干净机器才走的路径 ——
# 正常开发机上早就装过,任何一处写坏都会被旧文件掩盖。本套件用**临时 HOME**
# 从零走一遍:落位 bin+dylib → 写配置 → plist → status 四项判据 → 卸载摘干净,
# 并断言「屏幕上说安装失败时退出码必须非零」(合并前 Future<int> main 会把
# 退出码吃掉,失效的失败语义在 shell/CI 眼里仍是成功)。
# 缺 dart / 缺 Rust 核心产物 / 缺采集端可执行文件时,脚本自己打印 ⚠ 并返回成功。
DART_BIN="${PMON_DART:-$HOME/development/flutter/bin/dart}"
if [ -x "$DART_BIN" ] && [ -f ../rust-core/target/release/libpmon_core.dylib ]; then
run "安装器干净目录 e2e(38 项)" node installer-clean-e2e.js
# CLI **进程退出码** e2e:本 SDK 上 main 的返回值不设退出码(实测 int/Future<int>
# 返回 3 → 进程 0),只有显式 exitCode=/exit() 才生效。这套用**真实子进程**断言
# 四种必须 0 的场景(帮助/print-plan/抢不到锁/duration 到期)与两种必须非零的场景
# (依赖缺失 / viewer 缺参),专防"屏幕上说了、进程边界上是死的"这类假失败语义。
run "CLI 进程退出码 e2e(11 项)" node cli-exit-code-e2e.js
else
echo ""
echo -e "${YELLOW}⚠ 缺 dart 或 Rust 核心产物:跳过安装器/退出码 e2e(38 + 11 项)。${NC}"
echo " 需 (cd rust-core && cargo build --release) 与 dart。"
fi
# 客户端(Dart)测试:查看端 48 项 + 自适应单位 24 项 + FFI 冒烟 28 项 …
# 注意必须清掉代理变量:本机代理会让 127.0.0.1 也走代理,socket 相关用例会假失败。
FLUTTER_BIN="${PMON_FLUTTER:-$HOME/development/flutter/bin/flutter}"
if [ -x "$FLUTTER_BIN" ]; then
# 注意:flutter test 必须在客户端目录里跑 —— 本脚本的工作目录是 tests/,
# 直接跑会报 'Test directory "test" not found'(首版就踩了这一步)。
run "客户端 Dart 测试(598 项)" bash -c "cd ../client && exec env \
-u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY \
-u http_proxy -u https_proxy -u all_proxy \
'$FLUTTER_BIN' test --no-pub"
else
echo ""
echo -e "${YELLOW}⚠ 未找到 flutter($FLUTTER_BIN):跳过客户端 Dart 测试(100 项)。${NC}"
echo " 可用 PMON_FLUTTER=/path/to/flutter 指定。"
fi
# 依赖网关的测试
if [ "$GW_UP" = "1" ]; then
# ★ 决定性强的那一道:拿 Rust 核心产出的真实帧跑完整链路并查库断言。
# 其余套件用的都是手写样本载荷,测不出"采集层与白名单漂移"(正是当年 P0 的漏网原因)。
run "真实帧全链路(24 项)" node rust-frame-pipeline-test.js
run "断连竞态残留(3 项)" node race-residue.js
# ★ 中继路由端到端:打真网关三条 HTTP 路由(open/GET/PUT),验四道闸与
# 完整往返。relay-test.js 测的是 Relay 类(进程内),覆盖不到"路由怎么
# 接进来"——历史上正是这里出过一次"类全绿、路由 503"的事故。
run "中继路由端到端(14 项)" node relay-route-e2e.js
# ★ 中继客户端端到端:跑客户端自己的 Dart 实现(openLanRelay→upload→
# download),证明「服务端通 + 客户端函数在」真的能**连起来**用。
DART_BIN="${PMON_DART:-$HOME/development/flutter/bin/dart}"
if [ -x "$DART_BIN" ]; then
run "中继客户端端到端(6 项)" node relay-client-e2e.js
else
echo ""
echo -e "${YELLOW}⚠ 未找到 dart($DART_BIN):跳过中继客户端端到端(6 项)。${NC}"
fi
if [ "$BIZ_UP" = "1" ]; then
# ⚠️⚠️ 读这一段再读下面的数字:**这些套件打的是外部实例,不是仓库里的代码。**
# 它们的默认地址是 `http://127.0.0.1:4000` / `:3000`,也就是 docker-compose
# 起的 pmon-business / pmon-gateway。2026-11-08 实测那两个容器镜像构建于
# 2026-10-30 —— 比当天的改动早 9 天。所以「84 项全绿」曾经只证明**旧镜像**
# 的行为没变,对"仓库里的代码对不对"一个字都没说。
# 已修的那一类:`pairing-code-origin-test.js` 自起源码实例(见下一条)。
# 尚未修的:pairing-security / account / admin-api / viewer-e2e 等仍打外部实例。
# 在它们改成自起之前,**不要把它们的绿当作本仓代码的回归证据**。
# ★ 配对授权绕过缺陷的回归网。旧 e2e 只测"授权后能订阅",
# 从不提交错码,于是"完全不校验"与"校验正确"在测试眼里没有区别。
# 本套件有一半是"No"型断言(错误码必须被拒)。
run "配对码安全(84 项)" node pairing-security-test.js
# ★ 配对码来源(M4″):码只能来自设备那条连接。
# 打的是**源码直跑的临时实例**(自起 business + gateway,随机端口),
# 不是 :4000 的部署实例 —— 见紧随其后的那条警告。
run "配对码来源(16 项)" node pairing-code-origin-test.js
# ★ 认领的「在场证明」(M4′):`agent:hello` 自报 `owner_id` 不得建立归属、
# 6 位码随 hello 上报/断连即删、claim 必须有码、双桶限流、审计条件化。
# 断言的是**结果状态**(查库/查 Redis)而不是"请求返回成功" ——
# "静默忽略"与"静默接受"对客户端都是成功的,只有查状态才分得开。
# 每条"空集/恒为 NULL"的断言都配了必须变红的正控制。
run "认领在场证明(57 项)" node claim-proof-test.js
# ★ 账号体系:注册/登录/登出、设备归属与认领、**同账号免配对**、兼容模式不回归、
# 归属缓存(canView 热路径不再每次打 PG)
run "账号体系(75 项)" node account-test.js
# ★ 产品负责人截图的修复点:同一台手机因 device_id 漂移成了两条设备,
# 认领时按 hardware_id / 稳定后缀归并成一条(历史与授权都带过来)。
# 反面对照同样重要:不该并的(另一台机 / 歧义 / 别人的设备)绝不能被并。
run "设备归并端到端(31 项)" node device-merge-test.js
# 第二步开关:强制登录 / 配额 / 设备令牌防冒充(就地拉起严格模式临时实例)
run "安全开关(41 项)" node auth-mode-test.js
run "端到端冒烟(35 项)" node e2e-smoke.js
# 查看端端到端:真实采集端 + 真实配对 + **真实 Dart CLI 查看端**。
# 需要 dart 与已构建的核心库;缺任一则明确提示跳过(不静默)。
DART_BIN="${PMON_DART:-$HOME/development/flutter/bin/dart}"
if [ -x "$DART_BIN" ] && [ -f ../rust-core/target/release/libpmon_core.dylib ]; then
run "查看端端到端(21 项)" node viewer-e2e-test.js
else
echo ""
echo -e "${YELLOW}⚠ 缺 dart 或 Rust 核心产物:跳过查看端端到端(21 项)。${NC}"
echo " 需先 cd rust-core && cargo build --release"
fi
fi
run "持久化与重启恢复(24 项)" node persistence-restart.js
# ★ 断线补传去重的**跨网关重启**验证(E1):进程内窗口活不过一次重启,
# 而客户端补传最多 120 帧会跨重启继续发生 —— 修复前这批帧全部被判成新 seq
# 重复入库,历史回溯的均值被慢慢拉偏且**不报任何错**。
# 要求 PG(查 device_metrics 行数)+ Redis(窗口键);脚本自行拉起并重启网关。
run "去重跨重启(22 项)" node dedup-restart-test.js
echo ""
echo " 提示:泄漏专项耗时较长(约 80s),单独运行:"
echo " ROUNDS=500 node leak-check.js"
fi
echo ""
echo "════════════════════════════════════════════════"
if [ "$TOTAL_SKIP" -gt 0 ]; then
echo " 套件结果: ${TOTAL_PASS} 通过 / ${TOTAL_FAIL} 失败 / ${TOTAL_SKIP} 跳过"
else
echo " 套件结果: ${TOTAL_PASS} 通过 / ${TOTAL_FAIL} 失败"
fi
echo "════════════════════════════════════════════════"
[ "$TOTAL_FAIL" = "0" ]