docs: v2.1.0 收口回填(README/CHANGELOG/DEVELOPMENT/ROADMAP + 数字守卫)
- README:数字刷新为实测(52 能力 · 42 MCP · 467 单测 · 50 e2e · 23 Rust); 补 na_audit_l2 工具表项、看门狗三端接入段;删「控制台安装包只作为 CI artifact」过期行(9f95560 起已随 Release 发布)。 - CHANGELOG:[未发布] 定稿 v2.1.0 全部主题(rollback / 看门狗 / na-gui 清理 / e2e 与文档收口),与 A 线已回填段共存无重复。 - DEVELOPMENT:新增 §7.5 selfupdate 代理出网机制(NA_PROXY→环境变量→ 系统代理、PAC/socks5 明确报错、回环恒直连、计划任务环境变量告诫); §7.4 补 rollback/看门狗双通道分工表与「为什么运行期不做自动回滚」 三问引述。 - ROADMAP:新增第十节 —— v2.1.0 已落地项划掉、P2 登记(控制台自更新 v2.2.0 / Linux auditd / macOS ESF / macOS 安装器 / guard 日志轮换)。 - tests/unit/docs-numbers.test.mjs(新,6 例):README 五个数字与可 复现实测口径钉死(manifest 条目 / defs.ts 工具 / node --test 实测 / e2e 源码 / Rust 属性),「增能力不改 README」即红。单测数实测经 NODE_TEST_CONTEXT 剥离 + NA_DOCS_NUMBERS_CHILD 递归防护;注入故障 (52→53)实测 2 处守卫变红、恢复全绿。
This commit is contained in:
1 parent
b7f44f7a64
commit
0074d09f08
5 files changed
+284
-4
No files matched your search
@@ -7,6 +7,73 @@
|
||||
|
||||
## [未发布]
|
||||
|
||||
> 本段为 **v2.1.0** 定稿段(版本统一见 `efe4744`):selfupdate 代理出网、LKG 内容指针、
|
||||
> 三个架构审查 P0 修复、L2 三端暴露,以及 rollback / 看门狗 / na-gui 清理 / 收口。
|
||||
|
||||
### 新增 —— 控制端 `nodeagent rollback`:从 LKG 回滚被控端(ARCH T03 / PRD P0-2)
|
||||
|
||||
`rollback` 补上「新版本能启动但需要回退」的通道:读 LKG → 枚举备份(`*.bak`/`*.bak-*`,
|
||||
mtime 新→早)→ 分块流式 sha256 指纹匹配(`hash_full` 全量优先,老记录回落 12 位前缀)
|
||||
→ 覆盖写入口文件(同 deploy 姿势:就地覆写不用 rename,Windows 真机教训)→ 写后校验
|
||||
(失败用回滚源还原)→ 独立连接重启 → 90s 墙钟复核 `build.hash == LKG`。
|
||||
|
||||
- `--dry-run` 只读零写(列出候选与匹配结果);`--backup <路径>` 逃生门:显式指定
|
||||
回滚源、跳过指纹匹配(用于 LKG 记录丢失/全部备份被污染的极端场景)。
|
||||
- 无 LKG / 无指纹匹配:列候选后零写退出(code 2),不碰被控端一个字节。
|
||||
- 纯函数(`planRollback` / `pickBackupByHash` / `hashRemoteFile`)与 IO 分离,
|
||||
假 client 注入单测 19 例(含写后校验失败自动还原、多候选时序、Windows 路径)。
|
||||
- 能力圈边界写进 HELP 与 README:RPC 命令**需被控端可达**;「完全起不来」的场景
|
||||
由看门狗接管(下节)。
|
||||
|
||||
### 新增 —— 被控端启动看门狗:连续失败 N 次自动从 LKG 恢复(决策 R-1)
|
||||
|
||||
rollback 覆盖不了「进程完全起不来」—— 没有监听可连,RPC 无能为力。看门狗跑在
|
||||
supervisor 链上(Windows 计划任务 / Linux systemd / macOS launchd 三端已接入),
|
||||
启动连续失败 N 次(默认 3)自动从 LKG 指纹匹配的备份恢复入口文件。
|
||||
|
||||
- `scripts/startup-guard.mjs`(裸环境可跑:零项目依赖,只依赖 Node ≥22 内置模块)
|
||||
+ `scripts/lib/startup-guard-core.mjs`(纯函数零 import:状态机判断可单测)。
|
||||
- **30s 启动宽限期 + 端口探活**,两类误计数被消除:① agent 自身重启机制
|
||||
(kill -9 / taskkill 打死长命进程)是计划内重启;② supervisor 重启造成的端口撞车
|
||||
(新实例 EADDRINUSE 秒崩)—— 不区分的话连续 3 次即**误回滚好版本**。
|
||||
- 安全边界:**无 LKG / 无指纹匹配只记日志放行,绝不擦除 install-dir**;恢复目标
|
||||
恒为 LKG(单一、已验证,无回滚循环);恢复动作全量记 JSONL 日志。
|
||||
- 被控端启动成功后归零失败计数(`resetStartupGuardState`,`index.ts` 唯一改动点);
|
||||
守卫 spawn 时强制注入 `NODEAGENT_HOME = --data-dir`(Windows 计划任务动作不带
|
||||
环境变量,不注入则 agent 数据目录与守卫漂移,恢复链路静默失效)。
|
||||
- Windows `RestartCount` 3 → 10:看门狗需要「连续失败 3 次 + 恢复」才生效,3 次不够用
|
||||
(良性撞车退 0 不消耗重启次数,故 10 不会造成重启风暴)。
|
||||
|
||||
### 新增 —— na-gui 陈旧计划任务白名单清理(ARCH B-P1-6)
|
||||
|
||||
历史版本在被控端上注册过 `na-gui` 等旧名计划任务,漏删会留下「端口被占、
|
||||
新版本起不来」的幽灵任务。
|
||||
|
||||
- `scripts/lib/scheduled-tasks.mjs`:`TASK_WHITELIST`(`nodeagent` / `na-gui` /
|
||||
`nodeagent-console`)+ `planTaskCleanup` 纯函数。**白名单外一律不删** ——
|
||||
误删用户自己的任务是比漏删严重得多的生产事故;精确匹配(大小写敏感),
|
||||
宁可漏删也不模糊匹配。
|
||||
- `scripts/uninstall.ps1`:卸载末尾按白名单逐个注销(存在性检查 + 动作进日志)。
|
||||
- `scripts/control.ps1`:新增 `cleanup` 子命令(`control.cmd` 菜单第 8 项),
|
||||
供「拿到端口占用提示后自救」。
|
||||
- 单测 10 例(白名单命中才删 / 白名单外零动作 / 空列表 / 重复名 / 脏输入)+
|
||||
ps1 源码守卫与跨端一致性守卫(白名单不许 mjs 与 ps1 两边漂移,注入故障实测会红)。
|
||||
|
||||
### 收口 —— L2 e2e 补齐 + 文档数字刷新与数字守卫(ARCH T05)
|
||||
|
||||
- **e2e**:`run.mjs` 补 L2 平台门控用例(非 Windows 明确报
|
||||
`E_UNSUPPORTED_PLATFORM`,按**错误名**断言不按 code —— 协议级错误才 throw,
|
||||
能力执行失败走 `result.status=failed`);`windows.mjs` 补 status/collect/verify
|
||||
三条(字段与 manifest `returns_schema` 逐字对齐,collect 对非管理员环境 skip 容错)。
|
||||
e2e 总数 33 → 50(34 通用 + 16 Windows 专属)。
|
||||
- **README 数字刷新**:52 项能力 · 42 个 MCP 工具 · 467 项单元测试 ·
|
||||
50 项端到端用例 · 23 项 Rust 壳单测;补 `nodeagent rollback` 与看门狗段、
|
||||
`na_audit_l2` 工具表项;删「控制台安装包只作为 CI artifact」过期行
|
||||
(`9f95560` 起已随 Release 发布)。
|
||||
- **数字守卫** `tests/unit/docs-numbers.test.mjs`:README 五个数字与可复现实测口径
|
||||
(manifest 条目 / defs.ts 工具 / node --test 实测 / e2e 源码 / Rust 属性)钉死,
|
||||
不一致即红 —— 「增能力不改 README」从此会红(注入故障实测验证)。
|
||||
|
||||
### 修复 —— selfupdate 在境内**必然**失败(P0-1:裸 `fetch` 不读代理)
|
||||
|
||||
真机症状:被控端自更新下载 `fetch failed`。根因不是网络差 —— Node 的 `fetch`(undici)
|
||||
|
||||
@@ -168,7 +168,7 @@ nodeagent --node win audit anchor --note "每日例行" # 追加一个锚点
|
||||
> 真正发挥价值的前提:把锚点文件指向**另一台机器 / 网盘同步目录**。
|
||||
> 本机文件被整盘重写时,锚点也随之消失 —— 那就等于没锚。
|
||||
|
||||
**51 项能力** · **41 个 MCP 工具** · **269 项单元测试** · **30 项端到端用例** · **3 项 Rust 壳单测**(真跑 Node sidecar)(CI 在真实 Windows / Linux runner 上验证)
|
||||
**52 项能力** · **42 个 MCP 工具** · **467 项单元测试** · **50 项端到端用例**(34 通用 + 16 Windows 专属) · **23 项 Rust 壳单测**(真跑 Node sidecar)(CI 在真实 Windows / Linux runner 上验证)
|
||||
|
||||
> 关键里程碑:**GUI 语义**(`window.list` + `screen.find`,UIA 找不到自动降级 OCR)
|
||||
> 让 AI 从「看得到画面但读不懂界面」变成「按名字取坐标点下去」。
|
||||
@@ -384,8 +384,6 @@ Windows 版由 CI 产出(`.github/workflows/console-windows.yml`),**双架
|
||||
> 前两条查的是打包**前**的中间产物,这一条查的才是**最终交付物**;
|
||||
> 实测解出 x64 `node.exe` 82.9 MB / `0x8664`、arm64 73.5 MB / `0xaa64`)。
|
||||
|
||||
> 控制台安装包目前**只作为 CI artifact 产出**,尚未接入 `scripts/release.mjs` 随 Release 发布。
|
||||
|
||||
⚠️ 平台差异:`app.*` 与 `system.service.list` **仅在被控端为 Windows 时可用**,
|
||||
非 Windows 被控端会返回 `E_UNSUPPORTED_PLATFORM`。控制台连接后会探测
|
||||
`system.info.os` 并**事前禁用**这些入口,而不是发出注定失败的请求。
|
||||
@@ -492,6 +490,18 @@ nodeagent fanout <能力名> [--nodes a,b] [--args JSON] # 多设备并发下发
|
||||
> (`scripts/startup-guard.mjs`);仍不行再走带外恢复(`docs/DELIVERY-PIPELINE.md` 附录 C)。
|
||||
> 自动回滚刻意不做,理由见该文 §6。
|
||||
|
||||
**看门狗(被控端自启链上的一层)**:被控端由外层 supervisor 看守,启动连续失败 N 次(默认 3)自动从 LKG 恢复入口文件:
|
||||
|
||||
| 平台 | supervisor | 接入方式 |
|
||||
|---|---|---|
|
||||
| Windows | 计划任务 | `scripts/install.ps1` 把启动动作包一层 `scripts/startup-guard.mjs`(`RestartCount 10`) |
|
||||
| Linux | systemd | `scripts/install-linux.sh` 的 unit `ExecStart` 包一层 |
|
||||
| macOS | launchd | 无安装器,手工接法见 `docs/DEVELOPMENT.md` §7.4 |
|
||||
|
||||
安全边界:**无 LKG / 无指纹匹配的备份时只记日志放行,绝不擦除安装目录**;恢复目标恒为 LKG
|
||||
(单一、已验证,不做「择优回滚」)。看门狗自身「裸环境可跑」——零项目依赖,只依赖
|
||||
Node ≥22 内置模块 + 同目录纯函数核心,随安装包/便携包分发。
|
||||
|
||||
### 长任务与剪贴板
|
||||
|
||||
```bash
|
||||
@@ -543,7 +553,7 @@ nodeagent mouse drag 300 200 700 500 # 拖拽(拖文件/框选
|
||||
| `na_clip` | 读写剪贴板文本或图片 |
|
||||
| `na_fs_list` · `na_fs_read` · `na_fs_write` · `na_fs_stat` | 读目录 / 读文件 / 写文件 / 元信息 |
|
||||
| `na_restart` | 受控重启被控端 |
|
||||
| `na_audit` · `na_discover` · `na_nodes` · `na_use` | 查审计、发现设备、多设备切换 |
|
||||
| `na_audit` · `na_audit_l2` · `na_discover` · `na_nodes` · `na_use` | 查审计(`na_audit_l2` = 系统级 L2,仅 Windows 被控端)、发现设备、多设备切换 |
|
||||
| `na_event_watch` · `na_event_poll` · `na_event_list` · `na_event_unwatch` | 事件订阅;`notify:true` 走 MCP 日志通知实时推送,否则用 `na_event_poll` 拉取 |
|
||||
| `na_macro_run` | 回放 GUI 宏(步骤序列,逐步校验) |
|
||||
| `na_metrics` | 成功指标(闭环率 / 装软件率 / P95 / 拦截率)+ 达标判定 |
|
||||
|
||||
@@ -787,6 +787,46 @@ LKG 恢复入口文件。判据与边界见 `docs/DELIVERY-PIPELINE.md` §6。
|
||||
只依赖 Node ≥22 内置模块 + 同目录 `lib/startup-guard-core.mjs`(纯函数)。
|
||||
分发时两个文件必须保持相对位置一起拷贝(pack.mjs / install-linux.sh 已保证)。
|
||||
|
||||
### 与 `nodeagent rollback` 的分工(v2.1.0 回滚双通道)
|
||||
|
||||
| 通道 | 触发者 | 覆盖场景 | 边界 |
|
||||
|---|---|---|---|
|
||||
| 手动 rollback | 控制端 RPC(`nodeagent rollback`) | 新版本**能启动**但需回退 | 需被控端可达(有监听可连) |
|
||||
| 看门狗自动恢复 | supervisor 链上守卫 | 新版本**完全起不来**(启动连续失败 N 次) | 只恢复到 LKG;无 LKG/无匹配放行不动盘 |
|
||||
|
||||
**为什么运行期仍不做自动回滚**(v2.1.0 刻意收窄,引述 `docs/DELIVERY-PIPELINE.md` §6):
|
||||
① 无法归因 —— 远程设备上「更新后健康检查失败」分不清是更新本身坏了还是
|
||||
网络抖动 / 端口被占 / 依赖服务没起;② 回滚循环 —— 自动回滚可能触发
|
||||
「回滚→启动→又判定失败→再回滚」,无人值守设备上比停在坏版本更难恢复;
|
||||
③ 失控 —— 被控端一旦在运行期自作主张改自己的程序,控制端失去对
|
||||
「当前跑的是什么」的掌控。故运行期的决策权留给控制端(rollback),
|
||||
启动期则由看门狗兜底(目标只有一个:LKG)。
|
||||
|
||||
### 7.5 selfupdate 代理出网(v2.1.0,P0-1)
|
||||
|
||||
真机症状:被控端自更新下载 `fetch failed`。根因不是网络差 —— Node 的 `fetch`
|
||||
(undici)**不读** `HTTPS_PROXY` 等环境变量,而境内访问 GitHub 必经代理。
|
||||
`apps/agent/src/net/proxy.ts`(sidecar 已验证方案的 TS 移植,只依赖 node 内置模块)
|
||||
接管 `system.agent.update` 的下载:
|
||||
|
||||
- **代理来源优先级**:`NA_PROXY` → 目标协议环境变量(`http_proxy` / `https_proxy` /
|
||||
`HTTPS_PROXY` ...,大小写都认)→ 系统代理(Windows `reg query WinINET` /
|
||||
macOS `scutil --proxy`)。
|
||||
- **PAC 与 socks5 明确报 note、不静默降级**:PAC 需 JS 运行时解析、
|
||||
socks5 无内置客户端 —— 报出来让人知道「为什么没走代理」,而不是
|
||||
悄悄直连后给一个莫名的超时。
|
||||
- **回环目标恒直连**(有意的设计偏离):代理另一侧的 localhost 不是本机 ——
|
||||
CI/dev 机设了 `HTTP_PROXY` 时,发往 `127.0.0.1` 自建镜像的请求会被送进
|
||||
远程代理而 404。
|
||||
- **重定向链共享超时预算**(防 5 跳 × 30s 穿过 RPC 超时);
|
||||
**CONNECT 隧道 abort 自管**(外层 request 建立前 signal 无人受理 ——
|
||||
真机 75s 配置挂 109s 的教训);TLS 默认校验证书。
|
||||
- `maxBytes` 双闸(content-length 预检 + 流式中断),保住「64MB 上限」。
|
||||
- ⚠️ **计划任务环境变量不可见**:Windows 计划任务的启动动作**不带**用户环境变量,
|
||||
在 `install.ps1` 里设 `NA_PROXY` 对任务里的 agent 无效 —— 要么写进任务的
|
||||
`EnvironmentVariables`,要么配在 agent.json 的 `update` 段(被控端自己的配置,
|
||||
不依赖任何外部环境)。
|
||||
|
||||
---
|
||||
|
||||
## 第 8 章 工程结构
|
||||
|
||||
@@ -445,3 +445,39 @@ node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md>
|
||||
6. 发版/打包脚本不得假设 unix 命令存在(`zip`/`ls`/`du`/`node_modules/.bin/*`)
|
||||
7. CI 断言会随属性变化过期,且方向可能反了
|
||||
8. 工作区有他人未提交文件时禁用 `git add -A` / `commit -am`
|
||||
|
||||
---
|
||||
|
||||
## 十、v2.1.0「全面完善」:已落地与遗留(2026-10-26 收口)
|
||||
|
||||
本轮(`docs/ARCH-2026-10-25` / `docs/PRD-2026-10-25`)范围的全部落地情况:
|
||||
计划项从路线划掉,未做的按 P2 登记在案 —— 不塞进 v2.1.0。
|
||||
|
||||
### 已落地(从计划划掉)
|
||||
|
||||
| 项 | 状态 | 落点 |
|
||||
|---|---|---|
|
||||
| 版本统一 v2.1.0 + CHANGELOG 骨架 | ✅ | `efe4744` |
|
||||
| selfupdate 代理出网(NA_PROXY→env→系统代理) | ✅ | `apps/agent/src/net/proxy.ts` |
|
||||
| LKG 内容指针(`hash_full` / `backup_glob`) | ✅ | `apps/agent/src/lkg.ts` |
|
||||
| 控制端 rollback(ARCH T03) | ✅ | `apps/cli/src/cmd/rollback.ts` |
|
||||
| 看门狗启动自动恢复(决策 R-1) | ✅ | `scripts/startup-guard.mjs` + 三端 supervisor 接入 |
|
||||
| L2 三端暴露(MCP `na_audit_l2` + 控制台 + `agent.l2`) | ✅ | `b09ca68` |
|
||||
| na-gui 陈旧计划任务白名单清理 | ✅ | `scripts/lib/scheduled-tasks.mjs` + `control.ps1 cleanup` |
|
||||
| L2 e2e + 文档数字守卫 | ✅ | `tests/e2e/*`、`tests/unit/docs-numbers.test.mjs` |
|
||||
| 架构审查三 P0(validate 原型链 / 限速桶 key / describeArgs) | ✅ | `e864486`、`d106214` |
|
||||
|
||||
### 遗留(P2 登记,明确不塞进 v2.1.0)
|
||||
|
||||
| 项 | 优先级 | 为什么现在不做 |
|
||||
|---|---|---|
|
||||
| 控制台自更新(检查更新/自升级) | P2 · v2.2.0 | 控制台手动重装成本低;且需先定「更新通道与回滚」策略,不与本轮被控端通道混 |
|
||||
| Linux L2(auditd) | P2 | 需预装 auditd + 规则管理,属被控端环境依赖,超出「零依赖立起来」原则 |
|
||||
| macOS L2(ESF) | P2 | Apple entitlement 硬限,个人自签证书无法启用;除非走 Developer ID + 公证分发 |
|
||||
| macOS 被控端安装器 | P2 | 当前无安装器(launchd 手工接入见 `docs/DEVELOPMENT.md` §7.4);macOS 被控端场景少,先文档兜住 |
|
||||
| 看门狗日志轮换 | P2 | `startup-guard.log`(JSONL)无轮换;长期运行设备需运维关注,先以告警文案兜住 |
|
||||
|
||||
### 门禁数字(2026-10-26,Mac 实测)
|
||||
|
||||
52 项能力 · 42 个 MCP 工具 · 467 项单元测试 · 50 项端到端(34 通用 + 16 Windows 专属)·
|
||||
23 项 Rust 壳单测 —— README 数字由 `tests/unit/docs-numbers.test.mjs` 钉死,腐烂即红。
|
||||
@@ -0,0 +1,127 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
/**
|
||||
* 文档数字守卫(ARCH B-P1-3 / T05)。
|
||||
*
|
||||
* ## 为什么需要
|
||||
* README 的能力数 / MCP 工具数 / 单测数 / e2e 数是「会腐烂的数字」:
|
||||
* 每次加能力、加工具、加测试都可能忘同步。腐烂的数字比没有数字更糟 ——
|
||||
* 它给人虚假的信心(「README 说 52 项,肯定全测过」)。
|
||||
* 本守卫把 README 的数字与**可复现的实测口径**钉在一起:
|
||||
* 不一致即红,逼使「改代码必改文档」。
|
||||
*
|
||||
* ## 口径(全部在 Mac/Linux 上可复现,不依赖 Windows)
|
||||
* - 能力数:packages/protocol/src/capabilities/manifest/*.ts 的 `name: CapabilityNames.` 计数
|
||||
* - MCP 工具数:apps/mcp/src/tools/defs.ts 的 `name: 'na_` 计数
|
||||
* - 单元测试数:`node --test tests/unit/**\/*.test.mjs` 的汇总行实测
|
||||
* - e2e 数:tests/e2e/{run,windows}.mjs 的 `await test(` 计数之和
|
||||
* - Rust 数:apps/console/src-tauri/src/*.rs 的 `#[test]` / `#[tokio::test]` 计数
|
||||
*
|
||||
* ## 递归防护
|
||||
* 单测数实测要 spawn 一个全量 node --test 子进程;该子进程也会跑到本文件。
|
||||
* 用 NA_DOCS_NUMBERS_CHILD=1 让子进程内的本用例跳过 spawn(否则无限递归),
|
||||
* 子进程的汇总行仍包含本用例本身 —— README 数字即含守卫自身,自洽。
|
||||
*/
|
||||
|
||||
const root = join(import.meta.dirname, '../..');
|
||||
const readme = readFileSync(join(root, 'README.md'), 'utf8');
|
||||
const isChild = !!process.env['NA_DOCS_NUMBERS_CHILD'];
|
||||
|
||||
/**
|
||||
* README 数字行:**52 项能力** · **42 个 MCP 工具** · **467 项单元测试** · **50 项端到端用例** · **23 项 Rust 壳单测**
|
||||
* e2e 一项容许带可选中缀注释(如「(34 通用 + 16 Windows 专属)」)。
|
||||
*/
|
||||
function readmeNumbers() {
|
||||
const m =
|
||||
/\*\*(\d+) 项能力\*\* · \*\*(\d+) 个 MCP 工具\*\* · \*\*(\d+) 项单元测试\*\* · \*\*(\d+) 项端到端用例\*\*(?:([^)]*))? · \*\*(\d+) 项 Rust 壳单测\*\*/.exec(
|
||||
readme,
|
||||
);
|
||||
assert.ok(m, 'README 中未找到数字行(格式应为「**N 项能力** · **N 个 MCP 工具** · ...」)');
|
||||
return {
|
||||
capabilities: Number(m[1]),
|
||||
mcpTools: Number(m[2]),
|
||||
unitTests: Number(m[3]),
|
||||
e2e: Number(m[4]),
|
||||
rust: Number(m[5]),
|
||||
};
|
||||
}
|
||||
|
||||
function countFiles(dir, ext) {
|
||||
return readdirSync(join(root, dir))
|
||||
.filter((f) => f.endsWith(ext))
|
||||
.map((f) => join(root, dir, f));
|
||||
}
|
||||
|
||||
test('README 能力数与 manifest 实测一致(增能力不改 README 会红)', () => {
|
||||
const manifestFiles = readdirSync(join(root, 'packages/protocol/src/capabilities/manifest')).filter((f) =>
|
||||
f.endsWith('.ts'),
|
||||
);
|
||||
let caps = 0;
|
||||
for (const f of manifestFiles) {
|
||||
const src = readFileSync(join(root, 'packages/protocol/src/capabilities/manifest', f), 'utf8');
|
||||
caps += [...src.matchAll(/name: CapabilityNames\./g)].length;
|
||||
}
|
||||
assert.equal(readmeNumbers().capabilities, caps, `README 能力数与 manifest 不符(实测 ${caps})`);
|
||||
});
|
||||
|
||||
test('README MCP 工具数与 defs.ts 实测一致', () => {
|
||||
const src = readFileSync(join(root, 'apps/mcp/src/tools/defs.ts'), 'utf8');
|
||||
const n = [...src.matchAll(/name: 'na_/g)].length;
|
||||
assert.equal(readmeNumbers().mcpTools, n, `README MCP 工具数与 defs.ts 不符(实测 ${n})`);
|
||||
});
|
||||
|
||||
test('README MCP 工具表包含 na_audit_l2(v2.1.0 新工具必须上榜)', () => {
|
||||
assert.match(readme, /na_audit_l2/, 'README 工具表缺少 na_audit_l2 —— 新增 MCP 工具必须同步文档');
|
||||
});
|
||||
|
||||
test('README 单元测试数与 node --test 实测一致', () => {
|
||||
const documented = readmeNumbers().unitTests;
|
||||
if (isChild) {
|
||||
// 子进程(被本守卫 spawn 的全量运行)内不再递归 spawn:
|
||||
// 总数由父进程从子进程汇总行读取,此处直接通过与父进程相同的值。
|
||||
assert.ok(documented > 0, 'README 单测数应为正数');
|
||||
return;
|
||||
}
|
||||
const files = countFiles('tests/unit', '.test.mjs');
|
||||
// ⚠️ 必须剥掉 NODE_TEST_CONTEXT:node --test 多文件模式下每个测试文件跑在
|
||||
// child-v8 上下文中,该变量会被我们的子进程继承 —— 子 node 会以为自己
|
||||
// 「已被父测试运行器管理」而拒绝执行测试(秒退、无 TAP 输出)。
|
||||
const childEnv = { ...process.env, NA_DOCS_NUMBERS_CHILD: '1' };
|
||||
for (const k of Object.keys(childEnv)) {
|
||||
if (k.startsWith('NODE_TEST_')) delete childEnv[k];
|
||||
}
|
||||
const out = execFileSync(process.execPath, ['--test', ...files], {
|
||||
cwd: root,
|
||||
env: childEnv,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 120_000,
|
||||
});
|
||||
const matches = [...out.matchAll(/^# tests (\d+)$/gm)];
|
||||
assert.ok(matches.length > 0, '未找到 node --test 汇总行');
|
||||
const actual = Number(matches[matches.length - 1][1]);
|
||||
assert.equal(documented, actual, `README 单测数与实测不符(实测 ${actual},含本守卫自身)`);
|
||||
});
|
||||
|
||||
test('README 端到端用例数与 e2e 源码实测一致', () => {
|
||||
let e2e = 0;
|
||||
for (const f of ['tests/e2e/run.mjs', 'tests/e2e/windows.mjs']) {
|
||||
const src = readFileSync(join(root, f), 'utf8');
|
||||
e2e += [...src.matchAll(/await test\(/g)].length;
|
||||
}
|
||||
assert.equal(readmeNumbers().e2e, e2e, `README e2e 数与 run.mjs+windows.mjs 不符(实测 ${e2e})`);
|
||||
});
|
||||
|
||||
test('README Rust 壳单测数与 src-tauri 源码实测一致', () => {
|
||||
const rustDir = join(root, 'apps/console/src-tauri/src');
|
||||
let n = 0;
|
||||
for (const f of readdirSync(rustDir).filter((x) => x.endsWith('.rs'))) {
|
||||
const src = readFileSync(join(rustDir, f), 'utf8');
|
||||
n += [...src.matchAll(/#\[(tokio::)?test\]/g)].length;
|
||||
}
|
||||
assert.equal(readmeNumbers().rust, n, `README Rust 数与源码实测不符(实测 ${n},CI 在 ubuntu 全量编译运行)`);
|
||||
});
|
||||
Reference in new issue
Block a user