ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Civitai 单体仓库中“main 上 16 个既有测试失败“的排查实录:worktree、submodule 与 .envrc 环境陷阱如何制造假红

Civitai 单体仓库中“main 上 16 个既有测试失败“的排查实录:worktree、submodule 与 .envrc 环境陷阱如何制造假红 Civitai 单体仓库中main 上 16 个既有测试失败的排查实录worktree、submodule 与 .envrc 环境陷阱如何制造假红【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文基于 Civitai 仓库内的分诊报告 claudedocs/red-test-triage-2026-08-10.md 展开。一篇工单声称main分支存在 16 个既有的测试失败并对 5 个测试套件提出删除建议本报告在两个基线提交上做了完整实测证明这 16 个失败在main上从未存在——它们全部来自运行环境git worktree不检出 submodule、gitignore 掉的.envrc丢失、以及共享克隆中并发的git stash。读完后你能掌握一套假红排查方法论如何用三组数字失败文件数 / 失败测试数 / 通过测试数定位静默消失的测试如何用变异测试mutation test证明守护套件不是空转以及如何把一个环境陷阱固化成一条会响的红灯。背景一张16 个失败在 main 上的工单工单声称main上有 16 个 pre-existing test failures并因此建议删除 5 个红了所以已经失效的守护套件。分诊的结论状态记录于 2026-08-21在基线a43e49a4ba与2a2fe66428上验证是16 个失败在main上不存在在它们被报告的那天也不存在。unit项目在两个提交、三个不同 Node 版本下全部为绿packages与apps项目同样是绿的。工单点名的五个套件全部通过且各自收集到非零数量的测试。失败是真实观察到的真问题——但问题出在运行套件的环境而不是测试本身。一个新的git worktree不会检出 submodule也不会携带仓库中 gitignore 掉的.envrc而这两处缺失各自会产生一块独立的假红。这个陷阱自 PR #35672026-08-04起就已以文字形式记录在 CLAUDE.md 中四天后仍被连续三个 agent 踩中。所有实测运行都在从远端 tip 拉出的干净一次性 worktree 中进行配合全新pnpm install --frozen-lockfile并且除特别说明外已初始化event-engine-commonsubmodule。对应仓库中的 submodule 声明见 .gitmodules[submodule event-engine-common] path event-engine-common url https://github.com/civitai/event-engine-common.gitPhase 1实测基线——三个数字不是一个为什么强调三个数字因为本次涉及的问题形态恰好在它们之间移动。基线数据如下原始表格完整保留RunCommitNodeFailed FILESFailed TESTSPassed TESTSSkippedexitunit, submodule缺失a43e49a4ba22.22.2 (flake)73/ 891012,78241unit, submodule 存在a43e49a4ba22.22.2 (flake)0 / 891013,75310unit, submodule 存在a43e49a4ba24.18.1 (.nvmrc/CI)0 / 891013,75311 †unit, submodule 存在a43e49a4ba26.5.0 (ambient)1 / 891713,74611unit, submodule 存在2a2fe66428(2026-08-08)22.22.2 (flake)0 / 889013,71310packagesa43e49a4ba22.22.20 / 73 (2 skipped)01,00240appsa43e49a4ba22.22.20 / 53050700† 0 个失败测试但退出码非零——见下文 Finding 3。一个诚实的边界声明componentbrowser 模式项目没有跑。它需要与仓库 Playwright pin 匹配的 Chromium 版本而当时的宿主机没有。报告明确将其标注为未测量而不是绿——这是排查类文档应有的诚实度。说明报告测量时.nvmrc钉在 24.18.1当前仓库的 .nvmrc 已前进到24.19.0package.json的engines.node约束为24.0.0 25CLAUDE.md 亦声明.nvmrc是 CI 与 Dockerfile 基镜像的权威来源。每个失败文件收集的测试数陷阱的完整形态submodule 缺失的那次运行中73 个失败文件里72 个收集的测试数为零第 73 个prisma-inconsistent-orphan-relations.test.ts收集了 6 个、跳过 3 个并通过一个 unhandled rejection 而非断言失败。这就是陷阱的完整形状72 个文件失败了却没有测试失败。该次运行报告0 个失败测试、12,782 个通过而绿色运行报告 13,753 个通过。也就是说损坏的运行在失败测试计数读作零的情况下静默地抹掉了971 个测试。只对比失败测试数的读者看到的是两边都是 0对比通过测试总数与记忆的读者则看到一个无从归因的下降。73 个失败的共同根因是Cannot find module ../../../event-engine-common/...——git worktree add不会检出 submodule。其中 58 个在错误信息中直接命名了它其余是同一未解析导入级联出来的vi.mock工厂错误。Phase 2分类——五个守护套件是活的且经过变异测试证明非空转工单点名的每一个套件都是绿且非空转的。每个套件都做了变异测试故意破坏它守护的东西然后观察它以自己的特定错误信息变红且所用输入不会被任何前置检查短路。套件类型main上状态结论src/server/services/__tests__/no-wholesale-module-mock.test.ts质量守护ESLint RuleTester97 tests绿保留。非空转——已证明。src/server/middleware/__tests__/block-scope.normalize-endpoint.test.ts路由漂移守护27 tests绿保留。非空转——已证明。src/server/services/blocks/__tests__/app-spend-tier-privilege.test.ts权限面漂移守护9 tests绿保留。非空转——已证明。src/server/services/comics/__tests__/orchestrator-chat.wait-unit.test.tsledger 漂移守护12 tests绿保留。非空转——已证明。scripts/__tests__/typecheck.test.ts包装器行为守护5 tests绿保留。非空转——已证明。这 5 个文件在当前仓库中均可确认存在已逐一核对路径。变异证据如下完整继承原报告表格守护施加的变异结果no-wholesale-module-mock在eslint-local-rules.js中重新引入历史上的文本式检查——把任何源码文本包含importOriginal的工厂视为安全97 →29 失败 / 68 通过AssertionError: Should have 1 error but had 0: []。被杀掉的用例恰好就是洗白用例未使用的importOriginal参数、提及它的注释、作为值出现的裸字符串importOriginal。路由漂移新增src/pages/api/v1/blocks/mutation-probe.ts导出withBlockScope(...)27 →2 失败the allowlist is EXACTLY the static segments of the wrapped routes与pins the current set, so adding a route is a deliberate act两者都在字面量mutation-probe上产生 diff。spend-tier 权限向src/pages/api/v1/developer/block-manifests.ts发布者可触达的模块加入const __probe { spendTier: premium }9 →3 失败含AssertionError: src/pages/api/v1/developer/block-manifests.ts must not reference spendTier in code。wait-unit ledger新增src/server/services/comics/mutation-probe.ts内容为export const probeQuery { wait: 60000 }12 →2 失败no statically-resolvable wait exceeds the seconds envelope与matches the known ledger of numeric wait sites点名server/services/comics/mutation-probe.ts:60000。typecheck 包装器将 scripts/typecheck.mjs 中一行崩溃判定从 stdout 移到 stderr5 →3 失败AssertionError: expected to contain TYPECHECK CRASHED。scripts/typecheck.mjs 的存在本身就说明了这类包装器守护的价值全仓tsc --noEmit需要提高 V8 old-space 上限堆不够时 V8 会在检查进行到一半时中止不产生任何error TS行就死亡——从输出看与一次干净的运行字节级无法区分。该包装器把崩溃大声化显式打出TYPECHECK CRASHED并将其与干净和有类型错误两种状态区分开。typecheck 守护套件正是钉住这行 stdout 输出的存在。所有变异随后全部回滚五个套件合并重跑150 个测试全部通过被跟踪树保持字节级干净。对工单的一处范围更正工单称block-scope.normalize-endpoint.test.ts本应就 #3752 新增的/api/testing/eventloop-stall路由发表意见。它不会这一点值得知道而不是假设。该守护的路径遍历只匹配默认导出被withBlockScope(...)包裹的文件eventloop-stall.ts不是 block-scoped所以守护对其保持沉默是正确的。它对其覆盖的路由是活的——上面的变异证明了——但它是block-scope allowlist 漂移守护不是通用的新路由探测器。通用守护不存在如果需要那是一件新工作不是一次修复。Phase 3真正产生那 16 个失败的机制没有精确复现出 16报告也不作此声称。但以下四项有测量证据Finding 1 — submodule大头如上详述73 个文件失败72 个收集零测试失败测试计数读作 0971 个测试消失。一个独立 agent 在同日同提交上测量基线报告了字节级相同的产物73 个失败文件 / 12,782 通过 / 4 跳过外加 12 个全部来自未检出event-engine-common的pnpm typecheck错误。独立到达、同一产物——但他们的基线并不是main的状态。Finding 2 — 缺失的.envrc.envrc被 gitignore仓库提供了 .envrc.example 作为模板因此一个新 worktree 会静默地拿到系统 Node 而非 flake 钉住的版本。实测在 ambientNode 26.5.0下hiddenBlocks.test.ts在 happy-dom 中因window.localStorage报TypeError: Cannot read properties of undefined (reading clear)失败7个测试在 flake 的 Node 22 与 Node 24 下同一文件通过。脱离 flake 运行还丢失 Prisma engine 环境实测6个PrismaClientInitializationError: could not locate the Query Engine for runtime linux-nixos的 unhandled rejection。CLAUDE.md 早已从上一次遭遇中记录了这个完全相同的组合system Node 26.5.0 against the flakes 22.22.2 produced 7 spuriouswindow.localStorage is undefinedfailures under happy-dom plus 8 Prismalinux-nixosengine errors — every one a false red.7 8 15对照工单报告的 16。这是任何机制给出的最接近工单数字的匹配也是仓库早已写下的机制且两半均可独立复现。Finding 3 — 在 CI 自己的 Node 下unit 套件零失败却非零退出在a43e49a4ba、Node 24.18.1.nvmrc钉住、CI 通过node-version-file使用的版本下运行报告 891/891 文件、13,753 通过、0 失败——然后是 Prisma engine unhandled rejection 导致的Errors 6 errors与非零退出码。CI 的unitjob 带有continue-on-error: true所以今天这是不可见的一旦该 job 按其注释所述翻转为阻塞问题就会开始显现。已标记、未修复——修复是环境形状的且与 PR #3779 重叠。Finding 4 —git stash在环中工单说这组失败在 stashed clean tree 上可复现。git stash是仓库级的refs/stash位于 common git dir被这个克隆的所有 worktree 共享。三个 agent 在共享克隆上并发 stash并不是各自产出一棵干净的树它们是在对一个共享栈做 push 和 pop。这与三个 agent 从三个不同分支报告出完全相同的失败集相吻合是一个值得独立于本工单予以退役的危险。这里以机制陈述而非以测量陈述——未复现。What was changed把一个静默减法变成一条可读的红灯新增一个文件src/tests/submodules-checked-out.test.ts。它将 Finding 1 从静默减法变成一条可读的红色失败行。设计要点均可在源码注释中对照阅读从.gitmodules读取 submodule 路径而非硬编码——后加的 submodule 无需有人记得来更新守护即自动被覆盖正则提取path 行见declaredSubmodulePaths()。断言三个具体入口点event-engine-common/index.ts、feeds/、services/使部分检出——会产生同样消失而不失败症状的情况——也被抓住。不从 submodule 导入任何东西一个因它存在要报告的那个原因而加载失败的守护不是守护。防空转正向控制it.each的集合是推导出来的一个不可解析的.gitmodules会产生零用例并静默通过第一个测试钉住DECLARED.length 0且钉住event-engine-common在列。双向控制运行结果绿态收集 5 个测试5 通过。负向控制空 gitlink 目录4 失败 / 1 通过AssertionError: submodule event-engine-common is present but EMPTY — an uninitialised gitlink. Fix: git submodule update --init --recursive。负向控制目录完全缺失4 失败 / 1 通过AssertionError: submodule event-engine-common is not checked out. …。可达性在两种损坏状态下该文件仍收集到 5 个测试而另外 72 个文件收集数为零。它恰恰在一切安静时可见。零删除没有删除、禁用、跳过或隔离任何既有测试。值得注意的是CLAUDE.md 的 Git Worktrees 一节已经把这个陷阱写成了标准操作worktree 创建后必须git submodule sync --recursive git submodule update --init event-engine-common并写入.envrc新 worktree 无.envrc时会静默拿到系统 Node——但文字不能失败测试可以。这正是prose 无法 fail, this file can的落地形态。给人类的建议——不提议删除任何守护不建议删除任何守护。五个都是活的、具体的、廉价的150 个测试合计约 0.9s。工单因为红了所以失效应删除的前提不成立。三件事值得决策且分诊者没有越俎代庖将工单关闭为不可在main上复现环境产物以 Findings 1–2 为记录。背后的直觉是对的打错了靶子。Finding 3——unit 套件在 CI 自己的 Node 下零失败测试却非零退出。在该 job 翻转为阻塞前需要解决。与 PR #3779 重叠未触碰。Finding 4——30 个 worktree 的共享克隆里的git stash。独立于本工单且是三个 agent 得出同一个错误答案的最可能原因。复现任意一项原报告给出的复现命令完整保留CIVITAI/path/to/civitai WT/tmp/civitai-baseline git -C $CIVITAI fetch origin main git -C $CIVITAI worktree add --detach $WT origin/main git -C $WT submodule update --init --recursive # ← 决定答案的那一步 printf use flake\n $WT/.envrc direnv allow $WT (cd $WT direnv exec . pnpm install --frozen-lockfile direnv exec . pnpm run test:unit:run)其中pnpm run test:unit:run在 package.json 中定义为node scripts/test-unit-run.mjs跑的是 vitest 的unit*项目即src/主应用的单测项目。报告最后的方法论只有一句话读内容而不是读退出码三个数字都要读——失败文件数、失败测试数、通过测试数。失败测试计数为 0 的运行未必是一次通过的运行。小结可迁移的排查清单这篇分诊对任何多 worktree、带 submodule、依赖环境钉住的大型仓库都有参考价值三个数字原则对比测试运行时同时看 failed files / failed tests / passed tests。加载失败的文件不产生失败测试只从通过数里静默消失——单看任一指标都会被骗。环境先于代码submodule 未检出、gitignore 的.envrc丢失、Node 版本漂移26.5.0 vs 22.22.2 产生 7 个 happy-dom 假红外加 Prismalinux-nixosengine 错误都能批量制造main 上的失败。用变异测试给守护正名删除一个红了的守护之前先故意破坏它守护的东西如果它以特定错误信息变红它就是活的。五个套件 150 个测试全部经受住这一检验。把文字警告固化为可执行的守护CLAUDE.md 里已写了四天的警告仍被连续踩中直到 submodules-checked-out.test.ts 让陷阱变成一条自带修复命令的红色断言——且它自身通过正向控制防止空转、通过不依赖被守护对象来保证恰在一切消失时可见。共享克隆上的并发操作是隐蔽污染源refs/stash全克隆共享多 agent 并发 stash 等于对同一个栈操作——三个 agent 报告同一组假失败的最可能解释。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表