fix(deploy): TMPDIR 只判"未设"(同文件里 HOME 判了可写)+ 环境自足漏了"命令"(journalctl 两处是假绿)

pi 给了"第五次"的两条线索,都在我读得到的地方,逐条实测确认后修完:

**1. TMPDIR 与 HOME 不同规则(就在同一个文件里)**
① `HOME` 那边写了两条规则:`mkdir -p` 对**已存在的不可写目录会返回成功** ⇒ 必须单独判 `-w`;
判据落在"能不能写"不落在"路径像不像"。**同一条规则没落到 ② `TMPDIR` 上** ——
而 ENOSPC 正是这条链的元老问题(四次史里第 3 条就是 TMPDIR)。两种失败形状:
已给但**不可写**(EACCES)、可写但**已满**(`-w` 抓不到,要的是**空间**判定)。
已补 `-d` + `-w` + 可用空间(`df -Pk`,读不到⇒**不据此判定**;`0` 是**真的没有**);
不足 ⇒ 人话 + exit 2。**不 import** 插件那份 `test/lib/tmp-space.mjs`:
`deploy/` 侧要能独立分发,为去重引进平台代码不划算(按既定理由,写最小版本)。
实测 `TMPDIR=/root/nope` ⇒ `[FAIL] 环境不足:TMPDIR=… 不存在或不可写` + 退出码 2。

**2. 环境自足只覆盖"变量",没覆盖"命令" —— 其中 journalctl 两处是假绿**
   这节的要害是 pi 给的那句判据,我认:**"命令不在"必须走 2/红 + 人话;
   "命令在但输出为空"才是判定结果。** 原先两处把两者压成同一个字符串 `"0"`:
   journalctl 失败(被 `2>/dev/null` 吞掉)⇒ grep 读空 ⇒ `fc="0"` ⇒ **打印"无 panic/fatal"**。
   实测复现:`journalctl -u 不存在的-unit | grep -icE 'panic'` ⇒ `fc=[0]`。
   `sse` 那条同形、后果更坏:**把"读不到日志"归因成"插件没连上"**,让人去查密钥。
   ⚠️ 顺带实测:**`PIPESTATUS` 分不开这两种情况**(命令不存在与"存在但无匹配"都给 1),
   所以不能靠管道状态区分 —— 必须**先取输出、成功后再过滤**,命令存在性另做前提检查。
   改法:两处都改成"先取日志、看退出码";读不到 ⇒ `warn` 明说"读不到、无法据此判断"
   (既不假绿也不假红)。并给三个脚本加 `AGENTMAIL_REQUIRE` 前提检查
   (缺一个 ⇒ exit 2 + 人话),与四次史的处理**同形**,只是对象从变量换成命令。
   实测:`AGENTMAIL_REQUIRE` 里放不存在的命令 ⇒ 退出码 2。

**3. 顺带修 pi 点到的两处同族问题**
· `install.sh` 的 `HEAD_REV="$(git … rev-parse --short HEAD)"`:`set -e` 下失败**直接中止**
  (实测退出码 127、无翻译);而且 HEAD_REV 为空会让下一句报
  "这个包比源码旧:产物 gitRev=… ≠ HEAD=" —— **把"这里不是 git 仓库"说成"产物过期"**。
  已改成显式判失败 + 明说"读不到当前 HEAD,跳过新旧比对"。
· 同块第 94 行末尾挂着一个 `|| true` ⇒ 整行退出码恒 0 ⇒ 它作为 `if` 条件**永远为真**
  ("判据的形式在、区分力不在")。已改成显式计算、去掉 `|| true`。

docs 补两条纪律:15「"命令不在" ≠ "命令在但输出为空"」(含 PIPESTATUS 分不开的实测)、
16「一条规则写了,要检查它是否落到了所有同类对象上」。

验证:install.sh --check 空环境 exit 0、正常 exit 0;TMPDIR 不可写 exit 2;
npm test exit 0;prune 自检 22/22;drift 自检 35/0;check-shared-libs exit 0。
This commit is contained in:
2026-09-14 21:08:48 +08:00
parent 3be824849d
commit 8e3b04a267
5 changed files with 149 additions and 13 deletions

View File

@ -219,7 +219,27 @@ bash deploy/prune-deploy-artifacts.sh --self-check # 判据自检(16 项,
**不是**"自检的结论":自检本体 28/28 全过,但同一个进程接着跑了宿主判据、
于是整体 exit 1。报"自检失败"就是把两个量纲混成一个。
⇒ 报结论时**分开说**:"自检本体 N/M 通过;整体退出码还包含 X"。
15. **注释里的数字无法被判据守住。** 同一文件里曾同时写"写点五处"和"共 6 处"
15. **"命令不在" ≠ "命令在但输出为空"。** 把两者压成同一个字符串就会产出假绿 ——
实例(pi 评审 2026-09-14,实测复现):
`fc="$(journalctl … 2>/dev/null | grep -icE 'panic|fatal|SIGSEGV' || true)"`,
journalctl 失败(无权限读日志 / unit 不存在 / dbus 不通)⇒ 错误被 `2>/dev/null` 吞掉
⇒ grep 读空输入 ⇒ 输出 `0`、退出 1 ⇒ `|| true` ⇒ `fc="0"` ⇒ **打印"近 2 分钟无 panic/fatal"**。
下面那条 `sse` 同形,后果更坏:**把"工具缺失/读不到"归因成"插件没连上"**,
提示人去查密钥,而问题在日志读不到 —— 一条把人引向错误方向的假绿。
⚠️ 顺带实测:**`PIPESTATUS` 分不开这两种情况**(命令不存在与"存在但无匹配"都给 `1`),
所以不能靠管道状态区分,必须**先把输出取出来、成功后再过滤**;
"命令是否存在"另用 `command -v` 做前提检查(`AGENTMAIL_REQUIRE`)。
⇒ 规矩:**"命令不在"走 2/红 + 人话;"命令在但输出为空"才是判定结果。**
同族放宽:环境自足不能只覆盖**变量**,也要覆盖**命令**(`journalctl`/`curl`/`systemctl`/
`git`/`go`/`npm` 都曾是被假设存在的那一类)。
16. **一条规则写了,要检查它是否落到了"所有同类对象"上 —— 而不是只落到你想到的那一个。**
实例(pi 评审 2026-09-14):我在 `env-defaults.sh` 的 `HOME` 上写了两条规则
("`mkdir -p` 对已存在的不可写目录会返回成功 ⇒ 必须单独判 `-w`"、"判据落在能不能写、
不落在路径像不像"),**同一条规则没落到紧邻的 `TMPDIR` 上** —— 而 ENOSPC 正是这条链的
元老问题。两种失败形状(不可写 / 写满)都在**中间**炸,报错看起来像工程问题。
⇒ 同族的"变量"与"命令"是同一张表的两列,写规则时要把表**列全**:
设了要判"可写"、判了可写还要判"有空间"(`0` 是**真的没有**,读不到才是"不知道")。
17. **注释里的数字无法被判据守住。** 同一文件里曾同时写"写点五处"和"共 6 处"
(且它的式子 2+2+3 加起来是 7,实际 10 处)——**三处说法三个数**。
与"不要手抄期望用例数常量"同源:**两组矛盾的数字比没有数字更糟**,
因为它让读者以为有人数过。要判覆盖完整只能靠**机制**(整段 try/catch),不靠数数。