## 修的是什么
`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。
351 lines
24 KiB
Bash
Executable File
351 lines
24 KiB
Bash
Executable File
#!/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" ]
|