Skip to content

ci: 注册三个从没进 CI 的 Docker 门,build-arg 改推导(第四个已过时,另开 issue) - #803

Merged
vansin merged 10 commits into
mainfrom
ci/register-orphan-suites
Aug 17, 2026
Merged

ci: 注册三个从没进 CI 的 Docker 门,build-arg 改推导(第四个已过时,另开 issue)#803
vansin merged 10 commits into
mainfrom
ci/register-orphan-suites

Conversation

@vansin

@vansin vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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 里删任何东西。


正文最初没写、后来才加的两处(补记)——其中一条是安全相关

  1. 🔴 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
  2. tests/lib/** 补进两处触发路径。 test224 的镜像 COPYtests/lib/safe-rm.sh 并 source 它,
    qa.yml 的 path 过滤没有它 —— 只改那个共享 helper 的 PR 不会触发这道门。

已知待决(未动,等 owner 点头):recovered-suites 是单 job 串行,
if: 的 step 数 = 0,失败点之后的门不产出信号;以及 ARG 形状识别范围该进 NOT COVERED。
详见本 PR 最新一条状态评论。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

CI diagnosis — DO-NOT-MERGE current HEAD 47f17d77619973fd4cab1f6457118441bb4af6c6.

Failing check: L0 + L1 (report-only), run https://github.com/sleep2agi/agent-network/actions/runs/31656458158/job/94311893856. Raw sequence is decisive:

L1 registry snapshot ...
· build qa-cli-01-hub-start
Process completed with exit code 1

No Docker build log or suite output follows because Docker was never invoked.

Root cause is introduced by this PR: scripts/qa.sh runs under set -euo pipefail, and the new assignment

arg_name=$(grep ... Dockerfile | head -1 | awk ...)

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 (qa-cli-01-hub-start). The intended later if [[ -n "$arg_name" ]] is unreachable. This is not an Actions/network flake and not evidence that any of the three newly registered suites failed.

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 || true that would also hide a missing/unreadable Dockerfile. Then rerun the exact L0+L1 job and the claimed equivalence matrix for the four existing build-arg suites plus one no-ARG suite. Current report cannot be accepted because the actual CI path exits before exercising its main claim.

No code change made by reviewer; waiting for an append-only author fix and new frozen coordinates.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/qa.sh Outdated
Comment on lines +159 to +160
arg_name=$(grep -oE '^ARG (SOURCE_COMMIT|TEST[0-9]+_SOURCE_COMMIT)' \
"tests/$t/Dockerfile" 2>/dev/null | head -1 | awk '{print $2}')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread scripts/qa.sh Outdated
# 下面三个是 2026-08-13 扫出来的:形状完整的 Docker 门(Dockerfile + run.sh
# + 自己的 mutation),但从没被注册进 L1_TESTS,所以一直没人跑。
# 本地实测三个都 PASS(39s / 15s / 36s;L1 是并行跑的)。
"test224-grok-preview-security"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread scripts/qa.sh Outdated
Comment on lines +80 to +82
"test224-grok-preview-security"
"test597-dashboard-slash-namespace"
"test679-task-trace"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread scripts/qa.sh Outdated
Comment on lines +80 to +82
"test224-grok-preview-security"
"test597-dashboard-slash-namespace"
"test679-task-trace"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Second gate on new HEAD cf783526cb4d3b2fbabbb685f16a31805cab7c5a: still DO-NOT-MERGE pending truthful exact-source evidence.

The control-flow fix is narrow (one scripts/qa.sh delta) and addresses the observed zero-match exit. Current-main virtual merge was also checked correctly: merge-tree bfd613c7... preserves #802’s qa-180 Dockerfile and report byte-for-byte, so this PR does not revert #802.

However, the committed report says source_commit=034f00647d42d38d5086d7fc057eb7824a441791. That is the PR base: it contains neither the orphan-suite registration/generic inference commit (47f17d77) nor the zero-match fix (cf783526). The report therefore cannot prove current HEAD, and specifically cannot contain an execution of the fix added after it. An in-progress GitHub Actions run may prove current behavior transiently, but does not repair the repo’s false durable coordinate.

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: ... || true is zero-safe but also suppresses parser errors; missing Dockerfiles still fail at the later Docker build, while malformed/unrecognized ARGs can remain unbound for weak suites. A future meta-gate should validate declared binding names rather than infer-and-ignore. No author-branch edit made.

@vansin
vansin marked this pull request as ready for review August 13, 2026 01:15
@vansin
vansin force-pushed the ci/register-orphan-suites branch from cad8d07 to 354bf67 Compare August 13, 2026 01:16
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Review gate on current HEAD 354bf67998605ce17c0c78d16b40fc8793bf5a76: DO-NOT-MERGE pending a truthful durable report.

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 recovered-suites job. The exact Actions job is green (test224, test597, test679 all built and ran successfully; job 94314095687 completed in 2m44s), and L0+L1 plus both package-unit jobs are green. This supports the design.

It does not repair the committed report. docs/tests/report-register-orphan-suites.txt still says source_commit=034f00647d42d38d5086d7fc057eb7824a441791, the PR base. That commit contains none of the registration, zero-match fix, or the new independent-job topology. Therefore the durable repository evidence still points at a tree incapable of producing its claims.

Required closeout: append a report-only child after the full current check set finishes. It must distinguish the immutable source HEAD 354bf679... from the GitHub PR virtual-merge SHA used as $GITHUB_SHA, record the recovered-suites job/log coordinates and results, preserve the test597/test679 SOURCE_COMMIT enforcement limitation, and state that qa.yml remains report-only unless branch protection changes. Do not rewrite or relabel the base run.

No reviewer branch edit or merge performed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread .github/workflows/qa.yml
- 'tests/test725-agent-node-unit-ci/**'
- 'tests/test745-agent-network-unit-ci/**'
- 'tests/test746-setup-bun-pin/**'
- 'tests/test224-grok-preview-security/**'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread .github/workflows/qa.yml Outdated
steps:
- uses: actions/checkout@v4

- name: Build test224-grok-preview-security

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread .github/workflows/qa.yml Outdated
-f tests/test224-grok-preview-security/Dockerfile .

- name: Run test224-grok-preview-security
run: docker run --rm anet-test224-grok-preview-security

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 不修。
@vansin
vansin force-pushed the ci/register-orphan-suites branch from 2d4bf19 to 6b1953a Compare August 13, 2026 02:19
两条都是独立审(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 里三个都会跑完,
  排序不影响是否产出证据。
@vansin
vansin force-pushed the ci/register-orphan-suites branch from 6b1953a to c933f5f Compare August 13, 2026 02:33
vansin pushed a commit that referenced this pull request Aug 13, 2026
独立审(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 在解决的问题,不是本门的判据。
vansin pushed a commit that referenced this pull request Aug 13, 2026
自查发现本报告里有两个不同的 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
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

状态更新:上面那条 DO-NOT-MERGE 针对的是 head 354bf679,已被取代

那条评论提出的是 set -euo pipefail + 无匹配 grep 导致赋值早退、Docker 根本没跑。
该问题已修(命令替换末尾加 || true),并且我把三个套件从 L1 撤出、改放独立 job

当前坐标(冻结中,我不再改动)

source     aeec4b9c…   (代码提交)
report     c933f5fc…   (report-only 子提交)
base       034f0064    (current main)

witnessed-red 是在真脚本上做的:去掉 || true → rc=1 且日志停在
· build qa-cli-01-hub-start,与当初 CI 的症状逐字一致;加回后三种 Dockerfile 形状
(无 ARG / TEST765_ 前缀 ARG / 裸 ARG SOURCE_COMMIT)各取一个跑真 qa.sh --l1,ALL PASS in 84s

近期审查发现(经 commhub 收到,转录到此;我的转录,非他们的终审)

一条我认为是真的、且我该改 —— 但冻结中没动手:

recovered-suites单 job 串行;GHA 默认前一步失败会跳过后续
test224 红时,test597 / test679 不产出信号

我核了自己那个 job:7 个 step(checkout + build/run ×3),带 if: 的数量 = 0
所以它不造假绿(job 已红),但我在 PR 正文里按「三个门都跑」描述,那句话不成立

证据强度说准:「step 都不带条件」是实测 grep;「无条件 step 会被跳过」是 GHA 文档语义——
我在本仓找不到直接实证(近 80 个 run 里只有 1 个失败,且其失败点之后没有可观察的无条件 step)。

我的判断:这三个门互相独立(安全扫描 / dashboard 路由 / task-trace),又都是长期没人跑的孤儿门,
该要全量诊断而非 fail-fast,最小改法是给后续 step 加 if: always()等 owner 点头再改。

交叉核确认的其余点:三个套件的 ARG 形状与 qa.yml 传参完全吻合;
test224--network none 只加在 Run 步、Build 保持联网,编排正确;
三套件 COPY 的输入在 pull/push paths 两侧均覆盖;
|| true 正确封住无 ARG 时的 pipefail 早退。

一条我之前没意识到的边界:那段推导的 grep 只识别列首、无默认值的 ARG 两种形状;
未来出现缩进或 ARG X=default 会静默不传。当前 L1 分母没有这种形状,不是缺陷,但该进 NOT COVERED
同样等点头再改。

待决清单(共 2 处,均未动)

  1. 后续 step 加 if: always()(或拆 matrix);
  2. NOT COVERED 补上「ARG 形状识别范围」这一句。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

五条 P1 对当前 head c933f5fc 复核完毕:3 已修,2 仍在

审查针对 47f17d77 / cad8d076,坐标已过期,逐条对当前 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 待复审收口后一并落。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

补裁第 7 条 P1 —— 我上一轮漏了它,「五条全部裁定」那句话是错的

上一轮我说本 PR 是「五条 P1」并逐条给了裁定。实际是六条 P1 + 一条 P2。 我当时用 ... | head -6 列评论,第 7 条被截掉了,而它恰好是 P1。

(这是同一个坑在本轮内第四次出现:head -N 用在「有没有某个东西」的查询上,会把「没有」和「有但被截断」变成同一个输出。前三次分别咬在 --network none 的核对、comments 字段、以及 find 吞 stderr 上。)


qa.yml:100 Preserve recovered-suite reports outside removed containers —— 成立

当前 head c933f5fc 上,三个 Run 步骤全是裸 --rm:

:106  run: docker run --rm --network none anet-test224-grok-preview-security
:116  run: docker run --rm anet-test597-dashboard-slash-namespace
:126  run: docker run --rm anet-test679-task-trace

没有 -v 挂载、没有 docker cp、没有 actions/upload-artifact

test224 把 report-test224.txt 写在容器内 /artifacts 下,--rm 随即把那个文件系统删掉;test597 同样;test679 的输出只是 stdout。

结果是:这三个套件每次 CI 都真的跑了,但跑完什么都不留。 仓里能看到的只有更早提交进来的旧报告 —— 而那些报告与本次执行无关。

为什么这条比看起来重要

本仓的约定是实测要落 docs/tests/report-testN.txt。这条与 #809 上那条 P1(「干净容器的 capture 需要落成 report 制品」)是同一个根因:

只存在于易失上下文里的证据,不是证据。

#809 那次的代价已经具体化了:因为没有 report,「零连接」这个关键前提没被写下来,于是一个空闲 hub 的单样本被我写成了关于稳定线的安全结论(见 #809)。

这里的风险形态一样 —— 门跑绿了,但没有任何东西能证明它这次跑的是什么、在什么条件下绿的。等到某次它变红,或者有人质疑某个结论时,回不去。

修法

三个 Run 步骤都需要把产物带出容器,再落成仓内制品或 CI artifact:挂 -v "$PWD/docs/tests:/artifacts"(或跑完 docker cp),并配 actions/upload-artifact。test679 需要先让它把结果写成文件而不是只打 stdout。


本 PR 更正后的裁定表

# 内容 裁定
a test224 --network none 已修
b tests/lib/** 路径 已修
c 容忍无 ARG SOURCE_COMMIT 已修
d 三套件 Bun 输入未钉死 仍在
e 安全套件排在最前 仍在
f --rm 销毁套件报告 仍在(本次补裁)
(P2)注册套件变更时触发 QA 未单独裁定

冻结中未改分支。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

给这条 PR 补一个分母:200 个有 Dockerfile 的套件里,178 个不被任何自动 runner 引用

本 PR 捞回了三个「从没进 CI 的完整 Docker 门」(test224 / test597 / test679)。我做了一次全仓审计,想知道这是三个特例还是一片。

测量

有 Dockerfile 的套件            200
被 scripts/qa.sh 或任一 workflow 文本引用   22
不被引用                        178

但 178 不是缺陷数 —— 先说清楚判据的边界

我第一版的判据是「名字出现在 qa.sh 或 workflow 里才算接线」。这个前提部分不成立,查了才知道:

tests/run-parallel.sh 是按编号发现套件的,不靠硬编码名字:

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 或 qa.sh 调用它 —— 全仓只有 tests/README.mddocs/open-source-quality-review.md 两处文档提到它。所以它是手动工具,不改变「这 178 个不进 CI」这个结论,只改变「它们完全跑不了」这个说法(手动仍可跑)。

更要紧的是:其中大多数本来就不该常驻

命名形态 test<issue#>-<topic> 说明它们多是一次性验证套件 —— 当时为某个 issue 建、跑过、留了 report。这类套件不是「孤儿」,是归档

这跟我在 #831 上分的那两类是同一个区分:

参考页 pin / 常驻门 changelog pin / 一次性验证套件
描述的是 现在的行为 当时发生了什么
该怎么办 保持有效 保持原样

把 178 个全部接进 CI 会是另一种错,就像给 changelog 更新行号一样。

所以这个数字的正确用法是「候选表」

真正的问题不是「有 178 个没接线」,而是:这 178 个里,哪些编码的是应当持续成立的不变量?

本 PR 的三个正是这样的例子 —— test224 是安全套件,它显然应当常驻,而它此前从没进过 CI。这说明至少有一部分本该常驻的套件掉在了这 178 里,不是全部都是归档。

我没有能力单方面判定其余 175 个各属哪一类(那需要知道每个套件当初的意图)。这里只提供分母和判据边界,供决定要不要做一次分类。

一个可能有用的起点:安全/权限/凭据相关的套件优先看(test3-securitytest9-permissionstest30-v0.8-auth-deprecationtest631-private-config-permissionstest637-database-url-guardtest660-explicit-prod-db-optin 等),因为这类不变量通常不会因为 issue 关闭而失效。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

本 PR 在两种合并顺序下#798/#801.github/workflows/qa.yml 上冲突 —— 是真交叠,不是顺序问题。完整的 14 条合并就绪度与顺序实测见 #801 (comment) 。不构成合并建议或授权。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

更正我上一条:这个冲突不需要任何设计决策 —— 取并集即可,已验

上一条我说本 PR 与 #798/#801qa.yml 上是「真交叠,需人工解,或先决定 qa.yml 的最终形态」。后半句说重了。

把冲突真解开看,它是纯文本相邻冲突,不是语义冲突:

<<<<<<< HEAD   (#798 加的)
      - 'tests/test798-server-unit-ci/**'
      # test798 的镜像 COPY 了 test601 的 race-worker.ts …
      - 'tests/test601-hub-scheduled-tasks/**'
=======        (#803 加的)
      - 'tests/test224-grok-preview-security/**'
      - 'tests/test597-dashboard-slash-namespace/**'
      - 'tests/test679-task-trace/**'
      - 'tests/lib/**'
>>>>>>>

两边都只是往同一个 paths 列表里加不同的条目。 git 冲突是因为插入点相邻,不是因为内容互斥。

取并集后的实测

校验 结果
残留冲突标记 0
YAML 解析 通过
job 数 7
test798 / test601(来自 #798) 7 / 4 处
test224 / test597 / test679 / tests/lib(来自 #803) 8 / 7 / 7 / 2 处

两处冲突块(pull_request.pathspush.paths)形状相同,同样处理。

所以合并顺序的结论要更新

原来我把这条列成「#803 需人工决策」。实际是:

一句

我上一条把「git 报了冲突」直接读成了「需要人来决定」。这两件事不一样 —— 相邻插入的冲突,git 报冲突只是因为它不敢替你选顺序,不是因为存在取舍。

判据:看到冲突先把它解开读一遍,再判断它是取舍型还是相邻型。前者要人拍板,后者只要有人动手。

(完整的 14 条合并顺序实测见 #801 上那条评论。以上仍不构成合并建议或授权。)

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

qa.yml 的四处待改动:摊开对照 + 一个具体的合并顺序

我连着三轮在汇报里记「qa.yml 有 N 处待改动,需要协调」,却没给过协调方案。这条把它做掉。

main 上的现状

.github/workflows/qa.yml   107 行,3 个 job
  12  on:            (pull_request.paths 与 push.paths 两份清单)
  44  jobs:
  45    agent-network-unit
  62    agent-node-unit
  79    qa

四处改动各自动了哪里

来源 paths 清单 job 主体
#803 两份各 +4 行 第 76 行插入 80 行(recovered-suites,落在 qa: 之前)
#843 两份各 +4 行 文件末尾 +22 行(doc-source-pins)
#846 两份各 +3 行 文件末尾 +22 行(doc-claims)
#31 的接线建议 尚未成 PR

冲突面:只在 paths 清单,而且是机械并集

三者都追加在同一个锚点之后:

      - 'tests/test746-setup-bun-pin/**'      ← 锚点,两份清单里各出现一次
+     …#803 的 4 行 / #843 的 4 行 / #846 的 3 行…

git 会在这两处报冲突,解法是保留全部三块 —— 没有语义取舍,顺序也不影响行为(paths 是集合)。

job 主体方面:#803 插在 qa: 之前,#843#846 都追加在文件末尾。#803 与另外两者不冲突;#843#846 都在末尾,会冲突,但同样是「两个 job 都留下」。

建议的合并顺序

1. #803   (主体最大、插入位置最特殊,先落省得后面反复 rebase)
2. #843   (末尾追加 doc-source-pins;paths 取并集)
3. #846   (末尾追加 doc-claims;paths 取并集)

每一步的冲突解法都是并集,不需要判断谁对谁错。

⚠️ #843 还有一个非 qa.yml 的合并动作项:它引入的 docs/doc-source-pins-baseline.txt#810 / #834 耦合(那两个 PR 会让基线里的条目失效)。详见 #843 上的那条测量 —— 合完跑 python3 scripts/check-doc-source-pins.py . --write-baseline 即可。

顺带:我自己漏的一步,这轮补上了

建 test831 时我写过「没接进 CI 的门只是装饰」。下一轮建 test846 时,我自己就没接。是做这次对照、逐个查各 PR 改了什么时发现的 —— 不是别人提的。已补(46aef155),形状与 #843 一致。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

🔴 更正:我上一条给的 qa.yml 冲突解法是错的,而且错得很隐蔽

上一条我写「解法是保留全部三块 —— 没有语义取舍」。我这轮把它干跑了一遍,那句话是错的,并且照做会产出一个看起来正常、实际有一个 job 永远不执行的 CI 文件。

干跑怎么做的

origin/main 起一个临时分支,按我建议的顺序依次合 #843 → #844 → #845 → #846:

合 ci/doc-source-pin-guard        冲突 0
合 docs/mcp-tools-symbol-anchors  冲突 0
合 docs/mcp-tools-inline-anchors  冲突 0
合 docs/stale-issue-review        冲突 1  → .github/workflows/qa.yml

前三步零冲突这部分预测是对的(它们是一条链)。问题出在第四步的解法。

git 给的是 4 个冲突块,不是 1 个

#1 paths     ours 4 行 / theirs 3 行
#2 paths     ours 4 行 / theirs 3 行
#3 job-body  ours 2 行 / theirs 2 行     ← 两个 job 的**头部**
#4 job-body  ours 14 行 / theirs 13 行   ← 两个 job 的**主体**

对 ①② 取并集是对的(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 全在这里)

实测解析结果:

doc-source-pins    runs-on=None        steps=0
doc-claims         runs-on=ubuntu-latest  steps=5

这个文件是合法 YAML。 GitHub 会接受它,doc-source-pins 会作为一个什么都不做的 job 出现在检查列表里 —— 名字在、绿灯亮、门从没跑过。

我试着写「正确解法」,第二次错得更明显

我又写了一版脚本,把 ③ 的两个头分别与 ④ 的两个体重新配对。产出的 YAML 直接解析失败(expected <block end> at line 124)。

🔴 但最该记的是这一条

上面两版坏文件,我都提交了,然后 test831 与 test846 都退出 0。

因为这两道门根本不看 qa.yml。实测全仓:

.github/ 下的 workflow 与脚本、scripts/、tests/  中
  yamllint / actionlint 命中 = 0 个文件

没有任何东西会在合并前校验 qa.yml 自身。 一个坏掉的 workflow(不论是解析失败还是 job 空转)会一路走到 main,而所有现有的门都是绿的。

更正后的建议

  1. paths 两块:取并集,顺序无关(这部分我原来的说法成立)。
  2. job 主体两块:不能逐块并集。 必须把属于同一个 job 的碎片重新拼成完整块,再依次放置。这需要人看着改,脚本化容易出上面两种错的任意一种。
  3. 合完之后至少跑一次 yaml.safe_load 并断言每个 job 都有 runs-on 与非空 steps —— 光"能解析"不够,steps=0 也能解析。

顺带一个我认为该做但没做的

这个仓里没有任何 workflow 语法/结构校验。加一个很便宜(一个 actionlint 或十行 python 断言),而它能挡住的正是「job 名字在、但 steps 为空」这类假绿。我没有直接加,因为 qa.yml 现在有 5 个 PR 在改,再塞一个改动进去只会让协调更难 —— 建议等这批合完再加。

干跑用的三个临时分支与 worktree 已全部删除,没有推送任何东西

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

上一条我说「这需要人看着改」,但没给样子。这条给了 —— 附根因

根因:git 的「公共上下文」可以在语义上属于双方

冲突区的实际结构是这样的:

<<<<<<< HEAD
  doc-source-pins:
    name: doc source-pin floor (Docker)
=======
  doc-claims:
    name: doc claim freshness (Docker)
>>>>>>>
    runs-on: ubuntu-latest          ← git 认为这是公共上下文,只保留一份
    timeout-minutes: 8
    steps:
      - uses: actions/checkout@v4

<<<<<<< HEAD
      …test831 的 build/run 步骤…
=======
      …test846 的 build/run 步骤…
>>>>>>>

中间那 5 行在两个 job 里逐字相同,所以 git 把它判成公共上下文、只留一份。但它语义上属于两个 job 各一份

这就是盲目并集必然坏掉的原因:并集只处理 <<</===/>>> 之间的内容,而错在那之外 —— 它把一份共享文本留给了后面那个 job,前面那个 job 于是只剩一行 name

正确解法

每个 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 的步骤…

paths 那两块仍然是取并集(这部分我原来的说法成立)。

验证(同一个合并点,两种解法对照)

盲目并集      doc-source-pins steps=0   ← 合法 YAML,GitHub 接受,门从不跑
本次解法      doc-source-pins steps=3
              doc-claims      steps=3
              5 个 job 全部 runs-on 非空 + steps 非空
              test831 退出码 0(4 处 MUTATION_RED)
              test846 退出码 0(3 处 MUTATION_RED)

合完请跑这段断言 —— 「能解析」不够

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 为空"

坏掉的那版解析是通过的,只有这两条断言能拦住它。

干跑分支已删,没有推送任何东西

vansin pushed a commit that referenced this pull request Aug 13, 2026
上一轮我给出的是「冲突了怎么解」。这一轮做的是让冲突不发生。

冲突源于所有人都追加在同一处:
  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 上写清楚了,但最好的处理是不让人踩到它。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

上一条给的是「冲突了怎么解」。这一条做的是让冲突不发生(1d00bd1f)

我前两轮在这里写了根因(git 把两个 job 共享的 runs-on/steps:/checkout 判成公共上下文只留一份)和可抄的正确解法。但最好的处理不是把坑说清楚,是不让人踩到它

冲突的来源:所有人都挤在同一个位置

paths   #798 #801 #803 #843 #846  五个 PR 全部追加在 - 'tests/test746-setup-bun-pin/**' 之后
job     #798/#801 插在第 59 行     #803 插在 qa: 之前
        #843 追加在文件末尾        #846 也追加在文件末尾   ← 撞的就是这里

改动:把 #846 挪到没人用的位置

paths   改插到 - 'server/**' 之后 —— 距离 test746 九行,超出 git 默认的上下文窗口
job     从文件末尾挪到 jobs: 之后

paths 是集合、jobs 是映射,位置变化不改变行为。改完跑了结构断言(每个 job 有 runs-on 与非空 steps)。

干跑结果

#843 → #844 → #845 → #846 合:

合 #846 之后  冲突文件 = 0        (原来是 1,且是危险的 job-body 交织那种)
jobs = doc-claims / agent-network-unit / agent-node-unit / qa / doc-source-pins
结构不合格的 job = 无

再把 #803 也合进来:

冲突块 2 个,归因后 两块都是 #843 × #803,而且都是 paths 条目
  ours   = #843 的 - 'docs-site/**' …
  theirs = #803 的 - 'tests/test224-grok-preview-security/**' …
#846 已完全不参与

剩下的 #843 × #803 是安全的那一类 —— 纯 paths、取并集即可,没有 job 主体交织。

解成并集后的五合一结果(干净树,无冲突标记):

HEAD 脏度 = 0
jobs = doc-claims / agent-network-unit / agent-node-unit / recovered-suites / qa / doc-source-pins
结构不合格的 job = 无
paths pr/push = 21 / 20
test831 退出码 0(4 处 MUTATION_RED)
test846 退出码 0(3 处 MUTATION_RED)

一个我没当证据用的中间结果

第一次跑门时树上还带着冲突标记,两道门照样退出 0 —— 因为它们不读 qa.yml。那次的退出码说明不了任何事,我没有拿它当证据,重新在干净树上跑了一遍。

这正是我在这条 PR 上早先记过的那个盲点:门绿不代表 workflow 文件是好的。

干跑用的临时分支与 worktree 已删,没有推送任何东西。

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

独立深审结论:BLOCKER / DO-NOT-MERGE(exact head 4a00e683…)。

三个 recovered suite 本体均通过,但 exact-head CI 最后的 artifact upload 稳定失败:report-test224.txt 由 root 容器写成 mode 0600,qa.yml 将宿主 artifacts 目录 bind mount 后直接交给 upload-artifact,Actions 原始错误为 EACCES: permission denied, open .../suite-artifacts/report-test224.txt

这不是 cosmetic:PR 的目标是恢复三道长期信号,而当前 job 必红、证据也无法归档。请让容器按 runner uid 写入,或退出后显式修正属主/可读权限,并在同一 exact head 重跑整 job 到绿。修复后还需按 #798 之后 rebase,逐 job 保留完整 YAML 块。

只读审查;未改代码、未 approve/merge/deploy。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

CI 红的根因:不是门失败,是产物上传失败(只读诊断,没改任何东西)

失败 check recovered suites (Docker),失败步骤是 Upload recovered-suite artifacts ——
不是任何一道测试步骤。

先看被测的东西:全过

RESULT: PASS                                                        (×2)
PASS: targeted Docker context contains no host auth/config state
PASS: real child env equals the reviewed set; text boundaries redact; config/session dirs are 0700 …
PASS: candidate tarballs contain runnable entrypoints and force publishConfig.tag=preview
PASS: tarballs, extracted payloads, build output, test output, and report contain zero synthetic marker bytes
Summary: PASS (Docker-only; runtime executed with network disabled; no real credential was read)

这个 PR 要注册的那几道门,跑完了而且是绿的

红在哪

With the provided path, there will be 4 files uploaded
Artifact name is valid!
Root directory input is valid!
Beginning upload of artifact content to blob storage
##[error]An error has occurred while creating the zip file for upload
Error: EACCES: permission denied, open '/home/runner/work/_temp/suite-artifacts/report-test224.txt'

actions/upload-artifact@v4runner 用户身份打包,而
suite-artifacts/report-test224.txtDocker 容器写出来的 —— 属主/权限不匹配,
打开即 EACCES,zip 创建失败,整个 job 判红。

注意 if-no-files-found: warn,而且日志明说「there will be 4 files uploaded」——
所以不是「没找到文件」那种情况,文件在,只是读不了。

修的方向

在 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 写(docker run --user "$(id -u):$(id -g)"),
或用 docker cp 之后显式 chmod。具体取哪种由维护者定 —— 我只定位到 EACCES 这一层。

为什么值得单独说清楚

「CI 红」在 PR 列表里长得都一样。但这条红不代表这个 PR 注册的门有问题:
门全绿,红的是把报告传出来的那一步。如果按「红了就先搁着」处理,
这个 PR 会因为一个和它主张无关的权限问题被无限期挂起。

(顺带:同一批扫描里 #801 也是红的,但那条是真的门失败 ——
TEST798_RUNSH_BLOB 没被传进去,详见我在 #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()`,因为前面步骤红时
更需要把证据传出来。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

复核这条 BLOCKER 是否仍成立(2026-08-14)

判定针对 exact head 4a00e683…,当前 head 已是 134af215,所以逐条重跑了一遍。
先披露:那次权限修复是我推的(134af215),所以第 1 条我不做主观判断,只贴可复验的事实。

第 1 条(artifact upload 因 0600 权限稳定失败)—— 已解决,有产物为证

判定要求「让容器按 runner uid 写入,或退出后显式修正属主/可读权限」。当前 head 的 qa.yml:158-161:

- 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 步骤带 if-no-files-found: warn,
目录是空的它也会绿。所以我直接去查产物,而不是看 job 颜色:

run 31720356471(anet QA (v0),head_sha=134af215cedd)
  产物:recovered-suite-artifacts   6831 字节   expired=false

解包后:
  report-test224.txt   1117 字节     ← 正是当初报 EACCES 的那个文件
  test224.log          1117 字节
  test597.log         14787 字节
  test679.log          3684 字节

report-test224.txt 头部:
  # test224 — Grok preview credential and package gate
  date: 2026-08-13T16:25:04+00:00
  network: disabled by runner

recovered suites (Docker)每一步都是 success,含 Normalize…Upload…

顺带记一个我自己差点栽的坑:我第一次查产物查的是 lint 那个 workflow 的 run(0 个产物),
差点得出"仍未解决"。同一个 head 上有 6 个 run,得挑 anet QA (v0) 那个

第 2 条(在同一 exact head 重跑整 job 到绿)—— 已满足

134af215 上 11 个 check 全部 SUCCESS,含 recovered suites (Docker)
agent-node unitagent-network unit、两个 e2e 与三道 lint 门。

第 3 条(按 #798 之后 rebase)—— 现在无法满足,而且不是本 PR 能单方面做的

git merge-base --is-ancestor pr/798 pr/803  → 否
git merge-tree pr/798 pr/803                → 冲突:.github/workflows/qa.yml

#798 本身还没合进 main,所以"在 #798 之后 rebase"是一条合并顺序要求,
要等 #798 落地才能执行。这一条不该继续挂在本 PR 头上当 DO-NOT-MERGE 的理由 ——
它是 merge queue 的排序问题,建议按 #856 里那份顺序处理。

🔴 顺带发现一条判定没覆盖的:产物里的 source_commit 指向一个仓库里不存在的提交

report-test224.txt:  source_commit=4e3f28e62a6cb2dfe7a7bd887af2cf66e716d154

git cat-file -t 4e3f28e6…                 → 本地不存在
gh api …/commits/4e3f28e6…                → "Merge 134af215… into 034f0064"

成因在 qa.yml:132:

--build-arg SOURCE_COMMIT="$GITHUB_SHA"

pull_request 事件下 GITHUB_SHAGitHub 临时合并提交,不是 PR head。

这不算错:被测的确实是那棵合并树,记它比记 head 更准。但它有两个实际后果:

  1. 拿这个 SHA 去 git show 会失败,除非先 git fetch origin refs/pull/803/merge;
  2. PR 关闭后那个 ref 会消失,归档下来的产物就失去了可解析的溯源锚点。

建议两个都记(改动很小):

--build-arg SOURCE_COMMIT="$GITHUB_SHA" \
--build-arg SOURCE_HEAD_SHA="${{ github.event.pull_request.head.sha || github.sha }}"

push 事件下 GITHUB_SHA 就是真实提交,所以这只影响 PR 上的运行。
这条独立于原判定的三条,不挡合并,但会影响以后拿这些产物当证据时能不能复原现场。

建议

原 BLOCKER 的第 1、2 条已由 134af215 解决且我用产物本体验过;第 3 条是合并顺序,
不是本 PR 的缺陷。建议把判定从 DO-NOT-MERGE 降为「可合,但须排在 #798 之后并解 qa.yml 冲突」。

(只读:未改本 PR、未 approve、未 merge;产物是通过 API 下载到临时目录读的。
第 1 条涉及我自己推的提交,判断请以上面的产物清单和步骤结论为准,不以我的结论为准。)

@vansin

vansin commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

【独立复核结论更新|撤回旧 BLOCKER】

我先前 15:43 的 DO-NOT-MERGE / BLOCKER 结论审的是过期 head 4a00e6833e1cf2c726e7d1b348d8310ff788e889,不再适用于当前 PR。现按远端当前 head 134af215ceddebc14a2f8830b22dc1e0d69ac082 重新核验,结论更新为:CLEAN,技术上可合

独立证据:

  • current head 的 11 个 checks 全部 SUCCESS;
  • 重新下载 Actions run 31720356471recovered-suite-artifacts
  • report-test224.txt 实际存在,1117 bytes,下载后 mode 0644,内容以 Summary: PASS 收口;
  • 4a00e683 → 134af215 的唯一 delta 是 .github/workflows/qa.yml 新增 always-run 的权限归一化:上传前 chown runner uid/gid,并 chmod u+rw。这与真实可下载 artifact 共同证明旧 EACCES 已闭合。

因此:明确撤回旧 BLOCKER / DO-NOT-MERGE。

仍保留一条合并顺序约束:#798#803qa.yml 的 pull_request/push paths 两处有真实文本冲突。应先合 #798,再把 #803 rebase 到更新后的 main,人工保留双方触发路径并重新确认 checks;不要直接按两个旧 head 连续合并。

本评论只更新审查结论;未 merge、未改代码、未部署。

vansin added a commit that referenced this pull request Aug 17, 2026
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>
vansin added a commit that referenced this pull request Aug 17, 2026
* 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>
@vansin

vansin commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

合了。这是今晚我在 #863 上指出的那件事的解药,所以我核得比平时细。

我核的是「它真的跑了吗」,不是「它绿吗」

从 CI 日志里拉出来的程序输出(已滤掉命令回显):

[test597] MUTATION_RED: dashboard-native-slash-must-not-enter-scheduler   rc=1
          MUTATION_RED: namespaced-command-must-enter-scheduler           rc=1
          MUTATION_RED: dashboard-interval-migration-notice-is-visible    rc=1
          MUTATION_RED: near-match-must-not-route                         rc=1
          MUTATION_RED: cli-must-emit-namespaced-command                  rc=1
          MUTATION_RED: node-token-cannot-forge-dashboard-pass-through    rc=1
          RESULT: PASS
[test679] RESULT: PASS
[test224] PASS: network is off (verified: /sys/class/net = [lo])
          PASS: targeted Docker context contains no host auth/config state
          PASS: real child env equals the reviewed set; …0700 / 0600
          Summary: PASS (Docker-only; runtime executed with network disabled)

六条变异红全部落地 —— 这三个套件是第一次在 CI 里真的跑,而且不是空转。

🔴 顺带一个闭环:test224 那行 PASS: network is off (verified: /sys/class/net = [lo]) 是我今晚合的 #923 加的断言 —— 在那之前它只 log "network: disabled by runner"(一句自我声明,不是断言)。今天是它第一次真的被 CI 执行加断言和让断言被执行是两件事,今晚正好凑齐。

job 本身我核了三点

  1. 不是只 build —— 每个套件都有独立的 docker run step;
  2. set -o pipefail| tee 前面 —— 不加的话 tee 的 0 会盖掉套件的非零退出,这道门会变成永远绿;
  3. --network none 只给 test224 —— 和它自己的断言对得上,其余两个需要网络。

qa.yml 冲突

纯新增撞纯新增,取并集。按 main 那条判据(「一个测试目录属于这里,当且仅当 CI 真的执行它」)核过,新加的四条全部成立:三个套件各有真 docker run,tests/lib/** 被这三个镜像 COPY 进去。

合并后:yaml.safe_load OK / 4 个 job 名两两不同 / check-l1-paths-sync.py--selftest 都 rc=0。

冲突是我造成的(今晚 34 个提交动过 qa.yml),我自己解的。

剩下的账

tests/ 下还有约 160 个目录没有任何 workflow 会跑。本 PR 收回 3 个。#861 的那个数字不会因为本 PR 显著下降 —— 这是第一步,不是终点。

#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>
@vansin

vansin commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

二次合并 main(#798 落地后 qa.yml 又撞了一次,同样是纯新增撞纯新增,取并集,两边条目零重复)。重跑后 14 个 check 全绿,其中 recovered suites (Docker)server unit (Docker, non-root) 都在。上面那条 review 里的实测数字仍然成立。

@vansin
vansin merged commit cc325f4 into main Aug 17, 2026
14 checks passed
vansin pushed a commit that referenced this pull request Aug 17, 2026
七处冲突。逐个核过之后的处理:

## 四个共享文件:取 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>
vansin added a commit that referenced this pull request Aug 17, 2026
* 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>
vansin added a commit that referenced this pull request Aug 18, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants