docs(server): 写明单测的两条 DB 契约(第二条此前只存在于 qa.sh 里) - #805
Conversation
今天给 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),那道门就是按逐文件独立库跑的。
|
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.
Narrow acceptable fix: make the example executable (e.g. an explicit file slug plus pre-run removal, or a complete loop), describe current No author-branch edit or production action performed. |
There was a problem hiding this comment.
💡 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".
| the real contract: | ||
|
|
||
| ```bash | ||
| COMMHUB_DB=/tmp/qa-l0-$name.db bun test server/src/<one-file>.test.ts |
There was a problem hiding this comment.
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 👍 / 👎.
| the real contract: | ||
|
|
||
| ```bash | ||
| COMMHUB_DB=/tmp/qa-l0-$name.db bun test server/src/<one-file>.test.ts |
There was a problem hiding this comment.
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 👍 / 👎.
| 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. |
There was a problem hiding this comment.
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 👍 / 👎.
| the real contract: | ||
|
|
||
| ```bash | ||
| COMMHUB_DB=/tmp/qa-l0-$name.db bun test server/src/<one-file>.test.ts |
There was a problem hiding this comment.
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 👍 / 👎.
|
CI 上 失败位置在 本 PR 只改了一个文件 本地在本分支上是绿的。 已用同一份代码触发 CI 重跑,用「同代码再跑一次」来区分 顺带:main 上最近 5 次同一个 job 是 4 成功 1 失败,那次失败的日志已取不到, |
独审(通信牛)在 #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 条「调用方要遵守被调用物自己写明的契约」的反面: **写契约的文档,自己的示例先得遵守它。**
拿独审在 #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:**写排查步骤的文档,命令自己得先跑通。** 占位符这一类尤其危险 —— 它不会报错,只会安静地给出错误结论。
确认:
|
| 位置 | 形状 | 判定 |
|---|---|---|
#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 (不误红)
MAJOR 已修 —— 新坐标
|
P1「Replace the host-side test command with Docker」——成立,但修法有个前提,先说清楚
而本节「Running the unit tests」通篇给的是主机侧 最有力的证据来自本节自己的第 1 条这个危险只在主机上存在。 容器里没有「任何跑过 hub 的机器上的活库」可开。 也就是说:本节花了第一条篇幅,教人规避一个只有违反 Docker 规则才会遇到的风险。这不是文档写得不好,是它记录的流程本身站在规则外面 —— 该修。 但不能直接指向
|
更正:A 不可行。我核了
|
There was a problem hiding this comment.
💡 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".
|
|
||
| ```bash | ||
| rc=0 | ||
| for f in server/src/*.test.ts; do |
There was a problem hiding this comment.
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 同形"。这次不是 顺手扩范围,是因为我的改动把那个隐患从不可能变成了可能,必须一起处理。)
P2 成立,已修(
|
这三条我在窄审后都写过"成立",然后挂在"待收口后落"。收口从没到来,
而这条 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 上不存在的路径。
P1「把宿主跑法换成 Docker」—— 已改(
|
* 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>
docs(server): 写明单测的两条 DB 契约,第二条此前只存在于 qa.sh 里
今天给 server 补聚合单测门时踩了这个坑:第一版用一个共享 COMMHUB_DB 跑
bun test src/,红了 4 条,看起来像产品坏了。逐条查下来一条产品缺陷都没有 ——
是我违反了一条从没写在任何文档里的契约。
两条契约,第二条不明显:
COMMHUB_DB 必须设。这条已经是 fail-closed 的:NODE_ENV=test
(bun test 会设,已实测)且 COMMHUB_DB 未设时,db-adapter 拒绝并报
"REFUSING to open the default SQLite database"。默认值 ~/.commhub/commhub.db
在任何跑过 hub 的机器上都是活库,这道守卫不是形式主义。
每个测试文件要各自一个库。这条没有任何东西强制 —— 设一个共享的
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),那道门就是按逐文件独立库跑的。