PR-21 · 发布产物与 CI 测试命令:两个「门禁全绿但东西是坏的」缺陷 - #184
Open
titanwings wants to merge 5 commits into
Open
titanwings wants to merge 5 commits into
titanwings wants to merge 5 commits into
Conversation
重建说明:本提交由会话转录重建,提交信息取自原分支(ds/21-package-ci)。 文件内容为集成分支上的最终态,不是当时那一刻的中间态——原分支的 per-commit 文件树随 /tmp 清空丢失,转录只保留了提交信息与 git add 的路径清单。 原提交信息:(未记录)
titanwings
force-pushed
the
ds/20-repo-hygiene
branch
from
September 15, 2026 09:36
02f9461 to
0367c5f
Compare
titanwings
force-pushed
the
ds/21-package-ci
branch
from
September 15, 2026 09:36
df98aae to
074e6a2
Compare
这 19 个 PR 是堆叠的,base 是上一条 ds/* 分支,而 CI 的触发名单只有 [dot-skill-test, dot-skill, main],所以本分支的 push / PR 都不会触发工作流, PR 页面永远显示 no checks reported。这里只改触发条件,不动任何产品代码。
CI 从建立起**没有一次是绿的**(30 次运行全部 failure),原因有两个,都不是产品 代码的问题,而是门禁自己坏了: 一、`npm test` 在 Node 20 上根本跑不起来 "test": "node --test \"tests/*.test.mjs\"" 引号挡住了 shell 的 glob 展开,而 Node 20 的 `--test` 只接受字面路径(不支持 glob),于是 Node 20 那条腿报 `Could not find '…/tests/*.test.mjs'` 直接退出; Node 22 支持 glob,所以本机永远是绿的。矩阵里同时有 20 和 22,CI 必红。 更要命的是有一条测试**把这个坏字符串钉死了**: `assert.equal(manifest.scripts.test, 'node --test "tests/*.test.mjs"')`。 断言一条命令的**文本**,不等于断言它能跑 —— 这正是它没拦住的原因。 改成 `node --test tests/*.test.mjs`(不加引号):shell 先展开,Node 20 拿到真实 路径;Node 22 拿到同样的路径也认。(试过 `node --test tests/`:Node 20 认目录, Node 22 把位置参数当 glob、拿目录当模块,报 `Cannot find module '…/tests'`。) 新测试改为断言:命令不是裸 `node --test`、参数里没有引号、**每个参数都能对上 真实存在的文件**。 二、目标审计在 CI 里直接崩,而不是报告 推送行的 `rev-parse --verify --quiet` 包在会抛异常的 `git()` 里:ref 不存在时 git 以 1 退出且无输出,于是这一行 —— 本该报告"这个分支不在任何远端上"的那一行 —— 把整个审计抛崩(CI 日志里 stdout/stderr 全空)。CI 的 checkout 本来就没有 `origin/dot-skill-test` 跟踪 ref,所以每次必崩。 改动:ref 探测用不抛异常的 `gitRef()`;CI 环境里这一行记为**缺口**并说明理由 (CI 的源码本来就来自远端,这条要求在这里无法验证),而不是假装验证过;本地 仍严格,且把"工作树干净"这句标题真正纳入判定(此前标题这么写,检查只看 ahead,脏工作树照样绿)。 三、验收红行不再空着理由 visual-check 缺少 playwright 的提示打在 **stderr**,而这一行只引用 stdout, 于是失败信息是空的。现在缺 playwright 时单独报一行并给出该设的变量。 本地核对:Node 20 与 22 都收集 383 项。 (cherry picked from commit 2c90893)
CI 终于能跑测试之后(前一个提交修好了 npm test),Node 20 与 22 两条腿都稳定
红在同一条上:
not ok 123 - cli: --mode mcp routes to the MCP client and needs a target
本机复现的办法很直接 —— 把 `~/.colleague-skill/` 移走,这条立刻变红。原来
`credentialPaths()` 除了 `DISTILLY_HOME` 还会回落到 `~/.colleague-skill/<file>`,
而 feishu-mcp 的两条 CLI 路由测试调 `runCollectCli(..., { transport })` 时不传
env,于是 `loadCredential` 拿的是 `process.env` —— **我笔记本上那份改名前的旧
凭据**。CI 上没有那个文件,所以必红。同一个文件里"缺凭据要响亮失败"的那条测试
早就为此把 HOME 指到空目录并写了注释,这两条却漏了。
改动:
1. `tests/feishu-mcp.test.mjs` 的 sandbox 现在把凭据写进自己的 home,并带一份
`{ DISTILLY_HOME, HOME }`;两条路由测试显式传它。移走真实旧凭据后 8/8 通过。
2. `src/collect/slack.mjs` 的 `credentialPaths()` 改为 `env.HOME ?? homedir()`,
与 `kit.mjs` 一致:旧路径回落必须跟着被覆盖的 HOME 走。此前它无视 `env.HOME`,
一次"隔离"的运行照样能读到真实 home 的凭据,两个渠道对"刚读的是哪个文件"
也会给出不同答案。
3. `tests/collect.test.mjs` 里两条只会打印 `false !== true` 的断言补上现场信息
(收据、stderr、work 目录清单)—— 这条 CI-only 失败此前从日志里根本
看不出是"没写文件"还是"写到别处去了"。
全量:无真实旧凭据时 383/385(另 2 条是本机未提交/未推送时的审计行)。
(cherry picked from commit 2551c9b)
CI 现在给得出失败现场了,于是这条一看就清楚:
the page was not written; receipt={… "path":"…/knowledge/raw/slack/c0123-p001.json" …}
tree=knowledge/index.json, knowledge/raw/slack/c0123-p001.json
文件就在那里,只是名字是小写。`writeRaw` 会把名字过一遍 `slug()`,所以频道
`C0123` 落盘成 `c0123-p001.json`(收据里的 `channel_id` 仍然是 `C0123`)。断言
写的是 `C0123-p001.json`,在 macOS 上 APFS 大小写不敏感所以"通过",在 Linux
runner 上就红了 —— 又是一条"取决于在哪台机器上跑"的测试。
断言改成真实文件名,并在注释里写明为什么是小写,免得下次又被"修"回去。
(cherry picked from commit a467934)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
本 PR:
ds/21-package-ci→ds/20-repo-hygiene,1 个提交,170 insertions(+)。本 PR 的提交清单
PR-21 · 发布产物与 CI 测试命令:两个「门禁全绿但东西是坏的」缺陷
ds/21-package-ci(worktree/tmp/dot-skill-test-21,已本地合并进dot-skill-test)按
package.json的files真的npm pack了一次,并按 CI 的原话跑了node --testdst-evidence/不入库1. 缺陷 A:npm 发布的产物跑不起来
package.json的files停留在 Python 时代:files条目bin/、SKILL.md、prompts/、references/、INSTALL.md、INSTALL_EN.md、LICENSE、CITATION.cfftools/requirements.txtsrc/bin/distilly.mjs启动就import "../src/cli/args.mjs"assets/scripts/payloadEntries声明不符结果:
npm pack出来的 tarball 只有 35 个文件 / 105.7 kB,解包后而 prepack 门禁对着它说 "Distilly package payload is valid."。原因是
validatePayload(root = packageRoot)校验的是工作树(工作树里当然什么都有),payloadEntries甚至已经把src/assets/scripts列全了 —— 门禁检查的是它自己那份清单,而不是 npm 真正会打包的那份(
package.json的files)。.github/workflows/publish-package.yml会把这个 tarball 原样发出去。修法(三层,缺一层同类问题还会再来)
package.json:files换成bin/ src/ assets/ scripts/ SKILL.md prompts/ references/+ 四份文档;engines从>=18改成>=20(CI 矩阵与契约都是 20/22)。bin/distilly.mjs的validatePayload:在原有"路径都得在"之外,新增两条package.jsonfiles里每个条目都必须存在(挡住tools/、requirements.txt这类陈旧条目)payloadEntries里每个路径都必须被某个files模式覆盖(挡住漏掉src/),目录模式
src/视为覆盖其下全部,通配符按*不跨分隔符、**跨分隔符展开两条都给出可执行的 remedy。门禁从"描述工作树"变成"描述 tarball"。
scripts/check_release.mjs新增第 7 条:真的npm pack→ 解包 → 跑bin/distilly.mjs --version,并断言src/cli/args.mjs、src/commands/index.mjs、assets/distilly-template.html、SKILL.md都在包里。这是原来完全缺失的那一环:其余检查读的都是工作树。
改动前后(同一份缺陷 manifest 喂给新门禁):
--check-package(工作树完好,files缺src/)payload is validError: package.json `files` would omit required paths: src+Remedy: add "src/"--check-package(files含tools/)names paths that do not exist: tools, requirements.txtnpm pack产物--versionexit 1--version→1.0.0check_release.mjstools, requirements.txt2. 缺陷 B:CI 的单元测试步骤是红的
.github/workflows/ci.yml:30跑的是裸node --test,package.json的scripts.test同样是裸node --test。Node 的默认发现规则包含**/*-test.mjs,于是
scripts/blind-test.mjs被当成测试文件收集:以无参数方式 spawn → 按契约打印用法→ exit 2 → 被记成 1 个失败测试。
CI 的
testjob 在 Node 20 与 22 两个 leg 上都是红的。之所以一直没被发现,是因为本地一直用
node --test tests/*.test.mjs(带 glob)验证 —— 那条命令不会收集scripts/,所以 378/378 全绿。我此前汇报的"378/378 通过"用的是带 glob 的那条,不是 CI 实际跑的那条;这个差别本身就是缺陷的一部分。
修法
package.jsonscripts.test→node --test "tests/*.test.mjs"(显式指明套件在tests/)。ci.yml单元测试步骤 →run: npm test,与package.json同一条命令,不会再漂移。scripts/blind-test.mjs增加"被测试运行器收集"时的空操作分支:Node 在它 spawn 的每个文件里设置
NODE_TEST_CONTEXT,配合"且没有子命令参数"这个条件即可精确识别(只看环境变量不行 —— 它会被孙进程继承,
tests/blind-test.test.mjsspawn 的真实调用会被一起静默)。tests/package-payload.test.mjs钉死ci.yml必须run: npm test、scripts.test必须是指明tests/的字符串,且engines与 CI 矩阵一致。npm test(= CI 的命令)node --test(裸跑)node scripts/blind-test.mjs(直接跑)3. 修 A 的过程中发现的缺陷 C:入口守卫在符号链接下静默不执行
bin/distilly.mjs原本没有入口守卫(import它就会跑main()),为了能对validatePayload做单元测试,我加了守卫 —— 第一版写成:新加的第 7 条发布检查立刻失败了:解包出来的 bin 打印的是空字符串。
原因是
import.meta.url永远是 realpath,而argv[1]是调用方写下的路径。npm pack的产物解在/tmp/...下,而 macOS 上/tmp是/private/tmp的符号链接,两者不相等 → 守卫判定"我是被 import 的" → 什么都不做,exit 0。
这不是边角情况:
/tmp、/var都是符号链接binshim(node_modules/.bin/distilly)本身就是符号链接,所以发布出去的CLI 会对每一条命令静默地什么都不做
而且这个写法早就存在于
scripts/blind-test.mjs:780。用未改动的/tmp/dst复现:(同一文件用相对路径或在真实路径目录下跑就正常,所以一直没暴露。)
修法
新增
src/cli/entry.mjs的isEntryPoint(moduleUrl),两侧都取realpathSync;bin/distilly.mjs、scripts/blind-test.mjs、scripts/visual-check.mjs三处守卫统一改用它(
visual-check.mjs用的是pathToFileURL(argv[1]).href === import.meta.url,同样的问题)。tests/entry-point.test.mjs用临时目录里的符号链接实跑三个入口,断言它们真的产生了输出("exit 0 但什么都没做"和"真的做完了"必须能区分开)。
bin/distilly.mjs顺带因此变得可 import(测试能直接import { validatePayload }),这也是
tests/package-payload.test.mjs能精确覆盖四种 manifest 失败模式的前提。4. 测试结果
测试数 378 → 392(新增
tests/package-payload.test.mjs10 条、tests/entry-point.test.mjs4 条)。5. 已知缺口(本 PR 不含,已另外记账)
audit-objective.mjs在--skip-acceptance时把验收那条硬编码为true,推送那条也无条件true。前者是设计(验收在另一个 CI job 里跑),后者是为了让外部冻结不把审计染红 —— 但"17/17 满足;已知缺口 2 条"这样的措辞确实容易被读成
"全都满足了"。属于措辞问题,不是检查能力问题。
.github/PULL_REQUEST_TEMPLATE.md还教人跑python -m unittest discover、.github/ISSUE_TEMPLATE/bug_report.md还在问 Python 版本、scripts/parity.mjs还要python3且默认 rev 在当前树里没有tools/。src/commands/retrospect.mjs把--dir说成"skills 根目录",实际是人物目录;英文帮助里没有
--dir;同时给--person与--dir时--dir静默胜出。file字段是裸文件名(段落锚点是可解析的raw/...路径)。当前无害(
locations.raw才是指针,派生输出引用 0 个 turn 锚点),但消费者按anchor_detail[].file解析会失败。