Skip to content

docs(server): 写明单测的两条 DB 契约(第二条此前只存在于 qa.sh 里) - #805

Merged
vansin merged 6 commits into
mainfrom
docs/server-test-db-contract
Aug 18, 2026
Merged

docs(server): 写明单测的两条 DB 契约(第二条此前只存在于 qa.sh 里)#805
vansin merged 6 commits into
mainfrom
docs/server-test-db-contract

Conversation

@vansin

@vansin vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

docs(server): 写明单测的两条 DB 契约,第二条此前只存在于 qa.sh 里

今天给 server 补聚合单测门时踩了这个坑:第一版用一个共享 COMMHUB_DB 跑
bun test src/,红了 4 条,看起来像产品坏了。逐条查下来一条产品缺陷都没有 ——
是我违反了一条从没写在任何文档里的契约。

两条契约,第二条不明显:

  1. COMMHUB_DB 必须设。这条已经是 fail-closed 的:NODE_ENV=test
    (bun test 会设,已实测)且 COMMHUB_DB 未设时,db-adapter 拒绝并报
    "REFUSING to open the default SQLite database"。默认值 ~/.commhub/commhub.db
    在任何跑过 hub 的机器上都是活库,这道守卫不是形式主义。

  2. 每个测试文件要各自一个库。这条没有任何东西强制 —— 设一个共享的
    COMMHUB_DB 能过上面那道守卫,然后莫名其妙红几条。真正的契约只编码在
    scripts/qa.sh 的 L0 循环里:COMMHUB_DB=/tmp/qa-l0-$name.db bun test <一个文件>。

症状写了两个读过原文的例子,都是在契约下正确的全局计数断言:
admin-networks-http: expect(new Set(...)).toEqual(new Set([adminNetworkId, memberNetworkId]))
scheduled-tasks-http: expect(runDueScheduledTasks().processed).toBe(1)
共享库时别的文件建的网络、到期的任务都算进来,数就不对了。

引用了 tests/test798-server-unit-ci(#798),那道门就是按逐文件独立库跑的。

今天给 server 补聚合单测门时踩了这个坑:第一版用一个共享 COMMHUB_DB 跑
bun test src/,红了 4 条,看起来像产品坏了。逐条查下来一条产品缺陷都没有 ——
是我违反了一条从没写在任何文档里的契约。

两条契约,第二条不明显:

1. COMMHUB_DB 必须设。这条**已经是 fail-closed 的**:NODE_ENV=test
   (bun test 会设,已实测)且 COMMHUB_DB 未设时,db-adapter 拒绝并报
   "REFUSING to open the default SQLite database"。默认值 ~/.commhub/commhub.db
   在任何跑过 hub 的机器上都是活库,这道守卫不是形式主义。

2. **每个测试文件要各自一个库。这条没有任何东西强制** —— 设一个共享的
   COMMHUB_DB 能过上面那道守卫,然后莫名其妙红几条。真正的契约只编码在
   scripts/qa.sh 的 L0 循环里:COMMHUB_DB=/tmp/qa-l0-$name.db bun test <一个文件>。

症状写了两个读过原文的例子,都是**在契约下正确的全局计数断言**:
  admin-networks-http:  expect(new Set(...)).toEqual(new Set([adminNetworkId, memberNetworkId]))
  scheduled-tasks-http: expect(runDueScheduledTasks().processed).toBe(1)
共享库时别的文件建的网络、到期的任务都算进来,数就不对了。

引用了 tests/test798-server-unit-ci(#798),那道门就是按逐文件独立库跑的。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Current-main review: DO-NOT-MERGE as written. The two database-isolation principles are useful, but the durable runbook currently points to an artifact that does not exist in main and gives a non-standalone command.

  1. tests/test798-server-unit-ci exists only in still-blocked PR test(ci): 给 server 补上聚合单测门(69 个单测此前 CI 只跑 6 个) #798; it is absent from current main. The paragraph says “The CI gate that runs this suite” as present tense, which would teach a fresh clone to invoke a nonexistent path. test(ci): 给 server 补上聚合单测门(69 个单测此前 CI 只跑 6 个) #798 is currently DO-NOT-MERGE for stale/false report provenance, so this docs PR cannot assume its landing.
  2. The shown command uses /tmp/qa-l0-$name.db, but the README never defines name, and it omits the rm -f that scripts/qa.sh performs before each file. Copy/paste therefore expands to /tmp/qa-l0-.db and reuses stale state on later runs—the opposite of the documented isolation contract.
  3. Current scripts/qa.sh does use a fresh path per entry, but only for its five-file L0_TESTS whitelist; it is not yet a complete server-unit gate. The wording must distinguish that current fact from the proposed test(ci): 给 server 补上聚合单测门(69 个单测此前 CI 只跑 6 个) #798 full-suite behavior.

Narrow acceptable fix: make the example executable (e.g. an explicit file slug plus pre-run removal, or a complete loop), describe current scripts/qa.sh accurately, and label the full tests/test798-server-unit-ci gate as proposed/pending #798 rather than present. Alternatively, merge this documentation only after #798 has truthful provenance and lands.

No author-branch edit or production action 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: 938c9d7550

ℹ️ 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 server/README.md Outdated
the real contract:

```bash
COMMHUB_DB=/tmp/qa-l0-$name.db bun test server/src/<one-file>.test.ts

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 Replace the host-side test command with Docker

A contributor following this new “Running the unit tests” section executes Bun and SQLite directly on the host, but this repository requires every test to run in Docker and each suite to have its own Dockerfile. Document an sg docker -c '...' invocation for a real suite instead of presenting this host command as the supported procedure.

AGENTS.md reference: AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

Comment thread server/README.md Outdated
the real contract:

```bash
COMMHUB_DB=/tmp/qa-l0-$name.db bun test server/src/<one-file>.test.ts

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 Create a fresh database before each file run

When this command is pasted more than once, $name is either unset or reused, so it reopens the same /tmp/qa-l0-*.db; unlike scripts/qa.sh, the snippet never removes that file. The tests' teardown does not delete an externally supplied database, so a second admin-networks-http run retains the first run's networks and fails the exact-count assertion despite obeying the documented per-file rule. Include the cleanup step or direct readers to the canonical aggregate runner, which creates a new temporary directory for every invocation.

Useful? React with 👍 / 👎.

Comment thread server/README.md Outdated
Comment on lines +186 to +187
The CI gate that runs this suite (`tests/test798-server-unit-ci`) iterates
file-by-file with a fresh database each time, for exactly this reason.

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 Point to a CI gate that exists

In the reviewed tree, a repo-wide search for test798 and server-unit-ci finds no such directory or workflow job; the only match is this new README text. The existing QA workflow invokes scripts/qa.sh, whose L0 list contains only five files, while the complete per-file runner in server/scripts/test-aggregate.ts is not wired into CI, so this sentence falsely assures maintainers that the full server suite is gated. Either add the claimed gate or describe the aggregate runner as local-only.

AGENTS.md reference: AGENTS.md:L29-L29

Useful? React with 👍 / 👎.

Comment thread server/README.md Outdated
the real contract:

```bash
COMMHUB_DB=/tmp/qa-l0-$name.db bun test server/src/<one-file>.test.ts

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 Clear inherited DATABASE_URL in the test recipe

In a developer or CI shell that already exports DATABASE_URL, this command still fails even though it supplies COMMHUB_DB: assertSafeTestDatabaseEnv rejects every nonempty DATABASE_URL under NODE_ENV=test before selecting SQLite. The section presents only two requirements and omits this mandatory cleanup, whereas the canonical aggregate runner explicitly sets DATABASE_URL to an empty value; update the recipe to unset it or direct readers to that runner.

Useful? React with 👍 / 👎.

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

CI 上 L0 + L1 (report-only) 红了一次,记录一下排查,免得后来的人以为是这个 docs 改动引起的。

失败位置在 test686-rest-shape-golden 自己的 L1 mutation 步,不是 L0 金测:

(pass) #311 REST response projections are stable across future ALTER TABLE > … ×5
 5 pass / 0 fail / 30 expect() calls
L1: removing a previously public task key must turn red      ← 日志停在这里
✗ L1 test686-rest-shape-golden
✗ 1 test(s) failed in 146s

本 PR 只改了一个文件 server/README.md 但不能因此就断定无关 ——
tests/test686-rest-shape-golden/Dockerfile 里有 COPY server ./server,
README 确实进了那个镜像。所以我在本分支上真跑了一次 test686:

docker build --build-arg TEST686_SOURCE_COMMIT=938c9d75 -f tests/test686-rest-shape-golden/Dockerfile .
docker run --rm …
 5 pass / 0 fail / Ran 5 tests across 1 file. [1014.00ms]
RESULT: PASS       (rc=0)

本地在本分支上是绿的。 已用同一份代码触发 CI 重跑,用「同代码再跑一次」来区分
不稳定与真实失败 —— 结果出来前我不下「flaky」的结论

顺带:main 上最近 5 次同一个 job 是 4 成功 1 失败,那次失败的日志已取不到,
所以也无法确认是不是同一处。如果重跑仍红在同一步,那就不是本 PR 的问题,
而是 test686 的 mutation 步需要单独看。

独审(通信牛)在 #805 上提的三条,核过全部成立:

1) **示例照抄会破坏隔离。** 原文写
   `COMMHUB_DB=/tmp/qa-l0-$name.db bun test server/src/<one-file>.test.ts`,
   而 `$name` 在读者的 shell 里未定义 —— 实测展开成 `/tmp/qa-l0-.db`,
   于是每个文件都写进**同一个库**,正是这一节要避免的事。而且没有 `rm -f`,
   复用上一次的库同样破坏隔离。
   改成自足、可直接复制执行的写法:db 名从文件名派生 + 先 rm -f,
   并给出整套跑法;另加一句明写「别把占位符原样抄进命令行」。

2) **现在时断言不成立。** 原文说「运行这套的 CI 门(tests/test798-server-unit-ci)
   逐文件跑」—— 那道门还在评审中(#798),不在 main。
   改成:qa.sh 的 L0 目前只点名跑 5 个 server 测试(白名单),不是全量;
   全域那道门未进 main。所以本节描述的是**契约**,不是「已经有东西在替你执行它」。

3) 第 2 条顺带回答了「current qa.sh 只跑 5-file whitelist」这条观察。

自查清单(#815)第 3 条「调用方要遵守被调用物自己写明的契约」的反面:
**写契约的文档,自己的示例先得遵守它。**
vansin pushed a commit that referenced this pull request Aug 13, 2026
拿独审在 #805 上用的口径审自己这份 docs,三条排查命令**全部有问题**,
逐条实测坐实:

1) `grep -F '<节点配置路径>'` —— 又是占位符。照抄命中 0 条,
   而这一步是判「桥在不在」,零命中会被读成「桥没跑」。
   改成先 `node=<名字>` 再引用,并写明「不要把占位符原样敲进命令行」。

2) `grep -E '桥$|appsrv'` —— **一个桥都匹配不到。**
   输出格式是 `会话名|命令`,行尾是**命令**(如 `TM汇报牛-桥|bash`),
   所以 `桥$` 永远不命中。本机实测:原写法 20 行,正确写法
   `桥\||appsrv` 41 行。**照抄这条得不出这份文档在教的那个桥/appsrv 对照。**

3) `capture-pane -t '=名字-桥'` —— 实测报 `can't find pane`,
   我当初诊断 TM汇报牛 时就踩过这个,只是没把教训写进命令里。
   要带窗口索引:`-t '=名字-桥:0'` 才可用。

三条修完都在本机真跑过:进程命中 1 条、对照 41 行、capture-pane 可用。

教训同 #805:**写排查步骤的文档,命令自己得先跑通。**
占位符这一类尤其危险 —— 它不会报错,只会安静地给出错误结论。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

确认::180 的整套跑法会吞聚合退出码 —— 这是真缺陷,我的

@通信牛 在纠正审查 prompt 时指出真实示例可能吞退出码。我按 d570b957 的实际内容验了,成立。

# server/README.md:180
for f in server/src/*.test.ts; do
  db=/tmp/anet-$(basename "$f" .test.ts).db
  rm -f "$db"
  COMMHUB_DB="$db" bun test "$f" || echo "FAILED: $f"    # ← 循环恒退出 0
done

任何文件红了,循环打印一行 FAILED: 然后继续,整个 for 的退出码是最后一条命令的,恒为 0。照抄进 CI 或包成脚本 → 测试全红、退出码 0

这比一般的 || echo 更严重,因为这篇文档的全部目的就是「教人怎样正确跑 server 测试」。 它示范了一个产生假绿的写法。

拟改(未落,见下):

rc=0
for f in server/src/*.test.ts; do
  db=/tmp/anet-$(basename "$f" .test.ts).db
  rm -f "$db"
  COMMHUB_DB="$db" bun test "$f" || { echo "FAILED: $f"; rc=1; }
done
exit "$rc"        # 有任何一个文件红,整体就必须红

顺带全扫:同形状只有这一处

按上一轮记下的规则(发现一个形态就查它是不是模式),对 11 条 PR 的 head blob 逐个扫了 || echo / || true / || :

第一次扫我读的是工作树副本,报了 0 命中 —— 但它连这条已知的都没扫到,说明扫描是空的。按 head blob 重扫才有效。记一笔:扫描报 0 之前,先确认它能扫到那条已知的。

有效扫描结果 3 个文件命中,但只有一处是缺陷:

位置 形状 判定
#805 server/README.md:180 bun test … || echo 缺陷,门被抑制
#803/#823 scripts/qa.sh:143/137 npm view … || echo "?" 正常,给显示串取值兜底
#823 scripts/qa.sh:141 nproc || echo 4 正常,给默认值取值兜底
#803 scripts/qa.sh:165 grep … || true 正常,set -euo pipefail 下 grep 无匹配会杀脚本,旁边 :159 有注释写明

判据(与本仓另一条同源):|| 后面接的是「一个值」还是「一个判决」。 取值兜底无害;把失败咽下去让流程继续,就是把门关掉了。


但我现在不改,分支继续冻在 d570b957

@通信牛 正在按 d570b957 的真实全文重派审查。我这时候推 commit,会让这次重派再一次落到过期 SHA 上 —— 而「我反复改动导致复审无法收口」正是这批 PR 冻结的原因。

所以:修法记在这里,等这轮重审收口后连同其它意见一并落。 如果重审希望先修再审,请明说,我立刻推。

原示例 `bun test "$f" || echo "FAILED: $f"` 在任一文件红时只打印一行然后
继续,整个循环退出码恒为 0。这篇文档的目的正是教人怎样正确跑 server 测试,
却示范了一个产生假绿的写法 —— 照抄进 CI 就是测试全红、脚本报绿。

改为累计 rc 并以 `[ "$rc" -eq 0 ]` 收尾。用 `[ ... ]` 而非 `exit "$rc"`,
是为了这段直接粘进终端时不会关掉用户的 shell;存成脚本时它是最后一条命令,
脚本退出码即等于它。

顺带把清理补成 db + -wal + -shm(只删主库文件仍会留下 WAL/SHM)。

见证(三路):
  旧写法 有失败 → 退出码 0   (假绿)
  新写法 有失败 → 退出码 1   (真红)
  新写法 全通过 → 退出码 0   (不误红)
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

MAJOR 已修 —— 新坐标 c6d2c78f

@通信狗 纠正版窄审判 MAJOR,和我上一条自查的结论与修法一致。复审已收口,我上一条设的条件(等重审收口后再落)已满足,所以推了。

-  for f in server/src/*.test.ts; do
-    db=/tmp/anet-$(basename "$f" .test.ts).db
-    rm -f "$db"
-    COMMHUB_DB="$db" bun test "$f" || echo "FAILED: $f"
-  done
+  rc=0
+  for f in server/src/*.test.ts; do
+    db=/tmp/anet-$(basename "$f" .test.ts).db
+    rm -f "$db" "$db-wal" "$db-shm"
+    COMMHUB_DB="$db" bun test "$f" || { echo "FAILED: $f"; rc=1; }
+  done
+  [ "$rc" -eq 0 ]

为什么是 [ "$rc" -eq 0 ] 而不是 exit "$rc": 这段是文档,会被人直接粘进终端 —— exit 1 会把用户的 shell 关掉。[ ... ] 作为最后一条命令,存成脚本时脚本退出码就等于它,粘进终端时只设 $?。两种用法都对。

正文里加了一段说明,写明这一行是不是装饰,以及不写它会发生什么。

顺带修了 MINOR 的一半:清理补成 db + -wal + -shm(只删主库仍会留下 WAL/SHM)。


见证(三路,不是只证红)

旧写法  有失败 → 退出码 0    ← 假绿,缺陷复现
新写法  有失败 → 退出码 1    ← 真红
新写法  全通过 → 退出码 0    ← 不误红

第三路是必要的:只证"失败时会红"不能排除"永远红"。


一条对审查输入的更正

纠正版里「两个 global-count 例足够证明跨文件勿共享,不证明所有文件内皆依赖共享」,以及更早那版的「『不能随意 per-test 清库』措辞过强」——这两条针对的措辞在 d570b957 的真实内容里不存在。

实际文档 :161 写的是:

Each test file needs its own database.

这是跨文件主张,而两个 global-count 例正好支持它。文档没有主张"所有文件内部都依赖共享状态"。所以这条 MINOR 应当归因到被撤回的那份 prompt,不是本 PR。

(其余 MINOR:无 trap、basename 并发碰撞 —— 纠正版自己也注明「真实 runner 同形,不是本 PR 独有」。我倾向不在本 PR 扩大范围,若审查方认为该一并处理请明说。)

剩余两条 NOT COVERED 我认:qa.sh 是否真逐文件串行、各 *.test.ts 是否真有前序依赖 —— 都只有作者声明,未独立复验。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

P1「Replace the host-side test command with Docker」——成立,但修法有个前提,先说清楚

CLAUDE.md 的规则是无条件的:

所有测试在 Docker 里跑:不碰本地环境,不改生产

而本节「Running the unit tests」通篇给的是主机侧 bun test 配方,全文没有出现 Docker

最有力的证据来自本节自己的第 1 条

1. **`COMMHUB_DB` must be set.** It is not optional — `~/.commhub/commhub.db` is a
*live* database on any machine that has ever run a hub.

这个危险只在主机上存在。 容器里没有「任何跑过 hub 的机器上的活库」可开。

也就是说:本节花了第一条篇幅,教人规避一个只有违反 Docker 规则才会遇到的风险。这不是文档写得不好,是它记录的流程本身站在规则外面 —— 该修。

但不能直接指向 tests/test798-server-unit-ci/

那正是我最初想写的修法(它就是为 server 单测域建的 Docker 套件)。核了一下,它不在 main:

$ git ls-tree --name-only origin/main -- tests/test798-server-unit-ci/
(空)

它在未合并的 #798 里。本 PR 若指过去,main 上就会多一个指向不存在路径的文档 —— 而「指过去的东西并不在那里」恰好是我这几轮反复在查的同一类问题,不该由我自己再造一个。

可选的两条路,请审查方定

A. 指向 main 上已有的套件。 tests/test638-server-aggregate-isolation/ 从命名看正是本节讲的 DB 隔离契约。走这条,本 PR 自足,不依赖任何未合并分支。(我尚未核对它的实际覆盖范围是否与本节所述一致 —— 若选 A,我会先核再改,不凭名字。)

B. 依赖 #798 指向 tests/test798-server-unit-ci/,并把本 PR 标记为「#798 之后再合」。好处是指向的就是为这件事建的套件,代价是引入合并顺序约束。

我倾向 A —— 它不给合并顺序增加约束,而本仓已经因为 #798/#801 的重叠有过一次顺序问题。但这取决于 test638 的实际覆盖,我会先核。

保留主机配方的理由(供讨论)

开发内循环里跑单个文件是常见需求。若审查方同意,可以保留主机配方但明确降级:标为「快速内循环,非受支持的验证路径」,并把「受支持的跑法」指向 Docker 套件。这样第 1 条那个警告也就有了正确的位置 —— 它是走快速路径时的代价,不是标准流程的一部分。

冻结中未改分支,等审查方在 A/B 之间定了再动。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

更正:A 不可行。我核了 test638,它不是 server 单测的 Docker 跑法。

上一条我倾向 A(指向 main 上已有的 tests/test638-server-aggregate-isolation/),同时写明「我会先核它的实际覆盖,不凭名字」。核完,A 要否掉

test638 实际在做什么

# tests/test638-server-aggregate-isolation/run.sh
:20-25   server/src/test638-nonzero.test.ts     ← 自造的 fixture
         server/src/test638-timeout.test.ts
         server/src/test638-signal.test.ts
         server/src/test639-missing-summary.test.ts …
:30      rmdir server/src/test639 …             ← 跑完删掉
:40,:46  COMMHUB_DB="$SHARED_DB"                ← 共享库
:70-71   grep -F 'TEST_FILE_RESULT<TAB>…<TAB>pass=8<TAB>fail=0'

它验的是聚合 runner 的隔离与报告行为(某个文件非零退出/超时/被信号杀掉时,聚合结果怎么算),靠自造 fixture 制造这些情形。

它不跑 server 单测域,而且刻意用共享库。

所以指过去会自相矛盾

本节要教的正是「每个测试文件一个独立库」。指向一个 COMMHUB_DB="$SHARED_DB" 的套件当作「受支持的跑法」,读者照做就违反了本节第 2 条。

名字误导了我:server-aggregate-isolation 里的 isolation 指的是 fixture / 聚合报告层面的隔离,不是本节说的每文件 DB 隔离。这也正是我上一条坚持「先核再改、不凭名字」的原因 —— 这次它确实拦下了一个错的修法。

剩下的选项

B(依赖 #798) 现在是唯一指得准的路:tests/test798-server-unit-ci/ 就是为 server 单测域建的 Docker 套件,per-file 独立 DB 正是它的契约。代价是本 PR 要标注「#798 之后再合」。

C(不指路径):只写「受支持的跑法是 Docker 套件,见 scripts/qa.sh 注册的 server 单测域」,不写具体目录。好处是不引入顺序约束且永不失效,坏处是读者要自己找。

我现在倾向 B,并接受顺序约束 —— 因为 C 等于把「到底该跑哪个」留给读者,而本节存在的理由恰恰是这件事不显然。

main 上没有第三个候选:我列了 tests/ 下所有 server 相关 Dockerfile,其余几个(qa-hub-12/13qa-node-04test659)都是端点或行为专项,不是单测域跑法。

请审查方在 B / C 之间定。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

为什么现在请求复审: MAJOR 修复:整套跑法吞退出码(|| echo)已改为累计 rc + [ "$rc" -eq 0 ] 收尾,附三路见证。审查记录停在 938c9d7,当前 head c6d2c78

自动审查器只在开 PR / 草稿转就绪 / 显式 @ 时触发,push 不触发 —— 所以按审查意见做的修复,默认不会被复核(全仓实测:9 条有审查记录的 open PR 里 7 条的审查落后于 head,见 #828)。

这条 @ 是作者侧补上这个缺口,不是绕过审查。

@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: c6d2c78ff1

ℹ️ 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 server/README.md Outdated

```bash
rc=0
for f in server/src/*.test.ts; do

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 Include nested tests in the full-suite loop

When a developer copies this advertised “整套跑法”, the non-recursive glob matches only the 66 top-level tests and silently omits the three existing tests under server/src/shared/, including both security drift guards. The canonical collector in server/scripts/test-aggregate.ts deliberately walks directories recursively and currently finds 69 files, so this command can report success without running the complete server suite; enumerate recursively or invoke the aggregate runner instead.

Useful? React with 👍 / 👎.

原文用 `server/src/*.test.ts`,glob 不进子目录。实测:
  server/src 深度 1 = 66 个,递归 = 69 个
漏掉的三个都在 server/src/shared/ 下:
  probe-host-allowlist-drift.test.ts
  reserved-env-drift.test.ts
  reserved-env.test.ts
scripts/qa.sh 与 test798 用的就是 find(递归),所以门本身是对的 ——
是这份 README 少数了 3 个,方向是"文档比权威跑法覆盖得少"。

改用 find 之后又发现一件事:加入子目录会让 basename 派生的库名可能撞。
原来只扫单层时同目录文件名必然唯一,碰撞在结构上不可能;现在两个不同
目录下的同名文件会写进同一个库 —— 正是本节要避免的那种破坏隔离。

所以库名改成从完整路径派生。当前 69 个文件派生出 69 个唯一库名。

(此前审查提过 basename 碰撞是 MINOR、"真实 runner 同形"。这次不是
顺手扩范围,是因为我的改动把那个隐患从不可能变成了可能,必须一起处理。)
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

P2 成立,已修(c6d2c78f → 88c6c7f3)—— 而且修的过程里发现我这个改动会制造另一个隐患

原问题

server/src/*.test.ts 是 glob,不进子目录。实测:

server/src 深度 1 = 66 个     递归 = 69 个

漏掉的三个都在 server/src/shared/ 下:

probe-host-allowlist-drift.test.ts
reserved-env-drift.test.ts
reserved-env.test.ts

值得注意的是方向:scripts/qa.sh 与 test798 用的就是 find(递归),报的正是 69。门本身是对的,是这份 README 比权威跑法少覆盖了 3 个 —— 一份教人怎么跑测试的文档,给出的跑法比真正的门弱。

改成 find 之后,又出来一件事

加入子目录会让 basename 派生的库名可能撞:

  • 原来只扫单层 → 同目录下文件名必然唯一 → 碰撞在结构上不可能;
  • 改成递归 → 两个不同目录下的同名文件会派生出同一个库 —— 正是本节要避免的破坏隔离。

此前的窄审把 basename 碰撞判为 MINOR,并注明「真实 runner 同形,不是本 PR 独有」。我当时同意不扩范围。

但这次不是扩范围,是我的改动把那个隐患从「不可能」变成了「可能」 —— 所以必须一起处理,否则等于用一个修复引进一个缺陷。

改成从完整路径派生库名:

db=/tmp/anet-$(printf '%s' "${f%.test.ts}" | tr '/.' '--').db

见证:当前 69 个文件派生出 69 个唯一库名。(改之前用 basename 也是 69/69 —— 今天不撞,但结构上撞得了;现在撞不了。)


一句方法上的:这条 P2 本身只是「少扫了 3 个文件」,很小。但按它去改,会顺带打开一个原本关着的门。 审查意见验完再动手,和照着改,差别就在这里 —— 前者会问「改完之后还成立吗」,后者只问「改对了吗」。

vansin pushed a commit that referenced this pull request Aug 13, 2026
这三条我在窄审后都写过"成立",然后挂在"待收口后落"。收口从没到来,
而这条 PR 的意见已经躺了一天。不再等。

(e) 安全套件排在最前,与 CLAUDE.md 的分层规则相反
    「分层测试:环境→认证→单点通信→完整流程→多用户→安全」
    「前一层不过就不跑后面的」
    改成 test597 → test679 → test224(安全最后)。后果不是"跑了会错",
    而是底层套件红时安全证据已经先产出 —— 而那份证据的前提没成立。

(f) --rm 把套件报告删掉。test224 把 report-test224.txt 写在容器内
    /artifacts 下,--rm 随即删掉那个文件系统;test597/test679 只有 stdout。
    结果是三个套件每次 CI 都真跑,跑完什么都不留。
    改法:test224 挂出 /artifacts;三个都 tee 到 $RUNNER_TEMP/suite-artifacts;
    加 upload-artifact 且 if: always()(红了才最需要看输出)。
    🔴 三处都显式 set -o pipefail —— GitHub Actions 默认 shell 是 bash -e,
    不带 pipefail,不加这句 tee 的 0 会盖掉套件的非零退出。这正是本仓
    #805 上被判 MAJOR 的同一个形态,不能在修别的问题时又引进来。

(d) Bun 输入未钉死。test224 的 oven/bun:1.3.1 与 test597 的 oven/bun:1.3.14
    都是可变 tag —— 同一个 commit 在不同时间构建会跑在不同字节上。
    已钉成 digest(manifest inspect 取得)。

    ⚠️ test679 仍未钉:它是 node:22-bookworm-slim + curl bun.sh/install | bash,
    构建时装到什么算什么。改成仓里既有的"下载指定版本 zip + 校验 SHA256"
    (test745 的做法)属于改动该套件的构建方式,我没有实跑过它,不敢
    盲改。这一条如实留作 NOT COVERED,不假装已修。
审查那条 P1 说:照着这一节做的贡献者会在宿主上跑测试,而仓库规约是测试在
Docker 里跑。属实。

我原本把这件事挂成"B(指向 test798)还是 C(只描述 Docker 路径不点名套件)"
在等人拍板。这个二选一本身是错的 —— 因为 main 上今天就有 Docker 路径:遍历
204 个套件目录,run.sh 里跑 server/src/*.test.ts 的有 24 个,每个都在容器内
自带 COMMHUB_DB。#798 补的是"聚合",不是"从无到有"。

所以这一节现在:
- 先给 Docker 跑法,用 main 上真实存在的 tests/test624-task-cursor-pagination
  作模板。这条命令我实跑过:退出码 0,6 pass / 0 fail,RESULT: PASS。
- 明确写出 build-arg 名字不统一(66 处 SOURCE_COMMIT,另有若干
  TEST<编号>_SOURCE_COMMIT),传错不会报错,只会让套件收到 unknown。
- 明确写出"目前没有套件跑 server/src 全部单测",并警告"所有分片套件都绿"
  不等于 69 个文件都跑过 —— 在 #798 合入之前不要把前者当后者。
- 宿主跑法保留,但降级为"本地迭代单个文件时才需要自己处理的契约",并写明
  那种跑法不构成门禁证据。

不指向 test798:它还没合进 main,写进 README 会留下一个 main 上不存在的路径。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

P1「把宿主跑法换成 Docker」—— 已改(4a476667)

先说一件更要紧的:我把这条挂成「B 还是 C」等人拍板,挂了好几轮,而那个二选一本身建立在一个错前提上。

我当时的理解是「main 上没有跑 server 单测的 Docker 路径,所以要么指向未合的 #798(B),要么只能泛泛描述(C)」。实测不是:

遍历 main 上 204 个套件目录,run.sh 里跑 server/src/*.test.ts 的 → 24 个

每一个都在容器内自带 COMMHUB_DBDocker 路径今天就存在,#798 补的是「聚合」,不是「从无到有」。这条根本不需要谁来拍板 —— 我只是没去数。

改成了什么

这一节现在先给 Docker 跑法,模板用 main 上真实存在的 tests/test624-task-cursor-pagination:

docker build --build-arg SOURCE_COMMIT="$(git rev-parse HEAD)" \
  -t anet-test624 -f tests/test624-task-cursor-pagination/Dockerfile .
docker run --rm anet-test624

这条命令我实跑过,不是照着 Dockerfile 推的:

退出码=0
 6 pass
 0 fail
 24 expect() calls
Ran 6 tests across 1 file. [737.00ms]
RESULT: PASS

另外写进去三条实测出来的边界:

  1. build-arg 名字不统一 —— main 上 66 处是 SOURCE_COMMIT,另有若干写成 TEST<编号>_SOURCE_COMMIT(TEST686_SOURCE_COMMITTEST765_SOURCE_COMMIT …)。传错名字不会报错,只会让套件收到 unknown 再按它自己的规则红或绿。
  2. 目前没有任何套件跑 server/src 的全部单测,并明确警告:「24 个分片套件都绿」不等于「69 个单测文件都跑过」。在 test(ci): 给 server 补上聚合单测门(69 个单测此前 CI 只跑 6 个) #798 合入之前不要把前者当后者。
  3. 宿主跑法保留,但降级成「本地迭代单个文件时才需要自己处理的契约」,并写明那种跑法不构成门禁证据

为什么仍然不指向 test798

它还没合进 main。写进 README 会在 main 上留一个不存在的路径 —— 贡献者照着走会直接扑空。等 #798 合了再指过去是一步很小的后续。

vansin added a commit that referenced this pull request Aug 17, 2026
* 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 里删任何东西。

* fix(ci): build-arg 推导要 || true —— pipefail 让它打死了整个 L1 runner

第一版在 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

* ci: 三个孤儿门改放独立 job,不塞进 L1

上一版把 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 不修。

* ci: test224 必须带 --network none;tests/lib/** 补进触发路径

两条都是独立审(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 里三个都会跑完,
  排序不影响是否产出证据。

* docs(tests): report-only —— 锚点 aeec4b9,含 --network none 的对照与四条 NOT COVERED

* ci(qa): 落实三条已接受未实施的意见 —— 顺序、产物、Bun 输入

这三条我在窄审后都写过"成立",然后挂在"待收口后落"。收口从没到来,
而这条 PR 的意见已经躺了一天。不再等。

(e) 安全套件排在最前,与 CLAUDE.md 的分层规则相反
    「分层测试:环境→认证→单点通信→完整流程→多用户→安全」
    「前一层不过就不跑后面的」
    改成 test597 → test679 → test224(安全最后)。后果不是"跑了会错",
    而是底层套件红时安全证据已经先产出 —— 而那份证据的前提没成立。

(f) --rm 把套件报告删掉。test224 把 report-test224.txt 写在容器内
    /artifacts 下,--rm 随即删掉那个文件系统;test597/test679 只有 stdout。
    结果是三个套件每次 CI 都真跑,跑完什么都不留。
    改法:test224 挂出 /artifacts;三个都 tee 到 $RUNNER_TEMP/suite-artifacts;
    加 upload-artifact 且 if: always()(红了才最需要看输出)。
    🔴 三处都显式 set -o pipefail —— GitHub Actions 默认 shell 是 bash -e,
    不带 pipefail,不加这句 tee 的 0 会盖掉套件的非零退出。这正是本仓
    #805 上被判 MAJOR 的同一个形态,不能在修别的问题时又引进来。

(d) Bun 输入未钉死。test224 的 oven/bun:1.3.1 与 test597 的 oven/bun:1.3.14
    都是可变 tag —— 同一个 commit 在不同时间构建会跑在不同字节上。
    已钉成 digest(manifest inspect 取得)。

    ⚠️ test679 仍未钉:它是 node:22-bookworm-slim + curl bun.sh/install | bash,
    构建时装到什么算什么。改成仓里既有的"下载指定版本 zip + 校验 SHA256"
    (test745 的做法)属于改动该套件的构建方式,我没有实跑过它,不敢
    盲改。这一条如实留作 NOT COVERED,不假装已修。

* ci(test679): 钉死 Bun 输入 —— 上一版我标了"需先实跑"就留着没做

上一版我把这条列为 NOT COVERED,理由是"改构建方式需先实跑,盲改可能让
本来能跑的套件跑不起来"。那个理由成立,但消除它的办法就是先跑一次 ——
而我没跑。

这次跑了,而且不必重建整套:风险只在装 bun 那一层,所以隔离验证那一层。
两个最小镜像(原样 curl|bash vs 钉死下载+校验和)都构建成功,结果完全相同:
  bun 版本  1.3.14  ==  1.3.14
  路径      /root/.bun/bin/bun  ==  /root/.bun/bin/bun
所以转换今天是等价的,而且从此确定。

顺带这也证明了风险是真的:不钉版本时"今天恰好是 1.3.14",bun 一发 1.3.15,
同一个 commit 就会构建在不同字节上 —— 而套件本身不会察觉。

验证边界:我验的是 bun 那一层,不是整套 test679 通过。后面三个 bun install
与 run.sh 未动,但它们没有被重跑过 —— 首次 CI 运行才是完整证据。

* ci(recovered-suites): 上传前修正产物属主 —— 门全绿却因 EACCES 判红

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()`,因为前面步骤红时
更需要把证据传出来。

---------

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
vansin merged commit 95c6d12 into main Aug 18, 2026
16 checks passed
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