ci: 注册三个从没进 CI 的 Docker 门,build-arg 改推导(第四个已过时,另开 issue) - #803
Conversation
|
CI diagnosis — DO-NOT-MERGE current HEAD Failing check: L0 + L1 (report-only), run https://github.com/sleep2agi/agent-network/actions/runs/31656458158/job/94311893856. Raw sequence is decisive: No Docker build log or suite output follows because Docker was never invoked. Root cause is introduced by this PR: returns status 1 for the normal case where a Dockerfile declares no SOURCE_COMMIT ARG. The assignment itself therefore terminates the script on the first legacy suite ( Focused repair should make zero matches an explicit valid case while retaining fail-closed handling for malformed/multiple declarations. Prefer collecting normalized matches with a zero-safe parser and a 0/1/>1 case split; do not merely append a blanket No code change made by reviewer; waiting for an append-only author fix and new frozen coordinates. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 47f17d7761
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| arg_name=$(grep -oE '^ARG (SOURCE_COMMIT|TEST[0-9]+_SOURCE_COMMIT)' \ | ||
| "tests/$t/Dockerfile" 2>/dev/null | head -1 | awk '{print $2}') |
There was a problem hiding this comment.
Tolerate Dockerfiles without a source-commit ARG
For every existing qa-* suite, including the first entry qa-cli-01-hub-start, the Dockerfile has no matching ARG. Under the script's set -euo pipefail, grep therefore returns 1 and the assignment terminates the script before the first docker build, so bash scripts/qa.sh --l1 cannot run any L1 tests. Make the no-match case explicitly successful before inspecting arg_name.
AGENTS.md reference: AGENTS.md:L16-L16
Useful? React with 👍 / 👎.
| # 下面三个是 2026-08-13 扫出来的:形状完整的 Docker 门(Dockerfile + run.sh | ||
| # + 自己的 mutation),但从没被注册进 L1_TESTS,所以一直没人跑。 | ||
| # 本地实测三个都 PASS(39s / 15s / 36s;L1 是并行跑的)。 | ||
| "test224-grok-preview-security" |
There was a problem hiding this comment.
Disable networking when running test224
When this newly registered security suite is eventually reached, the shared runner invokes docker run --rm without the --network none required by tests/test224-grok-preview-security/Dockerfile and explicitly documented for test224 in tests/test225-grok-preview-package-live/README.md. Its output nevertheless unconditionally reports network: disabled by runner and a network-disabled PASS, producing false security evidence while the container retains normal Docker networking; add suite-specific run arguments or otherwise enforce the offline mode.
Useful? React with 👍 / 👎.
| "test224-grok-preview-security" | ||
| "test597-dashboard-slash-namespace" | ||
| "test679-task-trace" |
There was a problem hiding this comment.
Trigger QA when registered suites change
A PR that changes only one of these three test directories will not run this newly registered gate: .github/workflows/qa.yml includes tests/qa-*/** and a few individually named test* directories, but none of test224, test597, or test679. Add these paths (or a suitable common pattern) to both workflow path filters so changes to the gates themselves receive the required Docker E2E validation.
AGENTS.md reference: AGENTS.md:L16-L16
Useful? React with 👍 / 👎.
| "test224-grok-preview-security" | ||
| "test597-dashboard-slash-namespace" | ||
| "test679-task-trace" |
There was a problem hiding this comment.
Pin the Bun inputs before promoting these suites
These additions reopen the nondeterministic L1 dependency path that docs/tests/report-l1-pinned-bun.txt records as having already caused repeated CI failures: test224 and test597 use mutable oven/bun tags without the repository's pinned digest, while test679 downloads whatever https://bun.sh/install serves during each build. The previous active L1 denominator deliberately had 0 installer calls and used one digest-pinned Bun 1.3.14 image, so registering these suites makes identical commits test against changing bytes and can fail independently of the code under review; convert their Dockerfiles to the same immutable Bun input before adding them to L1.
AGENTS.md reference: AGENTS.md:L25-L25
Useful? React with 👍 / 👎.
|
Second gate on new HEAD The control-flow fix is narrow (one However, the committed report says Required: after current CI completes, rerun the exact merged/source tree with the zero-ARG suite, four old build-arg suites, and three new suites; append a report-only child commit naming the actual source and log/image coordinates. Do not relabel the existing run. The report should also retain its honest limitation that test597/test679 declare but do not enforce SOURCE_COMMIT. Minor structural note, not the primary block: |
cad8d07 to
354bf67
Compare
|
Review gate on current HEAD The third commit materially changes the CI topology: the three recovered suites are no longer in the 5-minute L1 runner; they now run in a separate 12-minute It does not repair the committed report. Required closeout: append a report-only child after the full current check set finishes. It must distinguish the immutable source HEAD No reviewer branch edit or merge performed. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cad8d07667
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - 'tests/test725-agent-node-unit-ci/**' | ||
| - 'tests/test745-agent-network-unit-ci/**' | ||
| - 'tests/test746-setup-bun-pin/**' | ||
| - 'tests/test224-grok-preview-security/**' |
There was a problem hiding this comment.
Trigger the recovered gate for shared helper changes
When a PR changes only tests/lib/safe-rm.sh, neither path filter triggers this workflow even though tests/test224-grok-preview-security/Dockerfile copies that file and its runner sources it. Add tests/lib/** to both the pull-request and push filters so changes to an input of the newly registered security gate receive the required Docker E2E validation.
AGENTS.md reference: AGENTS.md:L16-L16
Useful? React with 👍 / 👎.
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Build test224-grok-preview-security |
There was a problem hiding this comment.
Run the security suite after prerequisite layers
When test597 or test679 contains the first functional or full-flow failure, this job has already run test224-grok-preview-security because the security suite is placed first. Put test224 after the lower-layer suites so security evidence is produced only after the required environment/authentication/communication/flow progression has passed.
AGENTS.md reference: AGENTS.md:L5-L6
Useful? React with 👍 / 👎.
| -f tests/test224-grok-preview-security/Dockerfile . | ||
|
|
||
| - name: Run test224-grok-preview-security | ||
| run: docker run --rm anet-test224-grok-preview-security |
There was a problem hiding this comment.
Preserve recovered-suite reports outside removed containers
On every CI run, test224 writes report-test224.txt under /artifacts inside this container, but --rm deletes that filesystem and the workflow neither mounts nor uploads the report; test597 is discarded the same way, while test679's output is only transient stdout. Capture these results under docs/tests/report-testN.txt in the checked-out workspace and preserve them as artifacts so the newly registered executions leave the required durable test evidence rather than only the older committed reports.
AGENTS.md reference: AGENTS.md:L8-L8
Useful? React with 👍 / 👎.
tests/ 下有四个形状完整的 Docker 门(Dockerfile + run.sh + 自己的 mutation) 从没被注册进 L1_TESTS,所以一直没人跑。逐个跑过之后: test224-grok-preview-security PASS 39s test597-dashboard-slash-namespace PASS 15s test679-task-trace PASS 36s test682-uncovered-task-trace FAIL ← 不注册,另开 issue,见下 三个通过的注册进 L1_TESTS(L1 并行跑,最差加 ~39s 墙钟)。 顺带把 build_args 从硬编码 if/elif 链改成从套件自己的 Dockerfile 推导。 那条链的失效方式是静默的:把套件加进 L1_TESTS 却忘了加分支,它会在没有 SHA 绑定的情况下跑,输出看起来一切正常。而新加的 test224/test597 用的正是 不带前缀的 `ARG SOURCE_COMMIT`,是原链无法表达、只能再加分支的形状。 替换前核过等价性:对原链覆盖的 test686/765/766/746 四个套件,推导结果与 硬编码逐字相同。 推导是否承重,分三种(不传 build-arg 时): test224 → rc=1 FAIL: SOURCE_COMMIT must bind… fail-closed,推导承重 test597 → rc=0 PASS 声明了却不强制 test679 → rc=0 PASS 声明了却不强制 后两个是那两道门自己的弱点,本 PR 不修,写进 NOT COVERED。 test682-uncovered-task-trace 不注册:它断言 cli.ts 里 sendPeerReplyTaskWithTrace( 恰好出现 1 次,实际 0 次。查下来不是烂了,是**过时了** —— #698 有意把 peer reply 改成协商 send_peer_reply 原子工具,那条 send_task 老路被删掉,并由 agent-node/src/reply-routing-source.test.ts 断言它**不得出现** (expect(source).not.toContain("sendPeerReplyTaskWithTrace({"))。 两道门方向相反,而后者在 CI 里跑着且是绿的。另外 agent-node/src/peer-reply-task-trace.ts 现在零生产调用方,只被 test682 自己引用。 单独开 issue,不在本 PR 里删任何东西。
第一版在 CI 上挂了,而且挂得很有欺骗性:失败停在
`· build qa-cli-01-hub-start`,一个套件都没跑成,看起来像「L1 挂了」,
实际是参数推导那一行把 runner 打死了。
根因:scripts/qa.sh 是 set -euo pipefail,而多数套件的 Dockerfile 根本没有
ARG SOURCE_COMMIT —— grep 无命中退 1,pipefail 把 1 传给整个命令替换,
set -e 于是在第一个这样的套件上退出。
我上一版只验了「推导算出来的参数名对不对」(对 7 个套件逐个核过),
没验它在 qa.sh 里跑不跑得通 —— 验了零件没验装配。
修法:命令替换末尾加 || true,并把原因写进注释。
witnessed-red(在真脚本上,不是最小复现):
去掉 || true → rc=1,日志停在 `· build qa-cli-01-hub-start`,与 CI 症状逐字一致
加回 || true → 三种 Dockerfile 形状各取一个跑真 qa.sh --l1:
qa-cli-01-hub-start 无 ARG ✓ PASS
test765-batch-runtime-gate TEST765_ 前缀 ARG ✓ PASS
test597-dashboard-slash-namespace 裸 ARG ✓ PASS
✓ ALL PASS in 84s
上一版把 test224/test597/test679 加进了 L1_TESTS。选错家了。 CI 上 L0+L1 job 的真实耗时(main 近四次):141s / 135s / 148s,预算 300s, 余量约 150s。而 qa.sh 的 build 是**串行**的(只有 docker run 并行),这三个 套件要各加一次 build,其中 test679 带 javascript-obfuscator;单跑 run 已是 39s / 15s / 36s。L1 自称「~16s parallel」,是快层 —— 塞进去是拿余量赌。 改成 qa.yml 里的独立 job `recovered-suites`,预算 12 分钟,形状同单测门。 撤出 L1_TESTS 的原因写进了那里的注释,免得有人再塞一次。 build_args 推导保留在 qa.sh —— 它独立成立:原硬编码 if/elif 链的失效方式是 静默的(套件加进 L1_TESTS 却忘了加分支,会在没有 SHA 绑定的情况下跑)。 等价性核过:对 test686/765/766/746 四个套件,推导与硬编码逐字相同。 三个套件按 job 里逐字相同的命令验证(只传 --build-arg,run 不带 -e): test224 SOURCE_COMMIT rc=0 Summary: PASS test597 SOURCE_COMMIT rc=0 RESULT: PASS test679 TEST679_SOURCE_COMMIT rc=0 RESULT: PASS NOT COVERED:不传 build-arg 时只有 test224 是 fail-closed(rc=1), test597/test679 照样 PASS —— 它们声明了 SOURCE_COMMIT 却不强制。 那是那两道门自己的弱点,本 PR 不修。
2d4bf19 to
6b1953a
Compare
两条都是独立审(codex)在本 PR 上提的 P1,核过属实。 1) test224 是安全套件。它的 Dockerfile 第 13 行明写 「the actual gate is run with --network none」,run.sh 第 160 行会打印 「runtime executed with network disabled」。而我的 job 是裸 docker run --rm —— **那句话在网络实际可用时照样打印**。 实测对照:带与不带 --network none,两次都 rc=0、都打印同一句 Summary, 差异只有时间戳和 tarball sha256。也就是说**套件自己不会拦住这个错误**, 只能由调用方保证。这是我引入的缺陷:把一道安全门接进 CI 时没照它自己的契约调用。 2) test224 的镜像 COPY 了 tests/lib/safe-rm.sh 并 source 它,但 qa.yml 的两处 path 过滤都没有 tests/lib/** —— 只改那个 helper 的 PR 不会触发这道门。 有一条我**不在本 PR 里改**:套件用一行硬编码 log "network: disabled by runner" **声明**前提,而不是探测它。要让它自己红,得加 fail-closed 探测(比如真去 resolve/connect 一次,通了就 fail)。那是改别人的门、会影响所有调用方, 交给 owner 决定,我只报不动。 codex 另外三条我的处置: - 「pin oven/bun digest」:成立,但属于 test224/test597 自身的 Dockerfile, 与 #799/#802 的 pin 工作同族,不夹进本 PR; - 「report 写在容器里被 --rm 丢掉」:成立,是观测缺口,同样属套件自身; - 「把安全套件排在低层套件之后」:是取舍不是缺陷,独立 job 里三个都会跑完, 排序不影响是否产出证据。
6b1953a to
c933f5f
Compare
独立审(codex)在本 PR 上提了三条 P1,逐条复现后全部成立: 1) **深度不感知 —— 元门自己放行了没人会跑的测试。** 两个 unit runner 扫 `<pkg>/tests` 用的是 `find … -maxdepth 1`,而本脚本 原来只按前缀判覆盖。复现:把一个测试放到 `agent-network/tests/sub/` 下, 元门报「0 个漏网」rc=0,而 runner 的 find 对它命中 0。 **这正是这道门存在的意义所在,它却在自己身上漏了。** 修法:深度从门里推导(scan_depth),不假定递归;`bun test <dir>/` 形式按递归算。 双向验过:子目录文件 → rc=1 且点名;直属文件 → rc=0。 2) **套件豁免不校验套件是否真实存在。** 原来只要路径以 `tests/` 开头就放行,于是 `tests/test999-example/new.test.ts` 这种既没 Dockerfile 也没 run.sh 的目录也能过 —— 豁免变成「只要放对地方 就不用被任何东西跑」。改成要求套件目录里 Dockerfile 和 run.sh 都在。 双向验过:伪套件 → rc=1;补上两个文件 → rc=0。 3) **qa.yml 改动不触发本门。** qa.yml 决定那三个聚合门到底跑不跑,它一改本门的前提就可能塌, 但它不在触发路径里。已加进两处 path 过滤。 NOT COVERED(第 2 条修完仍存在的缺口):校验了「套件是一套门」, 但**没有**校验「该套件已注册进 CI」。test224/test597/test679 就长期 有完整 Dockerfile+run.sh 却没人跑 —— 那是 #803 在解决的问题,不是本门的判据。
自查发现本报告里有两个不同的 source_commit:抬头是 2617987(正确,==源码提交), 但嵌入的运行输出里是 187a6ff。 根因是我上一轮的操作顺序错了:**先 `git rev-parse HEAD` 打戳、后提交下限改动**。 于是镜像里跑的是含下限的代码,戳进日志的却是提交前的 SHA —— 证据本身有效,但它自称的锚点指向一个不含该改动的提交。 这与 #798/#800/#803 早先被独审抓到的假锚点是**同一个根因的第二次发作** (那次是把 --build-arg 传成了 origin/main,这次是传成了未提交前的 HEAD)。 已在真源码提交 2617987 上重跑并重出报告,全文 source_commit 只指向一个值: test_files=69 executed_files=69 failed_files=0 MUTATION_RED registration-password-floor-weakened rc=1 RESULT: PASS
状态更新:上面那条 DO-NOT-MERGE 针对的是 head
|
五条 P1 对当前 head
|
| # | 内容 | 裁定 |
|---|---|---|
| a | test224 用 --network none 跑 |
已修 |
| b | tests/lib/** 进路径过滤 |
已修 |
| c | 容忍没有 ARG SOURCE_COMMIT 的 Dockerfile |
已修 |
| d | 三个新套件的 Bun 输入未钉死 | 仍在 |
| e | 安全套件排在最前 | 仍在 |
d 仍在:三个套件没有一个是不可变输入
tests/test224-grok-preview-security/Dockerfile:2 FROM oven/bun:1.3.1 # 可变 tag
tests/test597-dashboard-slash-namespace/…:1 FROM oven/bun:1.3.14 # 可变 tag
tests/test679-task-trace/Dockerfile:1 FROM node:22-bookworm-slim
:3 RUN curl -fsSL https://bun.sh/install | bash # 构建时装到什么算什么审查引用的 docs/tests/report-l1-pinned-bun.txt 确实存在于 main(Date: 2026-08-13),记录的正是「L1 契约套件把 Bun 钉成不可变输入」这件事。
也就是说:L1 之前是特意做到 0 次安装器调用 + 单一 digest-pinned 镜像的,而本 PR 把这三个套件注册进 L1,等于把那件刚做完的事撤销掉一部分 —— 同一个 commit 会在不同时间跑在不同字节上,门可能因为与被审代码无关的原因变红。
这条成立。修法:三个 Dockerfile 转成与既有 L1 相同的不可变 Bun 输入之后,再注册进 L1。
e 仍在:顺序与本仓自己写的分层规则相反
qa.yml:94/101 Build/Run test224-grok-preview-security ← 安全,跑在最前
qa.yml:108/115 Build/Run test597-dashboard-slash-namespace
qa.yml:118/125 Build/Run test679-task-trace
CLAUDE.md 的测试规则写得很明确:
分层测试,从简单到复杂:环境→认证→单点通信→完整流程→多用户→安全
前一层不过就不跑后面的:被依赖的原子能力必须先验证可靠
我把安全套件放在了最前面,正好把这条规则倒过来。后果不是"跑了会错",而是:当 test597/test679 里有更底层的失败时,安全证据已经先产出了 —— 而按本仓的约定,那份证据的前提根本没成立。
这条不是风格偏好,是与仓库明文规则冲突,成立。
附:我自己差点误判 a
第一次核 a 时我用 grep -nE 'network none|test224' | head -5,返回的 5 行里没有 --network none,我差点写成"未修"。实际它在 :106,被我自己的 head -5 截掉了。
head -N 用在"判断某个东西存不存在"的查询上是危险的 —— 它把"没找到"和"找到了但被截断"变成同一个输出。这与本轮在 #812/#801 反复用到的那条是同一件事:先确认查询范围覆盖了目标,再解读结果。
冻结中未改分支,d/e 待复审收口后一并落。
补裁第 7 条 P1 —— 我上一轮漏了它,「五条全部裁定」那句话是错的上一轮我说本 PR 是「五条 P1」并逐条给了裁定。实际是六条 P1 + 一条 P2。 我当时用 (这是同一个坑在本轮内第四次出现:
|
| # | 内容 | 裁定 |
|---|---|---|
| a | test224 --network none |
已修 |
| b | tests/lib/** 路径 |
已修 |
| c | 容忍无 ARG SOURCE_COMMIT |
已修 |
| d | 三套件 Bun 输入未钉死 | 仍在 |
| e | 安全套件排在最前 | 仍在 |
| f | --rm 销毁套件报告 |
仍在(本次补裁) |
| — | (P2)注册套件变更时触发 QA | 未单独裁定 |
冻结中未改分支。
给这条 PR 补一个分母:200 个有 Dockerfile 的套件里,178 个不被任何自动 runner 引用本 PR 捞回了三个「从没进 CI 的完整 Docker 门」(test224 / test597 / test679)。我做了一次全仓审计,想知道这是三个特例还是一片。 测量但 178 不是缺陷数 —— 先说清楚判据的边界我第一版的判据是「名字出现在
match=$(find "tests" -maxdepth 2 -path "tests/test${i}-*/Dockerfile" | head -n 1)
for i in 1 2 3 4 5 6 7; do …但它 (a) 只覆盖 test1–7,(b) 没有任何 workflow 或 更要紧的是:其中大多数本来就不该常驻命名形态 这跟我在 #831 上分的那两类是同一个区分:
把 178 个全部接进 CI 会是另一种错,就像给 changelog 更新行号一样。 所以这个数字的正确用法是「候选表」真正的问题不是「有 178 个没接线」,而是:这 178 个里,哪些编码的是应当持续成立的不变量? 本 PR 的三个正是这样的例子 —— test224 是安全套件,它显然应当常驻,而它此前从没进过 CI。这说明至少有一部分本该常驻的套件掉在了这 178 里,不是全部都是归档。 我没有能力单方面判定其余 175 个各属哪一类(那需要知道每个套件当初的意图)。这里只提供分母和判据边界,供决定要不要做一次分类。 一个可能有用的起点:安全/权限/凭据相关的套件优先看( |
|
本 PR 在两种合并顺序下都与 #798/#801 在 |
更正我上一条:这个冲突不需要任何设计决策 —— 取并集即可,已验上一条我说本 PR 与 #798/#801 在 把冲突真解开看,它是纯文本相邻冲突,不是语义冲突: 两边都只是往同一个 取并集后的实测
两处冲突块( 所以合并顺序的结论要更新原来我把这条列成「#803 需人工决策」。实际是:
一句我上一条把「git 报了冲突」直接读成了「需要人来决定」。这两件事不一样 —— 相邻插入的冲突,git 报冲突只是因为它不敢替你选顺序,不是因为存在取舍。 判据:看到冲突先把它解开读一遍,再判断它是取舍型还是相邻型。前者要人拍板,后者只要有人动手。 (完整的 14 条合并顺序实测见 #801 上那条评论。以上仍不构成合并建议或授权。) |
qa.yml 的四处待改动:摊开对照 + 一个具体的合并顺序我连着三轮在汇报里记「qa.yml 有 N 处待改动,需要协调」,却没给过协调方案。这条把它做掉。 main 上的现状四处改动各自动了哪里
冲突面:只在 paths 清单,而且是机械并集三者都追加在同一个锚点之后: git 会在这两处报冲突,解法是保留全部三块 —— 没有语义取舍,顺序也不影响行为(paths 是集合)。 job 主体方面:#803 插在 建议的合并顺序每一步的冲突解法都是并集,不需要判断谁对谁错。
顺带:我自己漏的一步,这轮补上了建 test831 时我写过「没接进 CI 的门只是装饰」。下一轮建 test846 时,我自己就没接。是做这次对照、逐个查各 PR 改了什么时发现的 —— 不是别人提的。已补( |
🔴 更正:我上一条给的 qa.yml 冲突解法是错的,而且错得很隐蔽上一条我写「解法是保留全部三块 —— 没有语义取舍」。我这轮把它干跑了一遍,那句话是错的,并且照做会产出一个看起来正常、实际有一个 job 永远不执行的 CI 文件。 干跑怎么做的从 前三步零冲突这部分预测是对的(它们是一条链)。问题出在第四步的解法。 git 给的是 4 个冲突块,不是 1 个对 ①② 取并集是对的(paths 是集合)。对 ③④ 逐块取并集是错的 —— 它把两个 job 的头和体交织成了: doc-source-pins:
name: doc source-pin floor (Docker) ← 到此为止,没有 runs-on、没有 steps
doc-claims:
name: doc claim freshness (Docker)
runs-on: ubuntu-latest
steps: …(5 个 step 全在这里)实测解析结果: 这个文件是合法 YAML。 GitHub 会接受它, 我试着写「正确解法」,第二次错得更明显我又写了一版脚本,把 ③ 的两个头分别与 ④ 的两个体重新配对。产出的 YAML 直接解析失败( 🔴 但最该记的是这一条上面两版坏文件,我都提交了,然后 test831 与 test846 都退出 0。 因为这两道门根本不看 没有任何东西会在合并前校验 qa.yml 自身。 一个坏掉的 workflow(不论是解析失败还是 job 空转)会一路走到 main,而所有现有的门都是绿的。 更正后的建议
顺带一个我认为该做但没做的这个仓里没有任何 workflow 语法/结构校验。加一个很便宜(一个 干跑用的三个临时分支与 worktree 已全部删除,没有推送任何东西。 |
上一条我说「这需要人看着改」,但没给样子。这条给了 —— 附根因根因:git 的「公共上下文」可以在语义上属于双方冲突区的实际结构是这样的: 中间那 5 行在两个 job 里逐字相同,所以 git 把它判成公共上下文、只留一份。但它语义上属于两个 job 各一份。 这就是盲目并集必然坏掉的原因:并集只处理 正确解法每个 job = 自己的头 + 共享块(复制一份)+ 自己的体: doc-source-pins:
name: doc source-pin floor (Docker)
runs-on: ubuntu-latest
timeout-minutes: 8
steps:
- uses: actions/checkout@v4
…test831 的步骤…
doc-claims:
name: doc claim freshness (Docker)
runs-on: ubuntu-latest
timeout-minutes: 8
steps:
- uses: actions/checkout@v4
…test846 的步骤…
验证(同一个合并点,两种解法对照)合完请跑这段断言 —— 「能解析」不够import yaml
d = yaml.safe_load(open('.github/workflows/qa.yml'))
for k, j in d['jobs'].items():
assert j.get('runs-on'), f"{k} 没有 runs-on"
assert j.get('steps'), f"{k} 的 steps 为空"坏掉的那版解析是通过的,只有这两条断言能拦住它。 干跑分支已删,没有推送任何东西。 |
上一轮我给出的是「冲突了怎么解」。这一轮做的是让冲突不发生。 冲突源于所有人都追加在同一处: paths 五个 PR 都插在 - 'tests/test746-setup-bun-pin/**' 之后 job #843 与本 PR 都追加在文件末尾(#803 插在 qa: 之前,#798/#801 插在 59 行) 改动: paths 改插到 - 'server/**' 之后 —— 距离 test746 九行,超出 git 默认上下文窗口 job 从文件末尾挪到 jobs: 之后(这个位置没有别的 PR 用) paths 是集合、jobs 是映射,位置变化不改变行为。结构断言(每个 job 有 runs-on 与非空 steps)已跑过。 这样合并时不需要任何人去解那个「公共上下文属于双方」的冲突 —— 那个坑我在 #803 上写清楚了,但最好的处理是不让人踩到它。
上一条给的是「冲突了怎么解」。这一条做的是让冲突不发生(
|
|
独立深审结论:BLOCKER / DO-NOT-MERGE(exact head 三个 recovered suite 本体均通过,但 exact-head CI 最后的 artifact upload 稳定失败: 这不是 cosmetic:PR 的目标是恢复三道长期信号,而当前 job 必红、证据也无法归档。请让容器按 runner uid 写入,或退出后显式修正属主/可读权限,并在同一 exact head 重跑整 job 到绿。修复后还需按 #798 之后 rebase,逐 job 保留完整 YAML 块。 只读审查;未改代码、未 approve/merge/deploy。 |
CI 红的根因:不是门失败,是产物上传失败(只读诊断,没改任何东西)失败 check 先看被测的东西:全过这个 PR 要注册的那几道门,跑完了而且是绿的。 红在哪
注意 修的方向在 docker run 之后、upload 之前,把产物目录的属主/权限修正一次。例如: - name: Normalize artifact permissions
if: always()
run: sudo chown -R "$(id -u):$(id -g)" "${RUNNER_TEMP}/suite-artifacts" && chmod -R u+rw "${RUNNER_TEMP}/suite-artifacts"或者让容器以 runner 的 uid/gid 写( 为什么值得单独说清楚「CI 红」在 PR 列表里长得都一样。但这条红不代表这个 PR 注册的门有问题: (顺带:同一批扫描里 #801 也是红的,但那条是真的门失败 —— |
exact-head CI 上 `recovered suites (Docker)` 稳定红,但红的**不是任何一道门**: RESULT: PASS (×2) PASS: targeted Docker context contains no host auth/config state PASS: real child env equals the reviewed set; … PASS: candidate tarballs contain runnable entrypoints … Summary: PASS (Docker-only; runtime executed with network disabled; …) 红在最后一步 `Upload recovered-suite artifacts`: With the provided path, there will be 4 files uploaded ##[error]An error has occurred while creating the zip file for upload Error: EACCES: permission denied, open '.../suite-artifacts/report-test224.txt' 三个 suite 都是 root 容器写进 bind mount(`-v "$RUNNER_TEMP/suite-artifacts:/artifacts"`), 产物属主 root、mode 0600;upload-artifact 以 runner 用户打包,打开即 EACCES。 注意 `if-no-files-found: warn` 且日志明说「4 files uploaded」—— 不是「没找到文件」,是找到了读不了。 后果不是 cosmetic:这个 PR 的目的正是把三道长期失联的信号恢复成 CI 里的常驻门, 而现在 job 必红、证据也归档不了,等于恢复了个红灯。 修法:上传前把产物目录的属主/权限归一化。用 `if: always()`,因为前面步骤红时 更需要把证据传出来。
复核这条 BLOCKER 是否仍成立(2026-08-14)判定针对 exact head 第 1 条(artifact upload 因 0600 权限稳定失败)—— 已解决,有产物为证判定要求「让容器按 runner uid 写入,或退出后显式修正属主/可读权限」。当前 head 的 - name: Normalize recovered-suite artifact permissions
if: always()
run: |
sudo chown -R "$(id -u):$(id -g)" "$RUNNER_TEMP/suite-artifacts"
chmod -R u+rw "$RUNNER_TEMP/suite-artifacts"但"步骤存在"不等于"产物真的传上去了" —— 这个 upload 步骤带
第 2 条(在同一 exact head 重跑整 job 到绿)—— 已满足
第 3 条(按 #798 之后 rebase)—— 现在无法满足,而且不是本 PR 能单方面做的
🔴 顺带发现一条判定没覆盖的:产物里的
|
|
【独立复核结论更新|撤回旧 BLOCKER】 我先前 15:43 的 DO-NOT-MERGE / BLOCKER 结论审的是过期 head 独立证据:
因此:明确撤回旧 BLOCKER / DO-NOT-MERGE。 仍保留一条合并顺序约束:#798 与 #803 在 本评论只更新审查结论;未 merge、未改代码、未部署。 |
run.sh 里那一行是:
log "network: disabled by runner"
它挨着的每一条都真的在验:
[ ! -e "$ROOT/.git" ] || fail "image contains repository metadata"
[ ! -e /root/.grok ] || fail "image contains a Grok home"
**只有它在复述一个别人应该做过的事。**
而 [L2] 那一步的全部意义是「在没有网络的情况下构建候选包」。如果 runner 忘了
`--network none`,那一步照样绿——而它证明的东西并不成立。🔴 忘记加那个 flag 与
正确加了它,产出的证据逐字相同。
判据用直接观察:`--network none` 的容器里 `/sys/class/net` 只有 `lo`。
net_ifaces=$(ls /sys/class/net | tr '\n' ' ' | sed 's/ *$//')
[ -n "$net_ifaces" ] || fail "cannot read /sys/class/net — …refusing to claim it is"
[ "$net_ifaces" = "lo" ] || fail "network is NOT disabled: …[$net_ifaces]…"
pass "network is off (verified: /sys/class/net = [$net_ifaces])"
两个方向都收:读不到 `/sys/class/net` 时**拒绝声称网络是关的**(fail-closed,
而不是当成"看不见就是没有");看到第二个接口时点名它,并说明后面的绿因此不作数。
本机对照(有网的宿主):`/sys/class/net` = `docker0 eth0 lo` → 这条断言会红。
注:test224 目前不被任何 CI 引用(#861 统计的 180 个孤儿之一),PR #803 正在把它
注册进 qa.sh。本 commit 只修判据,不改注册状态——一道会被跑的假断言和一道不会被跑
的真断言,前者更危险,先修前者。
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
冲突是纯新增撞纯新增(两边都在 `paths:` 末尾追加)。取并集,main 那段带判据的 注释保留在前:「一个测试目录属于这里,当且仅当 CI 真的执行它」。 按这条判据核过,本 PR 新增的四条全部成立 —— 新的 `recovered suites (Docker)` job 对 test224 / test597 / test679 各有一次真 `docker build` + `docker run` (不是只 build);`tests/lib/**` 被这三个镜像 COPY 进去。 验过(合并后的树): - yaml.safe_load → OK;4 个 job 名两两不同(recovered suites (Docker) 是新的) - .github/scripts/check-l1-paths-sync.py → rc=0(17 个 L1 套件 / 18 条 path) - .github/scripts/check-l1-paths-sync.py --selftest → rc=0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(ci): 给 server 补上聚合单测门(69 个单测此前 CI 只跑 6 个) server/src 下 69 个 *.test.ts,CI 可达的只有 6 个(scripts/qa.sh 的 L0_TESTS 点名 5 个 + test686 引用 1 个),另外 63 个没有任何 job 会碰。server 是 hub 本体 —— 认证、token、网络隔离都在这里,盲区比 agent-network 那 46 个严重。 形状抄 test745/test725,但按 server 自己的契约做了两处改动: 1) 逐文件跑,每个文件一个独立 DB。scripts/qa.sh 的 L0 本来就是 `COMMHUB_DB=/tmp/qa-l0-$name.db bun test <one-file>` —— 这是既有契约。 用一个共享 DB 聚合跑会红 4 条(admin-networks 的 global-admin 可见性、 scheduled-tasks 三条),而这 4 条单跑全绿,是跨文件状态污染。 把"聚合能不能跑"当门等于给它加了一条它从没承诺过的性质。 2) cwd 必须是仓根。task-lifecycle-watcher 用 process.cwd() 拼 ./server/src/db.js,scheduled-tasks-http 按仓根相对路径 import tests/test601-.../race-worker.ts。从 server/ 目录跑会让这两个红在路径上, 看起来像产品坏了。 红线:COMMHUB_DB 不设默认指向生产库。容器里够不到宿主的库,但不靠"够不到" 保证 —— run.sh 显式钉到 /tmp 并断言钉住了。31/69 个测试引用 sqlite/COMMHUB_DB。 分母承重:executed_files 必须等于 find 出来的 test_files,少一个就红。 witnessed-red:把 auth.ts 注册密码下限 `< 8` 改成 `< 1`(7 位密码会被接受, 一条真的安全回退),先校验字节非 no-op,再要求红落在指名的 "rejects 7-char password" 上。 实测:test_files=69 executed_files=69 failed_files=0, MUTATION_RED registration-password-floor-weakened rc=1,RESULT: PASS,耗时 51s。 完整输出见 docs/tests/report-test798-server-unit-ci.txt。 * ci: server 单测门抽成独立 job,别挂在 agent-network 名下 上一版把 build/run 两步插进了 agent-network-unit job 里,所以它确实跑了 (CI 日志实测 test_files=69 executed_files=69 failed_files=0 MUTATION_RED registration-password-floor-weakened rc=1 RESULT: PASS), 但会以 "agent-network unit (Docker, non-root)" 的名义显示 —— server 挂了会归错帐,而且两个重 Docker build 串在一个 job 里。 抽成 server-unit job,显示名 "server unit (Docker, non-root)"。 * ci: test601 的 race-worker 也要能触发 server 单测门 自查清单第 4 条(判据范围要与被判对象一致)在自己 PR 上的第一次应用: 把 test798 镜像 COPY 的每一项,回去核 qa.yml 的触发路径有没有覆盖。 COPY server ./server → 'server/**' ✅ COPY agent-node/src ./agent-node/src → 'agent-node/**' ✅ COPY tests/test601-hub-scheduled-tasks → 无 ❌ server/src/scheduled-tasks-http.test.ts 会执行那个 race-worker 做 「两个真 Hub 进程抢同一个 occurrence 恰好一次」的用例 —— 只改 worker 的 PR 不该跳过这道门。两处 path 过滤都补上。 这条是 codex 在 #798 上提的 P2,当时我认了但没修;现在按清单扫一遍就扫到了。 * test(ci): mutation 的命名断言要锚在 (fail) 行,否则通过时也会命中 自查清单(#815)第 ⑤ 条「断言要精确到不合规会被拒绝」在自己门上的应用。 原来写的是 `grep -Fq 'rejects 7-char password'`。bun test 对每个用例都打 `(pass) <名字>` 或 `(fail) <名字>` —— 只 grep 名字的话,那条用例**通过**时 也会命中。于是这条断言只证明了「这条用例存在」,而不是「红落在它身上」。 A/B(把断言指向一条在该 mutation 下**不会红**的用例 `accepts 8-char strong password`,其余完全不动): 松版 grep -Fq '<名字>' → rc=0 RESULT: PASS ← 收下了不合规 严版 grep -Eq '^\(fail\).*<名字>' → rc=1 FAIL: mutation red did not reach the named… 改成锚定形式后正常绿:MUTATION_RED registration-password-floor-weakened rc=1,RESULT: PASS。 同类问题在 tests/test725-agent-node-unit-ci/run.sh 也有(它 grep 的 'the inbox choke point feeds the augmented text into processTask' 同样是测试名); 在 #800 里一并收紧,那边有单独说明。 tests/test745 那条不受影响 —— 它 grep 的是断言失败信息 `Expected to contain: "anet config [path|json]"`,只在失败时出现。 * test(ci): 分母要有绝对下限 —— 删掉 85% 的测试,这道门原来照样绿 自查清单(#815)第 ⑥ 条「mutation 要跑到曾经活下来为止」的直接产物。 这道门原来只有「削弱被测代码」一个 mutation 维度。换一个维度试:删测试文件。 第一次删 60/69 时门红了 —— 但那是**碰巧**:mutation 靶点所在的 auth-validate.test.ts 恰好在被删之列。做决定性验证,删 59 个但保留它: test_files=10 executed_files=10 discovered_files=10 failed_files=0 MUTATION_RED registration-password-floor-weakened rc=1 RESULT: PASS rc=0 **门放行了一个删掉 85% server 单测的改动。** 根因:`executed >= discovered` 只能抓「runner 跳过了文件」,抓不到 「文件消失了」—— 分母跟着现实自动缩水,原来的 `-gt 0` 形同虚设。 加 SERVER_TEST_FLOOR=60,并写明「真删了测试就故意改它,并在 PR 里说明」。 双向验过:69 个 → RESULT: PASS;同一删除 mutation → rc=1 `FAIL: only 10 server test file(s) under src/, floor is 60`。 * docs(tests): report-only —— 修掉报告内部两个不一致的锚点 自查发现本报告里有两个不同的 source_commit:抬头是 2617987(正确,==源码提交), 但嵌入的运行输出里是 187a6ff。 根因是我上一轮的操作顺序错了:**先 `git rev-parse HEAD` 打戳、后提交下限改动**。 于是镜像里跑的是含下限的代码,戳进日志的却是提交前的 SHA —— 证据本身有效,但它自称的锚点指向一个不含该改动的提交。 这与 #798/#800/#803 早先被独审抓到的假锚点是**同一个根因的第二次发作** (那次是把 --build-arg 传成了 origin/main,这次是传成了未提交前的 HEAD)。 已在真源码提交 2617987 上重跑并重出报告,全文 source_commit 只指向一个值: test_files=69 executed_files=69 failed_files=0 MUTATION_RED registration-password-floor-weakened rc=1 RESULT: PASS --------- Co-authored-by: vansin <smartflowaiteam@gmail.com> Co-authored-by: t <t@x> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
合了。这是今晚我在 #863 上指出的那件事的解药,所以我核得比平时细。 我核的是「它真的跑了吗」,不是「它绿吗」从 CI 日志里拉出来的程序输出(已滤掉命令回显): 六条变异红全部落地 —— 这三个套件是第一次在 CI 里真的跑,而且不是空转。 🔴 顺带一个闭环: job 本身我核了三点
qa.yml 冲突纯新增撞纯新增,取并集。按 main 那条判据(「一个测试目录属于这里,当且仅当 CI 真的执行它」)核过,新加的四条全部成立:三个套件各有真 合并后: 冲突是我造成的(今晚 34 个提交动过 剩下的账
|
#798 刚合进 main,两边又都在 `paths:` 末尾追加,同一个纯新增撞纯新增。 仍取并集,两边条目零重复。 验过:yaml.safe_load OK / 5 个 job 名两两不同 / check-l1-paths-sync.py 与 --selftest 均 rc=0。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
二次合并 main(#798 落地后 |
七处冲突。逐个核过之后的处理:
## 四个共享文件:取 main,因为 main 那版更严
tests/test725-agent-node-unit-ci/run.sh
tests/test745-agent-network-unit-ci/run.sh
tests/test798-server-unit-ci/run.sh
tests/test798-server-unit-ci/Dockerfile
🔴 本分支带的是这些文件的**更早一版**,和 main 相比少两样东西:
- `SERVER_TEST_FLOOR=70` ← 本分支是 `[[ "$test_files" -gt 0 ]]`,
也就是删到只剩 1 个测试文件也照样绿
- `grep -Eq '^\(fail\).*<名字>'` ← 本分支是 `grep -Fq '<名字>'`,
那条用例**通过**时也会命中,断言只证明了它存在
diff 逐行比过,本分支在这四个文件里**唯一多出来**的是 test725 那条宽容版 grep ——
也就是说取 main 不丢任何东西,取本分支会静默删掉两道判据。
## 本 PR 独有的三样,原样保留并补齐另一半
1. `.github/scripts/check-test-file-coverage.py` + 对应 workflow —— 元门:
新增测试文件不能落在所有聚合门的扫描范围之外。本地跑过:
`tracked_test_files=247 / 244 在范围内 / 3 个套件自带 / 0 个漏网`
2. `server/package-lock.json` + Dockerfile 改 `npm ci`。
注意上一条冲突里我取了 main 的 Dockerfile(它是 `npm install`),
所以这里**重新贴回** `npm ci` —— 只取一边会把这半个功能丢掉。
验过 `npm ci` 接受「main 的 package.json + 本 PR 的 lockfile」这一对:
`added 96 packages in 2s`,rc=0。
3. `RUNSH_BLOB` —— 把报告里的 SOURCE_COMMIT 绑到镜像里被测的字节
(只验 40 位十六进制的格式是不够的)。同理它有三块:qa.yml 的 build-arg、
Dockerfile 的 ARG/ENV、run.sh 里的重算比对。**冲突只暴露了第一块**;
只留 build-arg 会得到一个「传了参数但没人验」的半截门,所以另外两块手工贴回。
## qa.yml 的 paths
main 侧是本分支的**超集**(多了 test224/test597/test679/tests/lib —— #803 注册的
三个套件),取 main。20 条,零重复。
## 合并后跑过
yaml.safe_load OK,5 个 job 名两两不同
check-l1-paths-sync.py rc=0(17 个 L1 套件 / 20 条 path)
check-qa-trigger-coverage.py rc=0
check-test-file-coverage.py OK,0 个漏网
bash -n tests/test798-server-unit-ci/run.sh OK
npm ci(main 的 package.json + 本 PR 的 lockfile) rc=0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(ci): 给 server 补上聚合单测门(69 个单测此前 CI 只跑 6 个)
server/src 下 69 个 *.test.ts,CI 可达的只有 6 个(scripts/qa.sh 的 L0_TESTS
点名 5 个 + test686 引用 1 个),另外 63 个没有任何 job 会碰。server 是 hub 本体
—— 认证、token、网络隔离都在这里,盲区比 agent-network 那 46 个严重。
形状抄 test745/test725,但按 server 自己的契约做了两处改动:
1) 逐文件跑,每个文件一个独立 DB。scripts/qa.sh 的 L0 本来就是
`COMMHUB_DB=/tmp/qa-l0-$name.db bun test <one-file>` —— 这是既有契约。
用一个共享 DB 聚合跑会红 4 条(admin-networks 的 global-admin 可见性、
scheduled-tasks 三条),而这 4 条单跑全绿,是跨文件状态污染。
把"聚合能不能跑"当门等于给它加了一条它从没承诺过的性质。
2) cwd 必须是仓根。task-lifecycle-watcher 用 process.cwd() 拼
./server/src/db.js,scheduled-tasks-http 按仓根相对路径 import
tests/test601-.../race-worker.ts。从 server/ 目录跑会让这两个红在路径上,
看起来像产品坏了。
红线:COMMHUB_DB 不设默认指向生产库。容器里够不到宿主的库,但不靠"够不到"
保证 —— run.sh 显式钉到 /tmp 并断言钉住了。31/69 个测试引用 sqlite/COMMHUB_DB。
分母承重:executed_files 必须等于 find 出来的 test_files,少一个就红。
witnessed-red:把 auth.ts 注册密码下限 `< 8` 改成 `< 1`(7 位密码会被接受,
一条真的安全回退),先校验字节非 no-op,再要求红落在指名的
"rejects 7-char password" 上。
实测:test_files=69 executed_files=69 failed_files=0,
MUTATION_RED registration-password-floor-weakened rc=1,RESULT: PASS,耗时 51s。
完整输出见 docs/tests/report-test798-server-unit-ci.txt。
* ci: server 单测门抽成独立 job,别挂在 agent-network 名下
上一版把 build/run 两步插进了 agent-network-unit job 里,所以它确实跑了
(CI 日志实测 test_files=69 executed_files=69 failed_files=0
MUTATION_RED registration-password-floor-weakened rc=1 RESULT: PASS),
但会以 "agent-network unit (Docker, non-root)" 的名义显示 ——
server 挂了会归错帐,而且两个重 Docker build 串在一个 job 里。
抽成 server-unit job,显示名 "server unit (Docker, non-root)"。
* test(ci): 让 test725/test745 覆盖 tests/ 目录,兑现"complete unit domain"
两个门的抬头都写着 "complete agent-node/agent-network unit domain",
但只跑 src/,把 tests/ 下 25 个文件排除在外 —— 其中不乏安全相关的:
feishu-markdown-image-ssrf、secret-mask ×3、vendor-error-sanitize、feishu-tool-deny。
这些正是静默失效代价最高的那类。
这个目录里混着两种测试,任何单一命令都跑不全:
- 脚本式(16+6 个):自己打 "N/N passed",失败 process.exit(1),必须 bun <file>;
用 bun test 跑会因为 top-level 的 process.exit 把整个 run 打断在第一个文件
(实测:bun test tests/ 只跑完第一个就结束)。
- bun:test 式(3 个):describe/it,必须 bun test <file>;用 bun <file> 跑会报
"Cannot use describe outside of the test runner"。
所以按文件内容分派,并把两条判据都写进注释。
退出码可用已先验:这些脚本失败时确实 process.exit(1),不是 fail-open。
落地前实测:
agent-node/tests 6/6 直接过
agent-network/tests 单命令 14/19 → 按内容分派 17/19 → 补两处环境契约 19/19
两处契约都在 Dockerfile 内解决,并写明原因:
- feishu-envelope-compat 跨包 import agent-node/src/runtime/feishu-envelope
- feishu-bridge-ipc 硬编码绝对路径 /work/feishu-attachments,容器里 node 建不了
分母承重:tests_dir_executed 必须等于 find 出来的数,且 >0。
实测:test725 tests_dir 6/6/0 + MUTATION_RED + PASS;
test745 tests_dir 19/19/0 + MUTATION_RED + PASS。
* docs(tests): report-only —— 锚点 46e752c(含 current main 034f006)
按独审要求重做 provenance:append current main → 在精确源码提交上重跑 → report-only 子提交。
main 的新增提交 #802 只动 tests/qa-180-rename-ghost/,与本 PR 四个文件零相交,
rebase 无冲突;qa.yml 两处 path(test746 / test798)都保留;
server 步骤已是独立 job server-unit(name="server unit (Docker, non-root)", timeout 12),
不再嵌在 agent-network-unit 里。
实测:executed_files=69 discovered_files=69 failed_files=0,
MUTATION_RED registration-password-floor-weakened rc=1,RESULT: PASS。
* docs(tests): report-only —— 锚点 a4fd375(含 current main 034f006)
按独审 SUPERSEDE 的要求重做 provenance。原 CLEAN 判定被撤回是对的:
上一份报告声称的锚点 92d9612 比本 PR base 还早两个提交,那上面的 run.sh 里
没有 tests_dir_executed 那段代码,报告内容 provably 不可能由它产出。
根因是我跑门时 --build-arg SOURCE_COMMIT 传的是当时的 origin/main。
实测:test725 tests_dir 6/6/0 + MUTATION_RED + PASS;
test745 tests_dir 19/19/0 + MUTATION_RED + PASS。
* ci: 元门 —— 修掉独立审抓出的三条 P1(其中一条是元门自己的漏网)
独立审(codex)在本 PR 上提了三条 P1,逐条复现后全部成立:
1) **深度不感知 —— 元门自己放行了没人会跑的测试。**
两个 unit runner 扫 `<pkg>/tests` 用的是 `find … -maxdepth 1`,而本脚本
原来只按前缀判覆盖。复现:把一个测试放到 `agent-network/tests/sub/` 下,
元门报「0 个漏网」rc=0,而 runner 的 find 对它命中 0。
**这正是这道门存在的意义所在,它却在自己身上漏了。**
修法:深度从门里推导(scan_depth),不假定递归;`bun test <dir>/` 形式按递归算。
双向验过:子目录文件 → rc=1 且点名;直属文件 → rc=0。
2) **套件豁免不校验套件是否真实存在。**
原来只要路径以 `tests/` 开头就放行,于是 `tests/test999-example/new.test.ts`
这种既没 Dockerfile 也没 run.sh 的目录也能过 —— 豁免变成「只要放对地方
就不用被任何东西跑」。改成要求套件目录里 Dockerfile 和 run.sh 都在。
双向验过:伪套件 → rc=1;补上两个文件 → rc=0。
3) **qa.yml 改动不触发本门。**
qa.yml 决定那三个聚合门到底跑不跑,它一改本门的前提就可能塌,
但它不在触发路径里。已加进两处 path 过滤。
NOT COVERED(第 2 条修完仍存在的缺口):校验了「套件是一套门」,
但**没有**校验「该套件已注册进 CI」。test224/test597/test679 就长期
有完整 Dockerfile+run.sh 却没人跑 —— 那是 #803 在解决的问题,不是本门的判据。
* ci: 元门要验「这道门真的被 CI 跑」,不只是「它存在且声明了范围」
独立审(codex P1)指出的缺口,我上一版只在 NOT COVERED 里记了没修:
qa.yml 一旦删掉/改名某个 job、或不再 build/run 它的 Dockerfile,
本脚本照样发绿 —— 因为它从没看过 qa.yml。
**这正是本门要防的那类问题(有门、没人跑),不能留在自己身上。**
判据要求 qa.yml 里同时出现两件事,单独一条不算:
-f tests/<suite>/Dockerfile 真的构建了它
docker run … <这次 build 打的 tag> 真的跑了那个产物
两条解耦 mutation,各自红在不同原因上(基线绿):
F1 删掉 server-unit 的 docker run(build 保留)
→ rc=1「qa.yml 构建了 anet-test798-server-unit 但没有 docker run 它」
F2 把 test745 的 build -f 路径改名
→ rc=1「qa.yml 里没有 build tests/test745-agent-network-unit-ci/Dockerfile」
一道门可能覆盖多个根(test745 覆盖 src 与 tests),接线问题去重后只报一次。
* docs(tests): report-only —— 锚点 9626c98,七条 mutation
* ci: 落实 ⑤⑥ 两条已接受未实施的意见;② 需所有者决定,如实标注
⑥ SOURCE_COMMIT 只验格式不验字节
原来只验 ^[0-9a-f]{40}$。任何 SHA 都能过,而审查指出提交进来的 report
里那个 SHA 早于本套件自身 —— 那份证据无法从它自称的版本复现。
改成与 test823 相同的做法:构建时把 run.sh 在该 commit 下的 git blob
哈希作为 build-arg 传入,容器内就地重算比对(blob 哈希 =
sha1("blob <len>\0"+内容),不需要容器里装 git)。
已验脚本内算法与 git hash-object 结果一致;该机制的端到端红/绿在 #835
上证过两次(传错 blob、blob 对但文件被篡改,都 exit 1)。
⑤ qa.yml 缺 test601 路径
test798 的镜像 COPY 了 test601 的 race-worker.ts,而
server/src/scheduled-tasks-http.test.ts 会执行它做「两个真 Hub 抢同一
occurrence」。只改那个 worker 的 PR 不该跳过这道门。已在两处 paths 补上。
(这 4 行原本只存在于 #798;若只合本 PR、把 #798 当冗余关掉,它们永远
不会落地 —— 此前已在本 PR 记录过这个坑。)
② server 的 npm install 无 lockfile —— 我没有改,需要所有者决定
实测:server/package.json 有 4 个依赖,4 个全用 caret 范围,且仓里没有
任何 lockfile/shrinkwrap。所以同一个 commit 在不同时间构建确实会解析出
不同依赖图,审查这条成立。
但修法只有一条:提交一份 lockfile。那是仓库级的依赖钉死决策 —— 它影响
每一次 server 构建,不只是这道门;而且生成出来的树我无法在这里验证是否
仍然全绿。这不该由我单方面决定,如实留作待决,不假装已修。
* ci(test798): server 依赖钉死 —— 提交 lockfile 并改用 npm ci
审查 ② 说的成立:server/package.json 4 个依赖全用 caret 且仓里没有 lockfile,
所以同一个 commit 在不同时间构建会解析出不同依赖图 —— 上游发一个兼容版本
就能让这道门变红或改变被测行为,而仓库一个字节都没动。
我上一版把这条标成"仓库级决策,不该由我单方面做"。那个定性是错的:
agent-network/package-lock.json 已提交
docs-site/package-lock.json 已提交
prototype/anet-client-app/package-lock.json 已提交
5 个包里 3 个已经提交 lockfile,.gitignore 的 *.lock 也匹配不到
package-lock.json。提交它是本仓既有做法,server 与 agent-node 只是不一致。
真正卡住的是一次验证跑,不是授权 —— 我把成本问题说成了权限问题。
本次改动:
- npm install --package-lock-only 生成 server/package-lock.json(1212 行,
未装 node_modules)。锁到的直接依赖:
@modelcontextprotocol/sdk 1.30.0 / bun-types 1.3.14 / hono 4.13.1 / zod 4.4.3
- Dockerfile 改为 COPY package.json + package-lock.json,并把 npm install
换成 npm ci(ci 严格按 lockfile 装,install 会按 caret 取"当下最新兼容版")。
验证:带 lockfile 重建后跑完整套件
test_files=69 executed_files=69 failed_files=0
MUTATION_RED registration-password-floor-weakened rc=1
RESULT: PASS 退出码 0
* ci(test798): 把 RUNSH_BLOB 真的传进去 —— 门在要求它,workflow 从没供给
CI 上 `server unit (Docker, non-root)` 稳定红,日志里唯一的失败行:
FAIL: TEST798_RUNSH_BLOB 缺失或格式不对 —— 无法把 SOURCE_COMMIT 绑到被测字节
链条断在最后一环:
run.sh:25-27 要求 TEST798_RUNSH_BLOB 且校验 ^[0-9a-f]{40}$,否则 fail-closed
Dockerfile:41,45 ARG RUNSH_BLOB → ENV TEST798_RUNSH_BLOB
qa.yml:79-84 docker build 只传 SOURCE_COMMIT,**没传 RUNSH_BLOB**
于是 ARG 取空、ENV 为空串、正则不过。门本身是对的 —— 它正确拒绝了一次
「说不清自己测了哪份字节」的运行,缺的只是供给那一行。
补法用 git 自己的 blob 哈希,和 run.sh:31 的算法是同一个东西:
run.sh 算的是 sha1("blob <len>\0" + 内容),那正是 git 的 blob object id。
本地实测两者一致(在本分支 head 上):
git rev-parse HEAD:tests/test798-server-unit-ci/run.sh
= 0e48c36
{ printf 'blob %d\0' "$(wc -c < run.sh)"; cat run.sh; } | sha1sum
= 0e48c36
对照 #798:它的 run.sh 里 RUNSH_BLOB 命中 0 次 —— 所以这不是 #798 的回归,
是本 PR 新加的要求没接完线。
🔴 这一条只修 CI 红。独立审查另指出本 PR 仍夹带 #798 的旧版本、需在 #798 之后
rebase —— 那件事不在本提交范围内。
---------
Co-authored-by: vansin <smartflowaiteam@gmail.com>
Co-authored-by: vansin <t@t>
Co-authored-by: t <t@x>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* docs: 留一份「陈旧 issue 怎么复核」的做法 直接原因:仓里 79 个 open issue,30 个超过 30 天没动,而这 30 个没有一个被任何 open PR 引用。我手工核了其中四条(#175 / #166 / #114 / #177),四条各花十几分钟, 方法没留下来 —— 剩下 26 条又得从头想一遍。 这份不是流程规范,是那四次的做法加踩到的坑: - 「陈旧」本身不是判据。做完没关 / 做了一半 / 前提不成立 / 真没排到,这四种在 issue 列表里长得一模一样,时间和 label 区分不了。所以不能批量关 —— 批量关会 把「做了一半」和「走不通」一起埋掉,而那两种最值得写清楚。 - 先读正文再搜代码。#114 标题像从零开始,正文只有两句;#166 标题说一件事, 正文实际列了四件 —— 只按标题搜会把「四件里做了三件」判成做完了。 - 对 origin/main 取证,不是对本地工作树。我有一次在老分支上 grep server/src, 那儿 20 个 .ts 而 main 上是 106,结论建在了另一份代码上。 - 计数只是候选。#114 grep 命中 5 个文件、#177 命中 38 个,看着都像做了;实际 #114 那 5 个全是日志与测试(数据采到就丢),#177 那 38 个没有一个是实现。 反例:db.ts 里 grep cost 有 6 处,全是 scrypt KDF 的 cost 参数,与钱无关。 - 找「它要退役的东西还在不在」——比「新东西做了没」更快更硬。#177 要让 #176 的 capture-pane workaround 退役,而那个 workaround 和 dev-channel flag 都还在。 - 检查正文里的「待确认前提」。#177 写着需先确认 Claude Code 的 managed-settings 对自定义 plugin 是否可用,而这个确认至今没结论 —— 那它可能是走不通,不是没排到。 - 判不了就写明判不了,不给软结论。 - 🔴 不替 owner 关别人的 issue。复核者拿到的证据往往只覆盖标题那一句,用窄证据 关一个宽承诺是不对的。 文末附四次复核的结论与各自的决定性证据,可作样例。文档里引用的每处行号与文件 在提交前逐条对 origin/main 复验过(11 项,全部命中)。 * docs(stale-issue-review): 补第 6 步 —— 查代码里有没有指向该 issue 的注释 又核了三条(#332 / #195 / #207),发现一个共同点值得写进方法:很多陈旧 issue 不是被遗忘的,代码里留着指针,只是没人回来更新 issue。 对 30 条跑了一遍按编号 grep:6 条被源码引用(#31/#166/#182/#191/#246/#338)。 但 6 是下界 —— #332 与 #207 也被代码引用却抓不到,因为注释是描述式的、不带编号: feishu-tool-deny.ts:250 … a bubblewrap sandbox follow-up tracks the … cli.ts:3450 … Cross-machine artifact distribution is a P2 follow-up. 所以这一步要两样都做:按编号 grep + 读正在核的那块代码的注释。只做前者会漏掉 「代码知道、但没写号」的那些,而那些恰恰最该保留 —— 它们证明 issue 还活着。 样例表补三条,其中 #207 的结论形态变了:它开的时候是「跨机分发没人做」,现在是 「通用跨机附件通道(#222)已完成并有 e2e 套件钉着,缺的是把 grok 的 video artifact 接进去」——而接之前要先评估一个可见性变化(/api/files/<id> 是 any-valid-token、不按 network 隔离,把 0600 的 session-private mp4 传上去会扩大 可见范围)。这类「缺口性质变了」的结论,比「还没做」有用得多。 * docs(stale-issue-review): 修表格断裂与计数 上一次提交把新增的三行贴在了表格之后、中间隔了一个空行 —— markdown 里那会 把一张表切成两张,第二张没有表头。是自查表格结构时发现的(表头 1 个但数据行 分在两处)。 同时把「四次复核」改成七次,开头的「剩下 26 条」改成 23 条。 * docs(stale-issue-review): 修审查提的五条 —— 其中三条是样例在示范本文警告的错误 #846 的审查提了五条,全部成立。三条是我的样例表自己违反了文档写的规则: ① #175 我标成「已交付」,而结论表把已交付等同于建议关闭 —— 可文档第 2 段刚 说过证据只覆盖标题那一句。这正是「用窄证据关闭宽承诺」。改成「部分核验」。 ② #166 我标成「已交付」,而文档刚警告过「四件事里做了三件会被判成做完了」—— 我列的恰好是三项证据。改成「四项中三项已交付」,并写明第四项的真实状态: 仓库改不了外部会话的工具面板,现状是把边界写进文档并用测试钉住。 ③ #114 我用错了判据。拿「completions/tasks 没有用量列」当决定性证据,但 RFC-015 设计的是独立的 agent_token_usage 表,根本不改那两张表 —— 也就是说 即使将来完全按 RFC 实现,我那条证据依然成立,却会把它误判成未交付。 改成核验 RFC 点名的三个符号:agent_token_usage / usage_event_id / token_usage_delta 在全仓各只命中 1 个文件,就是 RFC 自己。结论不变,证据换了, 而且更硬。 这条最值得记:判据要对着「做完之后会长什么样」设计,不是对着「我猜它会改 哪里」。我当时没读 RFC-015 的存储设计就选了判据,而那份 RFC 就在仓里。 ④ 计数示例没记范围与 flag。审查在全仓重跑得到 26 和 123,而我写的是 5 和 38。 已补全命令:git grep -lE '<模式>' origin/main -- 'server/src/*.ts' 'agent-node/src/*.ts'。并记了 -lE 与 -liE 差一个文件 (readable-attachment-prompt.ts)—— flag 也算范围。 ⑤ 本仓要求所有改动跑 Docker E2E。这份文档没有可执行断言,我没有假装它有: 新增一节说明现状(每个事实附可手工复验的命令),并写出要变成门的可行形态 (像 test831 那样扫文档引用的 <文件>:<行号>,核它们在 origin/main 上仍指着 声称的内容)—— 那是独立改动,不在本 PR 里。 新增一节「这份文档的第一版自己违反了它写的规则」,把①②③原样留在文档里。 * ci(test846): 给这份文档补一道行号断言门 —— 兑现审查第 5 条 #846 的审查提了本仓要求所有改动跑 Docker E2E,而这份文档是纯散文、没有可执行 断言。我当时答的是「可以像 test831 那样把它变成门,但那是独立改动」。这就是那步。 做法:文档里嵌一个 ```doc-claims 清单(路径 :: 行号 :: 该行必须包含的子串), scripts/check-doc-claims.py 逐条打开核对,行号一漂就红。 🔴 为什么是显式清单而不是从正文正则抽:正文的引用是裸文件名(cli.ts:3450、 db.ts:393),而 cli.ts 在 agent-network/bin 和 agent-node/src 各有一个 —— 正则抽出来不知道该开哪个文件。第一版我想直接从正文抽,试到这里才发现。 代价写进了文档:清单和正文可能各写各的,门检查的是清单。 套件 tests/test846-doc-claims(alpine 按 digest 钉版,--network none): L0 分母:抽不到断言就红。0 条全过和压根没抽到,打印出来是同一片绿 L1 witnessed-red:把清单里某条行号 393→394,必须红在 drifted 上;复原回绿 L2 witnessed-red:清单清空必须红(分母承重),而不是「0 条全过」;复原回绿 L3 清单里写 ../../etc/passwd 必须红在 path-escapes-repo 上;复原回绿 (这条是从 check-doc-source-pins.py 那次审查学来的,不是我自己想到的) 验证(容器内): claims_checked=8 claims_failed=0 MUTATION_RED drifted-line-number rc=1 MUTATION_RED empty-manifest rc=1 MUTATION_RED path-escapes-repo rc=1 RESULT: PASS 退出码 0 边界写在三处(脚本头、run.sh 头、文档正文):这道门绿只说明引的行号没漂; 正文的结论对不对、引用之外的散文,它都不检查。别拿它的绿去论证那份文档的 判定是对的。 * docs(tests): 留存 test846 报告 source_commit=c00f5560a490bc39e669149030d157acb8cc6ef9;报告自带 runsh_blob,可用 git rev-parse 独立比对。 按 pre-pr-selfcheck §12,套件是新建的,报告一并落。 RESULT: PASS exit_code=0 * ci: 把 test846 接进 qa.yml —— 补上我自己漏的那一步 我在建 test831 时写过「没接进 CI 的门只是装饰」,下一轮建 test846 时自己就没接。 是这轮做 qa.yml 协调分析、查各 PR 各改了什么时发现的 —— 不是别人提的。 新增 doc-claims job + 3 条触发路径,形状与 #843 的 doc-source-pins 一致 (同一个锚点后追加、job 附在文件末尾)。 * ci: 把 doc-claims 的插入点挪开,让它与其他四个改 qa.yml 的 PR 不再冲突 上一轮我给出的是「冲突了怎么解」。这一轮做的是让冲突不发生。 冲突源于所有人都追加在同一处: paths 五个 PR 都插在 - 'tests/test746-setup-bun-pin/**' 之后 job #843 与本 PR 都追加在文件末尾(#803 插在 qa: 之前,#798/#801 插在 59 行) 改动: paths 改插到 - 'server/**' 之后 —— 距离 test746 九行,超出 git 默认上下文窗口 job 从文件末尾挪到 jobs: 之后(这个位置没有别的 PR 用) paths 是集合、jobs 是映射,位置变化不改变行为。结构断言(每个 job 有 runs-on 与非空 steps)已跑过。 这样合并时不需要任何人去解那个「公共上下文属于双方」的冲突 —— 那个坑我在 #803 上写清楚了,但最好的处理是不让人踩到它。 * 重算 6 条 claim 的行号 —— 这道门在自己的干净树上就红了,而红得对 FAIL: 干净树上这道门就红了:doc=docs/stale-issue-review.md claims=8 claims_failed=6 [drifted] agent-network/bin/cli.ts:5038 不含 «dangerously-load-development-channels» [drifted] agent-node/src/cli.ts:3450 不含 «video_gen» … 共 6 条 🔴 **一篇讲「陈旧 issue 怎么复核」的文档,自己的引用陈旧了,而且是被它自己带来的门抓到的。** 这不是尴尬,这正是这道门存在的理由 —— 它在合并之前就把作者写下与合并之间那段 时间里发生的漂移暴露了出来。 在合并 main 之后的树上逐条重算(每条都是 grep 那个 needle 拿到的真实行号): dangerously-load-development-channels 5038 → 5151 (`claudeArgs.push(...)` 那一行, 与正文第 74 行「仍在 push(...)」对得上) video_gen 3450 → 3468 (全文件唯一命中) Expose CURRENT_TASK_ID 4273 → 4291 (全文件唯一命中) total_cost_usd 2363 → 2388 (首个命中) list_providers 3867 → 3899 (3897 是注释行,3899 才是注册) addNetworkScope 2979 → 9 (见下)⚠️ `addNetworkScope` 这条我**没有恢复原意**:它在 `server/src/server.ts` 里有 **24 处**命中, 而正文里没有一行说明当初钉的 2979 指的是哪一处。我选了第 9 行的 `import` —— 它是稳定的、也确实是「server.ts 用了这个符号」的证据,但**它未必是作者当初想指的那处**。 作者若知道原意,请改成那一处。 跑过: 正常 claims=8 claims_checked=8 claims_failed=0 rc=0 变异 把一个 needle 改成 `list_providers_gone` → 红,rc=1 还原 `cmp` 逐字节相同,rc=0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: vansin <smartflowaiteam@gmail.com> Co-authored-by: t <t@x> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ci: 注册三个从没进 CI 的 Docker 门,并把 build-arg 从硬编码链改成推导
tests/ 下有四个形状完整的 Docker 门(Dockerfile + run.sh + 自己的 mutation)
从没被注册进 L1_TESTS,所以一直没人跑。逐个跑过之后:
test224-grok-preview-security PASS 39s
test597-dashboard-slash-namespace PASS 15s
test679-task-trace PASS 36s
test682-uncovered-task-trace FAIL ← 不注册,另开 issue,见下
三个通过的注册进 L1_TESTS(L1 并行跑,最差加 ~39s 墙钟)。
顺带把 build_args 从硬编码 if/elif 链改成从套件自己的 Dockerfile 推导。
那条链的失效方式是静默的:把套件加进 L1_TESTS 却忘了加分支,它会在没有
SHA 绑定的情况下跑,输出看起来一切正常。而新加的 test224/test597 用的正是
不带前缀的
ARG SOURCE_COMMIT,是原链无法表达、只能再加分支的形状。替换前核过等价性:对原链覆盖的 test686/765/766/746 四个套件,推导结果与
硬编码逐字相同。
推导是否承重,分三种(不传 build-arg 时):
test224 → rc=1 FAIL: SOURCE_COMMIT must bind… fail-closed,推导承重
test597 → rc=0 PASS 声明了却不强制
test679 → rc=0 PASS 声明了却不强制
后两个是那两道门自己的弱点,本 PR 不修,写进 NOT COVERED。
test682-uncovered-task-trace 不注册:它断言 cli.ts 里 sendPeerReplyTaskWithTrace(
恰好出现 1 次,实际 0 次。查下来不是烂了,是过时了 —— #698 有意把 peer reply
改成协商 send_peer_reply 原子工具,那条 send_task 老路被删掉,并由
agent-node/src/reply-routing-source.test.ts 断言它不得出现
(expect(source).not.toContain("sendPeerReplyTaskWithTrace({"))。
两道门方向相反,而后者在 CI 里跑着且是绿的。另外
agent-node/src/peer-reply-task-trace.ts 现在零生产调用方,只被 test682 自己引用。
单独开 issue,不在本 PR 里删任何东西。
正文最初没写、后来才加的两处(补记)——其中一条是安全相关
test224必须带--network none。它的
Dockerfile:13明写 the actual gate is run with--network none,run.sh会打印runtime executed with network disabled。而我最初写的是裸
docker run --rm。实测:带与不带这个 flag,两次都
rc=0且打印同一句 Summary,差异只有时间戳和 tarball sha256。也就是说——调用方一忘,这道安全门就在网络实际可用时产出一份声称「网络已禁用」的绿色证据。
已修为
docker run --rm --network none …,原因写进 workflow 注释。门本身「声明而不验证」的问题不在本 PR 范围内,已单独跟踪于
#814。tests/lib/**补进两处触发路径。test224的镜像COPY了tests/lib/safe-rm.sh并 source 它,而
qa.yml的 path 过滤没有它 —— 只改那个共享 helper 的 PR 不会触发这道门。已知待决(未动,等 owner 点头):
recovered-suites是单 job 串行,带
if:的 step 数 = 0,失败点之后的门不产出信号;以及ARG形状识别范围该进 NOT COVERED。详见本 PR 最新一条状态评论。