---
name: 2026-08-09-issue236-241-curl-timeout-misread-as-enospc
description: >
  六个 nightly 自动开的 issue #236–#241,标签 `ci` 不是 `bug`,**一个根因,而且我一开始判错了**。
  **头条是那个误判**:当天早上的定时巡检里我把 `exit code 28` 读成 **ENOSPC(磁盘写满)**,据此在飞书
  报告里写了「B 类:我们自己的 workflow 暴露,磁盘写满」,还建议「在 oracle 源码构建前腾空间或缓存构建
  产物」。**28 是 curl 的 `CURLE_OPERATION_TIMEDOUT`。** 我看到 `28` 想起 `ENOSPC` 的 errno 是 28 就直接
  接上了,而**那一步是 `curl`,shell 的退出码来自 curl 的退出码表,不是 errno 表**——两张表恰好在 28
  这个数字上都有条目。证据一直在日志里:`apt` 在 16:55:03 成功结束,然后**沉默 2 分 15 秒**才报 exit 28,
  而磁盘写满是**立刻**失败的、不会先卡两分钟;另外两个 run 的日志里 `no space left` / `ENOSPC` /
  `disk full` 出现次数都是 **0**。**真根因**:失败步骤 `install lua 5.1.5 oracle (source build)` 里是
  裸的 `curl -sLO https://www.lua.org/ftp/lua-5.1.5.tar.gz`,**没有 `--max-time`、没有
  `--connect-timeout`、没有 retry**,上游一次可达性抖动就让它挂到 shell 放弃。**后果比「一次红」更糟**:
  这一步失败让后面三个差分 fuzz 步骤被 **skip**(step 5/7/9 是 `skipped`),于是 nightly 报 failure 而
  **实际什么都没测**。**而且一次抖动开了六个 issue**:infra issue 标题里嵌了 `${{ matrix.variant }}`,
  p1/p3/p4 生成三个不同标题,现有的按标题去重看不出它们是同一件事,两次抖动 × 三个 tier = 六个 issue。
  **修法**:四处 call site(`ci.yml` ×2、`bench-acceptance.yml` ×1、`nightly-diff-fuzz.yml` ×1)统一改用
  新的 `scripts/fetch-lua-tarball.sh`——限时 + 重试(`--retry` 已覆盖超时,`--retry-all-errors` 额外放宽)
  + 下载后校验官方 SHA-256 再解包 + 复用已校验的 tarball(cache 命中完全跳过网络);**不做镜像**
  (`github.com/lua/lua` 没有 5.1.5 tag,重新打包的副本过不了官方 checksum,而 checksum 这个性质比
  「多一个源」更值钱);infra issue 标题改成按**日期**去重(`run_id` 更差:一天六轮就是六个 issue),第二、三个 job 改为评论。新增
  `scripts/test-fetch-lua-tarball.sh` 五个用例**全部离线**(`file://` origin),所以这个测试自己不会变成
  一个会抖的 CI 步骤——而这正是本轮改动的用意。**五条教训**:① 同一个数字在不同的退出码表里含义不同,
  先确认这个退出码是**谁**产生的;② **时间形式是区分故障类别的免费证据**(「apt 成功之后沉默 2 分 15 秒」
  直接排除磁盘写满,资源耗尽立刻失败、超时才会先卡);③ 一个失败步骤让后续步骤 skip,会造出「报红但
  什么都没测」,比直接失败更坏;④ 自动开 issue 的去重键必须与「一次事件」的粒度对齐;⑤ 检查「某个标志
  存在」的测试不能 grep 整个文件——第一版就因为那个词在注释里还在而照旧通过。
metadata:
  type: reflection
  date: 2026-08-09
---

# 28 是谁的 28(2026-08-09,commit `81acb57`)

> 范围:分支 `fix/236-241-nightly-enospc`,1 个 commit `81acb57`。改动全在工程侧:
> `scripts/fetch-lua-tarball.sh`(新增)、`scripts/test-fetch-lua-tarball.sh`(新增)、
> `.github/workflows/ci.yml`(两处 call site)、`.github/workflows/bench-acceptance.yml`(一处)、
> `.github/workflows/nightly-diff-fuzz.yml`(一处 call site + infra issue 标题去重键)、
> `Makefile`(`test-scripts` 目标接上新脚本)。**产品代码零改动。**
>
> 分支名 `fix/236-241-nightly-enospc` 里的 `enospc` 是我最初那个误判留下的痕迹,已经不准确;
> 留着是因为分支名不值得改,但读到它的人应该知道它指的是**超时**而不是磁盘。

## 任务

六个 nightly 自动开的 issue #236–#241,标签 `ci`(不是 `bug`)。一个根因。

## 0. 头条:我把 28 接到了错的那张表上

这一轮最重要的产出不是修法,是我自己的误判过程。

当天早上的定时巡检里,我把 `exit code 28` 读成 **ENOSPC(磁盘写满)**,并据此在飞书报告里写了
「B 类:我们自己的 workflow 暴露,磁盘写满」,还建议「在 oracle 源码构建前腾空间或缓存构建产物」。

**这是错的。28 是 curl 的 `CURLE_OPERATION_TIMEDOUT`。**

我当时是怎么错的:我看到 `exit code 28`,想起 `ENOSPC` 的 errno 是 28,就直接接上了 —— 而**这一步是
`curl`,shell 的退出码来自 curl 的退出码表,不是 errno 表**。两张表恰好在 28 这个数字上都有条目:

| 表 | 28 的含义 | 谁产生它 |
|---|---|---|
| POSIX `errno.h` | `ENOSPC`(No space left on device) | 一个失败的**系统调用**,经 `perror` / Go 的 `syscall.Errno` 之类的通道显示出来 |
| curl 退出码 | `CURLE_OPERATION_TIMEDOUT` | `curl` 进程自己的 `exit()`,shell 读到的就是这个 |

而**证据一直在日志里**,两条:

1. **时间形式**:`apt` 在 16:55:03 成功结束,然后**沉默 2 分 15 秒**才报 exit 28。磁盘写满是**立刻**
   失败的 —— 写入撞上没有空间就当次返回错误,不会先卡两分钟。会先卡的只有等待类失败。
2. **反向搜索为零**:另外两个 run 的日志里 `no space left` / `ENOSPC` / `disk full` 的出现次数都是
   **0**。如果真是磁盘写满,`apt`、`tar`、`make` 里总有一个会把这句话打出来。

两条都是免费的,都在我已经打开的那个日志里,而我一条都没用上就下了结论 —— 因为「28 = ENOSPC」这个
联想来得太快,快到我没有先问「这个 28 是谁的 28」。

## 1. 真根因:一个没有任何界限的 curl

失败步骤是 nightly 的 `install lua 5.1.5 oracle (source build)`,里面是:

```
curl -sLO https://www.lua.org/ftp/lua-5.1.5.tar.gz
```

**没有 `--max-time`、没有 `--connect-timeout`、没有 retry。** 上游一次可达性抖动就让它挂到 shell
放弃 —— 那 2 分 15 秒就是它在等,而 exit 28 就是它等到自己放弃。

## 2. 后果比「一次红」更糟:报红,而且什么都没测

这一步失败会让后面三个差分 fuzz 步骤被 **skip**(那两个 run 的 step 5/7/9 状态是 `skipped`),
于是 nightly 报 failure 而**实际什么都没测**。

这比「跑了并且发现问题」更坏,因为**红色看起来像后者**。一次真分歧和一次「装依赖没装上」在
Actions 页面上是同一个红叉,而前者要人立刻去读,后者只要重跑。更糟的是反过来的情形:这一晚
本来该跑的三条差分 fuzz 腿一条都没跑,那一晚的探索预算是零,而没有任何东西说出这件事。

## 3. 一次抖动开了六个 issue:去重键选错了粒度

infra issue 的标题原先是:

```
TITLE="nightly-fuzz infra failure (${{ matrix.variant }}, $(date -u +%F))"
```

标题里嵌了 `${{ matrix.variant }}`,于是 p1/p3/p4 三个 job 生成**三个不同的标题**,而下面那段
「按标题去重」的逻辑(`gh issue list --search "in:title ..."`,命中就评论、不命中就新开)看不出
它们是同一件事。两次抖动 × 三个 tier = **六个 issue**。

关键点在于:**infra 失败天然横跨所有 tier**。装不上依赖、oracle 不可用、上游不可达,这些与
被测的是 p1 还是 p4 毫无关系;而 divergence 失败恰恰相反,它天然属于某一个 tier。同一个标题
模板服务了这两类性质不同的事件,而 tier 这一维只对其中一类有意义。

修法最终把标题的键换成**日期** —— 三个 tier 与一天六次 schedule 都收拢到一个 issue;第一个报的
job 开 issue,其余评论(评论里带上自己的 tier),`BODY` 里记下「首个报告的 tier」。`run_id` 只能合并
同一次 run 的三个 tier,一天上游持续故障仍会开六个 issue,因此不是最终键。

## 4. 修法

### 4.1 一个共享的取包脚本

四处 call site 有同样的裸 curl:`ci.yml` ×2(test 与 difftest 两个 job 的 macOS cache-miss 分支)、
`bench-acceptance.yml` ×1、`nightly-diff-fuzz.yml` ×1。现在统一走
`scripts/fetch-lua-tarball.sh`,四个性质:

| 性质 | 写法 | 为什么 |
|---|---|---|
| **限时** | `--connect-timeout 15 --max-time 120` | 挂住的取包应该快速失败,而不是把整个步骤耗掉 |
| **重试** | `--retry 4 --retry-delay 5 --retry-all-errors` | 观察到的失败是瞬时的。`--retry` 才是关键 —— 它的默认值是 0,所以旧的裸 curl 根本不重试。`--retry-all-errors` 只是额外放宽,**不是**超时重试的前提:curl 手册写的是「transient error means **either: a timeout**, an FTP 4xx ... 」,所以单靠 `--retry` 就能覆盖 #236–#241 那次失败。此前把它写成前提是错的,记在这里因为那曾是保留这个 flag 的唯一理由。|
| **校验** | 解包**之前**核对官方 SHA-256 | 截断或被替换的下载不能被静默编译进差分 oracle —— 那会让整轮差分结论失效而且无声 |
| **复用** | 已存在且校验通过的 tarball 直接用 | 于是 actions cache 命中可以完全跳过网络 |

**不做镜像**,这是一个明确的取舍:`github.com/lua/lua` 没有 5.1.5 tag,任何重新打包的副本都过不了
官方 checksum,而 checksum 这个性质比「多一个源」更值钱 —— 多一个源降低的是取包失败率(重试已经在
降它),而 checksum 防的是「取到了错的东西并且编译了它」,后者的失败是无声的。

### 4.2 去重键换成日期

见上面第 3 节。注释里写清了「标题里**故意不放** `matrix.variant`」以及为什么,因为下一个读它的人
最自然的改动方向就是把 tier 加回去(标题里没有 tier 看起来像信息丢失)。

### 4.3 一个不会自己变成抖动源的自测

新增 `scripts/test-fetch-lua-tarball.sh`,挂进 `Makefile` 的 `test-scripts` 目标(该目标同时在
`make all` 与 `ci.yml` 的独立 job 里跑,纪律来自 #179)。五个用例:

1. checksum 不符 → 必须拒绝、删掉文件、非零退出、**不解包**;
2. checksum 正确 → 必须解包;
3. 已有一个校验通过的 tarball,而 URL 指向一个不存在的文件 → 仍然成功,证明走了缓存而不是网络;
4. 真脚本的 curl 调用必须带 `--connect-timeout` / `--max-time` / `--retry`。

**五个用例全部离线**(用 `file://` origin),于是这个测试自己不会变成一个会抖的 CI 步骤 ——
而这正是本轮改动的用意。一个「防住上游抖动」的测试如果自己依赖上游,它加的是噪声不是防线。

每个用例都用变异实测过。其中第 5 条**第一版写错了**:我 grep 整个文件,于是把 `--max-time` 从
curl 调用里删掉之后它**照旧通过** —— 因为那个词在文件顶部的注释里还在(注释里正好写着
「bounded: `--connect-timeout` and `--max-time`」)。改成只截取 curl 那条调用再检查。

## 期望与实际

| 期望 | 实际 |
|---|---|
| 六个 issue,至少几个不同的原因 | 一个根因,一次上游抖动 |
| exit 28 是 ENOSPC,该去腾磁盘空间 | 是 curl 的超时;整个 run 里没有任何磁盘证据,而且时间形式一开始就排除了磁盘 |
| 这一晚 nightly 报红是因为撞到了什么 | 这一晚**什么都没测**,三条差分 fuzz 腿全被 skip |
| 六个 issue 是六次事件 | 两次事件,被标题里的 tier 拆成了六份 |
| 修法核心是加 `--max-time` | 限时只是四个性质里的一个;`--retry` 本身已覆盖超时,`--retry-all-errors` 只是额外放宽,而 checksum 防的是另一类(无声的)失败 |
| 加个镜像源更稳 | 镜像过不了官方 checksum,取舍反了 |
| 「flag 存在」这种检查一 grep 就完了 | grep 整个文件时注释满足了它,测试对代码零约束 |

## 教训

### 教训 1(头条:同一个数字在不同的退出码表里含义不同;先确认这个退出码是**谁**产生的)

我把 `exit code 28` 接上了 `ENOSPC`(errno 28),而那一步的退出码来自 `curl`
(`CURLE_OPERATION_TIMEDOUT`)。

**为什么容易错**:数字退出码看起来像一个全局命名空间,而它根本不是 —— 每个程序有自己的退出码表,
shell 报的是**最后一条命令**的退出码。而 errno 表因为用得最多、最先被记住,会变成默认的解释来源;
一个数字在两张表里都有条目时,先被想起来的那张就赢了,而「先被想起来」与「正确」没有关系。这一次
两张表都非空、都在讲一种资源类失败(空间 / 时间),所以错误的那个解释读起来完全自然。

**判据**:看到一个数字退出码,**先定位是哪个命令退出的,再去查那个命令自己的退出码表**;shell 报的
是最后一条命令的退出码,不是 errno。自查办法:说出「28 是 X」之前,先说出「28 是**谁的** 28」——
说不出主语就说明还没定位到命令,这时任何解释都是猜。

推论:这条对 Go 的 `exit status N` 同样成立,而本仓已经在
[[unreproducible-crasher-triage]]「静默死亡先做两步分类」里用过一次它的另一半 —— 那一节说
`exit status 2` 是 **Go runtime 自身** fatal 的退出码(所以必然有一份栈迹),`signal: killed`
才是 OS 层。那一节回答的是「有没有输出可找」,本条回答的是「这个数字属于谁的表」;是同一族的
上一格:**先确认退出方式的归属,再解释它的含义**。

### 教训 2(时间形式是区分故障类别的免费证据)

「apt 成功之后沉默 2 分 15 秒才失败」直接排除了磁盘写满 —— 资源耗尽是**立刻**失败的,超时才会
先卡。

**为什么容易错**:错误码是**显式的**(它就打在那里、看起来像答案),而时间戳是**隐式的**(要自己
去减),于是注意力天然落在码上。但错误码只说「失败了,而且是这一类」,时间形式说的是「失败发生
在等待之后还是尝试的瞬间」,而后者能一刀切开好几类:

| 时间形式 | 排除掉的 | 剩下的 |
|---|---|---|
| **立刻**失败(<1s) | 超时、锁等待、重试耗尽 | 资源耗尽(空间 / 配额 / 权限)、参数错误、文件不存在 |
| 失败前有**长时间沉默**(分钟级) | 资源耗尽、参数错误 | 网络超时、锁等待、上游挂住、看门狗 |
| 失败前有**周期性**输出 | 挂住 | 重试循环、轮询 |

**判据**:分类一个 CI 失败时**先看时间戳之间的间隔**,它往往比错误码本身更能说明是哪一类;而且它
在日志里是免费的。自查办法:在写下「这是 X 类失败」之前,先说出「这一步花了多久」以及「X 类失败
应该花多久」;两者量级不符就说明分类错了。

与本仓已有纪律的关系:[[unreproducible-crasher-triage]]「上限的余量」那一节已经在用**时间**做判据,
但用法是**前瞻的**(预测一个上限允许的量在 CI 上要跑多久、与看门狗比余量);本条是**回溯的**
(从已经发生的耗时反推故障类别)。同一个量,两个方向。#224/#225 那一轮的 `panic: deadlocked`
也是一个例子:那次判对了(10 秒看门狗),而判对的关键同样是把时间量出来。

### 教训 3(一个失败步骤让后续步骤 skip,会造出「报红但什么都没测」)

nightly 的 step 5/7/9 是 `skipped`,而整个 run 报 failure。

**为什么容易错**:红色的**默认含义**是「跑了并且发现了问题」,这个默认在绝大多数情况下是对的,
所以看到红色时的第一动作是去找「发现了什么」。而依赖步骤失败造成的红色含义相反 ——「没跑」——
它与真失败**在同一个信号上**(一个红叉、一封同样的通知),区分它们需要点开去看每一步的状态,
而那正是人在看到红色时最不会先做的事。危险方向是**乐观**的那一侧:一晚的探索预算变成零,而
没有任何东西说出这件事;真分歧至少会被人读到。

**判据**:给 CI 加依赖步骤时,问一句「**这一步挂了之后,后面被 skip 的步骤里有没有本来该产生结论
的**」;有的话,失败信息里要写清「未执行」而不是只写「失败」。自查办法:把一个 workflow 的步骤
列成两栏 —— 一栏是「准备类」(装依赖、取包、建缓存),一栏是「产生结论类」(测试、fuzz、比对);
凡是准备类步骤失败会 skip 掉结论类步骤的,那条失败路径就需要一句显式的「本轮未测」。

本轮两半都已补上:去重键按日期收拢同一天所有 run 与 tier;infra issue 的 body 读取
`steps.difffuzz.outcome`,差分步骤被 skip 时明确写出「本轮未执行任何差分 fuzz」。

### 教训 4(自动开 issue 的去重键,必须与「一次事件」的粒度对齐)

infra 失败天然横跨所有 tier,而标题里嵌 tier 就让一次事件变成三个 issue。

**为什么容易错**:去重键最自然的写法是「把能唯一标识这次失败的东西都放进标题」,而这个直觉在
divergence 类事件上是对的(它天然属于某个 tier,tier 是它身份的一部分)。同一个标题模板服务两类
性质不同的事件时,更细的那一维会被无条件继承下来 —— 而**多加一维只会让分组更细,永远不会报错**,
所以这个错误没有任何自动信号,它的全部代价都落在读 issue 的人身上。

**判据**:给自动化加去重时,先问「**这类事件的天然单位是什么**」(这里是一次 run),再选键;按最细
的维度分组会把一件事拆成 N 件,而那 N 件都要人去读。自查办法:对每一维问「这一维变化时,是同一件
事还是两件事」——答「同一件事」的维度就不能进键(这里:tier 变化时仍是同一次上游抖动,所以 tier
不进键;run 变化时是两次事件,所以 run 进键)。

配套的实现要点:被去掉的那一维**不能丢**,只是要从标题挪到正文 —— 现在 tier 出现在 body 的
「首个报告的 tier」与后续 job 的评论里。去重键与信息量是两件事。

### 教训 5(检查「某个标志存在」的测试,不能 grep 整个文件)

第 4 个用例第一版 grep 文件,于是 `--max-time` 被从 curl 调用里删掉后测试照旧通过 —— 因为那个词在
注释里还在。

**为什么容易错**:一个 shell 脚本没有 AST 可查,「检查代码属性」最方便的实现就是文本搜索,而文本
搜索的作用域默认是整个文件。更糟的是,**这个脚本的注释里必然提到那些标志**(注释就是在解释为什么
要限时),所以注释与代码在文本上不可区分,而且注释越写得好、这个测试越容易假绿。

**判据**:检查代码属性的测试要作用在**代码本身**上(这里是那条 curl 调用),而不是文件文本;
**一个注释就能满足的测试没有在测代码**。自查办法:把被检查的那个属性从代码里删掉、但保留一切
注释,跑一次 —— 不变红就是没有在测代码。

这是「绿色 ≠ 在测你以为在测的」的一个廉价新实例,与 [[prove-the-path-under-test]] 的
[[2026-07-25-issue179-test-go-fuzz-retry-revive-round]] 教训 1 同族(那条讲 shell 判据只用
`rc` + 反向 grep 会假绿,必须配 positive marker),载体也一样是 shell 自测脚本。本条补的是新的
一格:**positive marker 的作用域也要证明** —— 本轮第 4 个用例有 positive marker(它 grep 的是
「必须出现」的字节串,不是反向断言),假绿的原因是那个 marker 的**搜索范围**里有一个非代码的
来源。所以那条纪律要读成两半:有 positive marker,而且它只能被被测代码满足。

同时这也是 [[unreproducible-crasher-triage]]「一个防住某个数值的回归测试必须用变异实测确认」
在**非数值属性**上的形式:那条管「把被防的值改回去」,本条管「把被防的标志删掉」,自查动作是
一样的,而本轮的变异实测确实抓到了它 —— **变异实测是这一轮唯一发现这个缺陷的手段**,读代码
读不出来(那段 grep 逻辑本身完全正常)。

## Promotion 决策

- **教训 1 / 教训 2 → [[unreproducible-crasher-triage]]**:「静默死亡先做两步分类」那一节,
  它已有的第 1 步是「先分类退出方式:`exit status 2` 是 Go runtime fatal / `signal: killed` 是
  OS 层」,与本轮是同一族(**先分类退出方式,再解释含义**),所以两条作为该节的前置一格并入:
  教训 1 是「这个退出码属于谁的表」(比既有那一步更前,因为它连主语都还没定),教训 2 是
  「时间形式先于错误码」(一个与退出码正交的、免费的分类维度)。
- **教训 3 / 教训 4 → [[unreproducible-crasher-triage]]** 新节「CI 自动化本身的失败信号」:
  两条都不是 crasher 分诊,而是**产生 crasher issue 的那套机制**自己的缺陷,而本篇是仓里
  唯一讲「nightly 报上来的东西怎么读」的 guide,自动化侧的信号质量属于同一入口。教训 3 管
  「红色的含义可能是没跑」,教训 4 管「一次事件被拆成 N 个 issue」——分诊的第一步是读 issue,
  而这两条决定了那批 issue 是不是可信的输入。
- **教训 5 → [[prove-the-path-under-test]]** §2 反模式那一族(与 §4.9x / §9.1a 相邻):
  「检查代码属性的测试作用在文件文本上」是该家族的一个新载体,并且给 #179 那条 shell 纪律补上
  「positive marker 的作用域也要证明」这一半。

## 触发场景

- **看到一个数字退出码时** → 先说出「这是**谁的**退出码」,再去查那个命令自己的退出码表;
  说不出主语就别解释它。errno 表不是默认答案。
- **分类一个 CI 失败时** → 先把时间戳减一遍。立刻失败与「沉默几分钟后失败」是两类不同的故障,
  这个证据在日志里免费,而且往往比错误码更能定类。
- **给一个故障下了「X 类」的定性之后** → 反向搜一次那一类的特征字符串(磁盘类搜
  `no space left` / `ENOSPC`),出现零次就该重新想。
- **给 CI 加依赖步骤时** → 问「这一步挂了之后,后面被 skip 的步骤里有没有本来该产生结论的」;
  有的话失败信息要写「未执行」而不只是「失败」。
- **给自动开 issue / 告警加去重键时** → 先问「这类事件的天然单位是什么」;对每一维问「这一维
  变化时是同一件事还是两件事」,答「同一件事」的不进键(但要挪进正文,别丢)。
- **给 shell 脚本或配置写「某个标志/字段存在」的自测时** → 检查代码本身而不是文件文本;写完
  把那个标志删掉、保留注释跑一次,不变红就是没测到。
- **给一个「防住外部抖动」的东西写测试时** → 让那个测试本身离线(本轮用 `file://` origin);
  依赖上游的防抖测试加的是噪声不是防线。

## 关联

[[unreproducible-crasher-triage]](「静默死亡先做两步分类」是教训 1/2 的落点,同族的上一格:
先确认退出方式的归属再解释含义;「上限的余量」那一节用时间做**前瞻**判据,本轮教训 2 是同一个量的
**回溯**用法;新节「CI 自动化本身的失败信号」是教训 3/4 的落点)·
[[prove-the-path-under-test]](教训 5 的落点,§2 反模式家族的 shell 载体;§4.9x 与 §9.1a 是最近的
邻居 —— 都是「判据恰好落在成立的那一侧」)·
[[2026-07-25-issue179-test-go-fuzz-retry-revive-round]](同一个 `test-scripts` 门禁、同样是 shell
自测脚本的假绿;那轮定下「必须配 positive marker」,本轮补「marker 的作用域也要证明」,并且
tooling 脚本接门禁那条纪律让本轮的新测试有地方挂)·
[[2026-08-04-issue224-225-watchdog-margin]](同样是「产品代码零改动,价值全在诊断上」的一轮;
那轮的 `panic: deadlocked` 也是靠把时间量出来才判对故障类别)·
[[2026-07-19-fuzz-worker-forensics-round]](`exit status 2` 的语义是那一轮的头条,教训 1 与它是
同一族的两格)·
`scripts/fetch-lua-tarball.sh` · `scripts/test-fetch-lua-tarball.sh` ·
`.github/workflows/nightly-diff-fuzz.yml`(oracle 源码构建步骤 + infra issue 去重键)·
`.github/workflows/ci.yml`(test 与 difftest 两个 job 的 macOS cache-miss 分支)·
`.github/workflows/bench-acceptance.yml` · `Makefile`(`test-scripts`)·
`docs/design/engineering.md` §3.2 / §4(nightly 机制与 oracle 的 CI 供给)·
`docs/design/p1-interpreter/12-testing-difftest.md` §8(CI 门禁)
