From 5ba9e50bb0e899d64e2f313c230338c82301e2c8 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:16:50 +0800 Subject: [PATCH] fix(ci): stabilize native Windows coverage --- ...2026-08-08-native-windows-pull-request-ci.i18n.yaml | 4 ++-- .../2026-08-08-native-windows-pull-request-ci.md | 2 +- .../2026-08-08-native-windows-pull-request-ci.zh.md | 2 +- .../2026-08-18-in-job-partitioned-coverage.i18n.yaml | 4 ++-- .../process/2026-08-18-in-job-partitioned-coverage.md | 2 +- .../2026-08-18-in-job-partitioned-coverage.zh.md | 2 +- .../tests/fixtures/process-exit-host.ts | 2 -- .../subprocess-local/tests/process-exit.spec.ts | 4 ---- scripts/run-gates.spec.ts | 10 +++++++++- scripts/run-gates.ts | 10 ++++++++-- 10 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml index 8d3ba3e8ff..cb84ea9e03 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md -2026-08-08-native-windows-pull-request-ci.md: 113193bcc05dae132b045382bea822b4296b9ff0 -2026-08-08-native-windows-pull-request-ci.zh.md: b038f11da5cbf7d5278b8600c9691879c93a5231 +2026-08-08-native-windows-pull-request-ci.md: 7bcdfa7a3e560f247b3041ddf6dd214031c540fc +2026-08-08-native-windows-pull-request-ci.zh.md: 12c853364cbfb191e3bf7c06734559dcdd498ab3 diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md index 113193bcc0..7bcdfa7a3e 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md @@ -18,7 +18,7 @@ Every pull request also starts an ordinary independent `windows-native` job name The native job is deliberately absent from `all-checks-passed.needs` and does not use `continue-on-error`: the aggregate neither waits for it nor changes conclusion because of it, while the job retains its own unmasked result. Workspace build, production-site, and 100%-per-file coverage failures make the native job fail. Static, documentation, package, built-artifact, lint, and snapshot inventories run in the same job as observational gates: their failures remain visible without changing the native aggregate result because Linux owns their blocking verdict. -The 16-core lane admits four concurrent outer gates. Workspace build, production-site validation, instrumented coverage, and exempt-heavy coverage appear first and start together; observational gates enter as those slots become available. [In-job partitioned coverage](2026-08-18-in-job-partitioned-coverage.md) uses eight single-worker shards, while the exempt-heavy gate receives two workers from `DSH_COVERAGE_MAX_WORKERS=6`; together with build and site, the initial outer schedule has about twelve active execution units instead of exceeding twenty. Every Vitest project uses forked workers because Node 24's CJS lexer fatal reproduced in shared worker threads on Windows and POSIX. Both coverage gates set Vitest's default per-test and polling budgets to 30 seconds because unrelated process, Git, SQLite, watcher, grammar, and static-gate fixtures can exceed 15 seconds only under the complete lane's concurrent Windows instrumentation. The script-only translation-pairing merge suite runs in the exempt-heavy gate because it imports only `scripts/` sources and child processes; V8 instrumentation contributes no threshold coverage there but magnifies Git-process latency. Lefthook concurrency fixtures retain their outcomes with 30-second case budgets and a 10-second process-ready probe, while the installer allows five seconds for a preempted lock owner to publish its record after exclusive creation. Directory-picker composition gives its debounced config write an explicit 15-second poll budget; workspace-context composition fixtures use a test-owned signal without an unrelated one-second deadline. These lane-scoped budgets preserve asserted outcomes, while the 120-minute job deadline still bounds a stuck run. The LSP sources and the ACL-sandbox sources remain in the Windows denominator: stub-based failure-path suites carry every in-process ACL-sandbox file to 100%, and only the runner entry stays excluded — it executes exclusively as a spawned child outside the instrumented run, its behavior pinned end-to-end by the runner suite. Narrow annotated V8 ignores cover only unreachable branches (peer-platform arms and lifecycle-unreachable guards), with their behavior tests retained on the owning platform. +The 16-core lane admits four concurrent outer gates. Workspace build, production-site validation, instrumented coverage, and exempt-heavy coverage appear first and start together; every observational gate waits for both coverage gates before entering the available slots, so source-scanning tests cannot race static gates that create temporary contract files. [In-job partitioned coverage](2026-08-18-in-job-partitioned-coverage.md) uses eight single-worker shards, while the exempt-heavy gate receives two workers from `DSH_COVERAGE_MAX_WORKERS=6`; together with build and site, the initial outer schedule has about twelve active execution units instead of exceeding twenty. Every Vitest project uses forked workers because Node 24's CJS lexer fatal reproduced in shared worker threads on Windows and POSIX. Both coverage gates set Vitest's default per-test and polling budgets to 30 seconds because unrelated process, Git, SQLite, watcher, grammar, and static-gate fixtures can exceed 15 seconds only under the complete lane's concurrent Windows instrumentation. The script-only translation-pairing merge suite runs in the exempt-heavy gate because it imports only `scripts/` sources and child processes; V8 instrumentation contributes no threshold coverage there but magnifies Git-process latency. Lefthook concurrency fixtures retain their outcomes with 30-second case budgets and a 10-second process-ready probe, while the installer allows five seconds for a preempted lock owner to publish its record after exclusive creation. Directory-picker composition gives its debounced config write an explicit 15-second poll budget; workspace-context composition fixtures use a test-owned signal without an unrelated one-second deadline. These lane-scoped budgets preserve asserted outcomes, while the 120-minute job deadline still bounds a stuck run. The LSP sources and the ACL-sandbox sources remain in the Windows denominator: stub-based failure-path suites carry every in-process ACL-sandbox file to 100%, and only the runner entry stays excluded — it executes exclusively as a spawned child outside the instrumented run, its behavior pinned end-to-end by the runner suite. Narrow annotated V8 ignores cover only unreachable branches (peer-platform arms and lifecycle-unreachable guards), with their behavior tests retained on the owning platform. The 16-core allocation is the measured capacity point for this inventory. Six-worker coverage trials produced complete passes in 6 minutes 27 seconds and 7 minutes 50 seconds, while exact-head trials with four, three, and two concurrent workers inside one instrumented Vitest process exposed unreliable fixtures and worker exits. Separate single-worker child processes retain process isolation. Sixteen-shard samples reduced instrumented coverage to 112.66–122.01 seconds, but used the whole host before the exempt, build, and site work was counted; eight shards deliberately trade some latency for headroom. A 32-core comparison reduced aggregate gate time by only 1.47 seconds and still triggered the CJS-lexer fatal inside a fork worker, so additional cores did not provide a reliable wall-clock improvement. diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md index b038f11da5..12c853364c 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md @@ -18,7 +18,7 @@ Status: implemented 原生作业被刻意排除在 `all-checks-passed.needs` 之外,且不使用 `continue-on-error`:聚合流程既不等待它,也不会因它改变结论;该作业则保留自身未被掩盖的结果。工作区构建、生产网站和逐文件 100% 覆盖率检查失败会使原生作业失败。静态检查、文档、包、构建产物、lint 与快照清单在同一作业内作为观测性门禁运行;其失败保持可见,但不会改变原生聚合结果,因为这些检查的阻断性判定由 Linux 负责。 -16 核通道最多同时运行 4 道外层门禁。工作区构建、生产网站验证、插桩覆盖率与豁免重型覆盖率排在最前并同时启动,观测性门禁在这些槽位释放后进入调度。[job 内分区覆盖率](2026-08-18-in-job-partitioned-coverage.md)使用 8 个单 worker 分片,豁免重型门禁则从 `DSH_COVERAGE_MAX_WORKERS=6` 获得 2 个 worker;再加上构建与网站,初始外层调度约有 12 个活动执行单元,而不是超过 20 个。每个 Vitest 项目都使用 fork worker,因为 Node 24 的 CJS lexer 致命故障可在 Windows 与 POSIX 的共享 worker 中复现。两项覆盖率门禁都将 Vitest 默认的单测试和轮询时间预算设为 30 秒,因为在完整通道并发的 Windows 插桩下,多个互不相关的进程、Git、SQLite、watcher、语法和静态门禁 fixture(测试前置数据)可能超过 15 秒。translation-pairing 合并套件只导入 `scripts/` 源码和子进程,因此放入豁免重型套件门禁;V8 插桩不会为它贡献任何阈值覆盖率,却会放大 Git 进程延迟。Lefthook 并发 fixture 保留原有结果,采用 30 秒单用例预算与 10 秒进程就绪探测;安装器则允许被抢占的 lock 持有者在独占创建后用 5 秒发布记录。directory-picker 组合为防抖配置写入提供显式的 15 秒轮询预算;workspace-context 组合 fixture 使用测试自有、没有无关 1 秒截止时间的信号。这些只属于该通道的预算保留了原有断言结果,120 分钟的 job 截止时间仍会约束卡死的运行。LSP 源码与 ACL 沙箱源码仍计入 Windows 分母:基于 stub 的失败路径套件把每个进程内 ACL 沙箱文件都带到 100%,只有 runner 入口保持排除——它只作为 spawn 出的子进程在插桩运行之外执行,其行为由 runner 套件端到端钉住。窄范围且带注释的 V8 ignore 只覆盖不可达分支(另一平台专属分支、生命周期内不可达的防御守卫),其行为测试仍保留在所属平台。 +16 核通道最多同时运行 4 道外层门禁。工作区构建、生产网站验证、插桩覆盖率与豁免重型覆盖率排在最前并同时启动;每道观测性门禁都要等待两道覆盖率门禁完成后才进入可用槽位,避免扫描源码的测试与创建临时约定文件的静态门禁发生竞态。[job 内分区覆盖率](2026-08-18-in-job-partitioned-coverage.md)使用 8 个单 worker 分片,豁免重型门禁则从 `DSH_COVERAGE_MAX_WORKERS=6` 获得 2 个 worker;再加上构建与网站,初始外层调度约有 12 个活动执行单元,而不是超过 20 个。每个 Vitest 项目都使用 fork worker,因为 Node 24 的 CJS lexer 致命故障可在 Windows 与 POSIX 的共享 worker 中复现。两项覆盖率门禁都将 Vitest 默认的单测试和轮询时间预算设为 30 秒,因为在完整通道并发的 Windows 插桩下,多个互不相关的进程、Git、SQLite、watcher、语法和静态门禁 fixture(测试前置数据)可能超过 15 秒。translation-pairing 合并套件只导入 `scripts/` 源码和子进程,因此放入豁免重型套件门禁;V8 插桩不会为它贡献任何阈值覆盖率,却会放大 Git 进程延迟。Lefthook 并发 fixture 保留原有结果,采用 30 秒单用例预算与 10 秒进程就绪探测;安装器则允许被抢占的 lock 持有者在独占创建后用 5 秒发布记录。directory-picker 组合为防抖配置写入提供显式的 15 秒轮询预算;workspace-context 组合 fixture 使用测试自有、没有无关 1 秒截止时间的信号。这些只属于该通道的预算保留了原有断言结果,120 分钟的 job 截止时间仍会约束卡死的运行。LSP 源码与 ACL 沙箱源码仍计入 Windows 分母:基于 stub 的失败路径套件把每个进程内 ACL 沙箱文件都带到 100%,只有 runner 入口保持排除——它只作为 spawn 出的子进程在插桩运行之外执行,其行为由 runner 套件端到端钉住。窄范围且带注释的 V8 ignore 只覆盖不可达分支(另一平台专属分支、生命周期内不可达的防御守卫),其行为测试仍保留在所属平台。 16 核配置是这项清单经实测选定的容量规格。使用 6 个 coverage worker 的试验分别以 6 分 27 秒和 7 分 50 秒跑出完整通过结果,而在单个插桩 Vitest 进程内使用 4 个、3 个和 2 个并发 worker 的分支头精确试验暴露出不稳定的 fixture 与 worker 退出。相互独立的单 worker 子进程保留进程隔离。16 分片样本把插桩覆盖率缩短到 112.66–122.01 秒,但还未计入豁免、构建与网站工作就已经占满整台宿主;8 个分片刻意用部分延迟换取余量。32 核对比仅将聚合门禁时间缩短 1.47 秒,且仍在 fork worker 内触发 CJS lexer 致命故障,因此增加核心数没有带来可靠的墙钟时间改善。 diff --git a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml index 417f35afa8..62baafd8ed 100644 --- a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md -2026-08-18-in-job-partitioned-coverage.md: 5cbec688a9967bcb23a2277e11a119c7d278d7ee -2026-08-18-in-job-partitioned-coverage.zh.md: b5d7db566b3883f26ec5528084a05a97b6e97b6a +2026-08-18-in-job-partitioned-coverage.md: d6b8f98095ebb77c6caf88ce67999e683c71f74c +2026-08-18-in-job-partitioned-coverage.zh.md: 36a49b91b2544c611bf77350bda66b06a2708ba6 diff --git a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md index 5cbec688a9..d6b8f98095 100644 --- a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md +++ b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md @@ -18,7 +18,7 @@ When partitioning is enabled, `scripts/run-gates.ts` selects `pnpm run test:cove The coordinator waits for every child, validates that the blob directory contains exactly the expected files, and then runs one `vitest --merge-reports ... --coverage` command. Only that merged command applies the repository's per-file statement, branch, function, and line thresholds, so a partition is never judged against an intentionally partial inventory. -`DSH_COVERAGE_MAX_WORKERS` continues to size the uninstrumented exempt gate and the ordinary non-partitioned path; it does not resize partition children. Native Windows gives the exempt gate two workers and admits four concurrent outer gates: build, production-site validation, instrumented coverage, and exempt-heavy coverage start first, then the observational inventory enters as slots become available. Linux overlaps four instrumented partition processes with two exempt workers, restoring the ordinary path's former four-way instrumented concurrency while keeping every instrumented process single-worker. +`DSH_COVERAGE_MAX_WORKERS` continues to size the uninstrumented exempt gate and the ordinary non-partitioned path; it does not resize partition children. Native Windows gives the exempt gate two workers and admits four concurrent outer gates: build, production-site validation, instrumented coverage, and exempt-heavy coverage start first, and the observational inventory waits for both coverage gates before entering the available slots. Linux overlaps four instrumented partition processes with two exempt workers, restoring the ordinary path's former four-way instrumented concurrency while keeping every instrumented process single-worker. ## Failure and output semantics diff --git a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md index b5d7db566b..36a49b91b2 100644 --- a/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md +++ b/.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md @@ -18,7 +18,7 @@ Status: implemented 协调器等待全部子进程结束,验证 blob 目录只包含预期文件,然后执行一次 `vitest --merge-reports ... --coverage`。只有这条合并命令应用仓库的逐文件语句、分支、函数与行阈值,因此系统不会拿有意不完整的测试清单单独判定任一分区。 -`DSH_COVERAGE_MAX_WORKERS` 继续控制无插桩豁免门禁和普通非分区路径的规模,不会调整分区子进程。原生 Windows 为豁免门禁分配 2 个 worker,并允许 4 道外层门禁并发:构建、生产网站验证、插桩覆盖率与豁免重型覆盖率先启动,观测性清单随后在槽位释放时进入调度。Linux 让 4 个插桩分区进程与 2 个豁免 worker 重叠运行,在保持每个插桩进程只有 1 个 worker 的同时,恢复普通路径原有的 4 路插桩并发。 +`DSH_COVERAGE_MAX_WORKERS` 继续控制无插桩豁免门禁和普通非分区路径的规模,不会调整分区子进程。原生 Windows 为豁免门禁分配 2 个 worker,并允许 4 道外层门禁并发:构建、生产网站验证、插桩覆盖率与豁免重型覆盖率先启动,观测性清单等待两道覆盖率门禁完成后才进入可用槽位。Linux 让 4 个插桩分区进程与 2 个豁免 worker 重叠运行,在保持每个插桩进程只有 1 个 worker 的同时,恢复普通路径原有的 4 路插桩并发。 ## 失败与输出语义 diff --git a/packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts b/packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts index e59289be09..721c5134c3 100644 --- a/packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts +++ b/packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts @@ -13,7 +13,6 @@ if ((kind !== 'ordinary' && kind !== 'terminal') } const treeState = join(root, 'tree.json') -const ready = join(root, 'ready') const proceed = join(root, 'proceed') const managedTree = fileURLToPath(new URL('./managed-tree.ts', import.meta.url)) @@ -58,7 +57,6 @@ const published = JSON.parse(await readFile(treeState, 'utf8')) as { root?: unkn if (!Number.isSafeInteger(published.root) || !Number.isSafeInteger(published.descendant)) { throw new Error('managed tree published invalid process ids') } -await writeFile(ready, 'ready') await waitForFile(proceed) if (trigger === 'dispose') { diff --git a/packages/subprocess/subprocess-local/tests/process-exit.spec.ts b/packages/subprocess/subprocess-local/tests/process-exit.spec.ts index 217338fa1e..cdfc4f4ae0 100644 --- a/packages/subprocess/subprocess-local/tests/process-exit.spec.ts +++ b/packages/subprocess/subprocess-local/tests/process-exit.spec.ts @@ -107,10 +107,6 @@ async function runScenario(kind: ManagedKind, trigger: ExitTrigger) { let treeGone = false try { state = await readTree(join(root, 'tree.json')) - await vi.waitFor(() => readFile(join(root, 'ready'), 'utf8'), { - interval: 10, - timeout: scenarioTimeoutMs, - }) if (process.platform !== 'win32') identities = await captureIdentities(createProcessInspector(), state) await writeFile(join(root, 'proceed'), 'proceed') const outcome = await child diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index 7ca0e78084..5e29bad199 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -110,7 +110,15 @@ describe('gate graph validation', () => { expect(byId.get('coverage')?.allowFailure).not.toBe(true) expect(byId.get('coverage-exempt-heavy')?.allowFailure).not.toBe(true) expect(observational).not.toHaveLength(0) - for (const gate of observational) expect(byId.get(gate.id)?.allowFailure).toBe(true) + for (const gate of observational) { + const completeGate = byId.get(gate.id) + expect(completeGate?.allowFailure).toBe(true) + expect(completeGate?.needs).toEqual(expect.arrayContaining([ + 'coverage', + 'coverage-exempt-heavy', + ...(gate.needs ?? []), + ])) + } }) it('applies one configured test and polling timeout to both coverage gates', () => { diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index b7d8963d8e..92b30fcdb0 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -454,15 +454,21 @@ function ciWindowsBlockingGates(): Gate[] { } function ciWindowsCompleteGates(): Gate[] { + const coverage = coverageGates() + const coverageNeeds = coverage.map(gate => gate.id) const observational = ciWindowsObservationalGates() // The required production site replaces the observational MPA build; both // VitePress modes write the same output directory and cannot overlap. .filter(gate => gate.id !== 'build' && gate.id !== 'docs-site-build') - .map(gate => ({ ...gate, allowFailure: true })) + .map(gate => ({ + ...gate, + allowFailure: true, + needs: [...new Set([...coverageNeeds, ...(gate.needs ?? [])])], + })) return [ pnpmScript('build', 'build'), pnpmScript('windows-site', 'docs:build', { label: 'production site' }), - ...coverageGates(), + ...coverage, ...observational, ] }