Просмотр исходного кода

fix(ci): harden optimized Windows gate fixtures

Tianyi Cui 1 месяц назад
Родитель
Сommit
811788e57a

+ 2 - 2
.agents/notes/implemented/process/2026-07-31-coverage-exempt-heavy-suites.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-07-31-coverage-exempt-heavy-suites.md
-2026-07-31-coverage-exempt-heavy-suites.md: 7bcb078986515b0137f8166e2c6c3b3354ea8d5e
-2026-07-31-coverage-exempt-heavy-suites.zh.md: 580d1e06a089b6e148babfe1eca22de72010c4c0
+2026-07-31-coverage-exempt-heavy-suites.md: b27039e529dad12eb82637a749577f4922c22f03
+2026-07-31-coverage-exempt-heavy-suites.zh.md: 99c707f079c215f196b5378f8e4739dce4076846

+ 3 - 3
.agents/notes/implemented/process/2026-07-31-coverage-exempt-heavy-suites.md

@@ -19,11 +19,11 @@ The `ci-coverage` aggregate splits into two parallel gates; every test still run
 - **Instrumented gate** (`test:coverage`): sets `DSH_COVERAGE_EXEMPT_HEAVY=1`, which makes `vitest.config.ts` drop the exempt suites from both projects' excludes; every remaining file runs instrumented and carries the entire threshold proof. The variable is injected through the gate's own env (the existing `Gate.env` mechanism), not the workflow-global environment, so the uninstrumented gate beside it and any local `vitest run` never see it and behave unchanged.
 - **Uninstrumented gate** (`test:coverage-exempt-heavy`): runs exactly the exempt suites through paired positional filters, keeping the correctness signal whole.
 
-Linux coverage CI and native Windows CI use [in-job partitioned coverage](2026-08-18-in-job-partitioned-coverage.md) inside the instrumented gate. Its merged report carries the same threshold proof; the exempt gate and its membership rules remain unchanged. Linux overlaps the two gates. Native Windows runs the exempt gate after the instrumented merge, while the lightweight observational inventory overlaps the exempt work, so the full-corpus child does not compete with sixteen coverage processes.
+Linux coverage CI and native Windows CI use [in-job partitioned coverage](2026-08-18-in-job-partitioned-coverage.md) inside the instrumented gate. Its merged report carries the same threshold proof; the exempt gate and its membership rules remain unchanged. Linux overlaps four partition children, two exempt workers, and up to eight corpus children, so this combined fan-out is the first check if that lane regresses. Native Windows runs the exempt gate after the instrumented merge, while the lightweight observational inventory overlaps the exempt work, so the full-corpus child does not compete with sixteen coverage processes. The Oxlint contract suite keeps temporary package probes valid under concurrent source checks and hides its script-only probes from glob discovery.
 
 `scripts/coverage-exempt.ts` is the single roster point, holding the membership contract and the filter/exclude pairs so the two sides cannot drift.
 
-`transform-corpus.spec.ts` discovers the complete built-bundle set once, assigns every path to exactly one of eight Node-loader children, and asserts the shard union before launch. The test-support pair and the ACL/win32-process pair retain their original order in one shard because their pinned Vitest-state and Koffi exemptions depend on preceding module state.
+`transform-corpus.spec.ts` discovers the complete built-bundle set once, assigns every path to exactly one of up to eight non-empty Node-loader children, and asserts the shard union before launch. `client-runtime` follows `acp-snapshot` for its pinned Vitest-state exemption, while `win32-process` follows `sandbox-windows-acl` for its pinned Koffi exemption.
 
 ### The roster, reconciled entry by entry
 
@@ -69,7 +69,7 @@ The eight-child corpus run checks the same 239 native Windows bundles with 234 e
 
 - The exempt suites execute without adding instrumentation cost to the thresholded gate; partitioned wall-clock measurements belong to the [in-job partitioning decision](2026-08-18-in-job-partitioned-coverage.md).
 - Native Windows schedules the exempt suites after instrumented coverage and overlaps them with observational checks; Linux retains the parallel coverage split.
-- The corpus suite uses eight child Node loaders but emits one blocking test result; its affinity roster is part of the exemption oracle and must move with affected bundles.
+- The corpus suite uses up to eight non-empty child Node loaders but emits one blocking test result; its affinity roster is part of the exemption oracle and must move with affected bundles.
 - `DSH_GATE_CONCURRENCY` has two schedulable gates in this lane again, so the aggregate scheduler is no longer a pass-through.
 - Adding a heavy suite to the roster requires the membership audit above; a wrong entry fails the instrumented gate loudly rather than eroding coverage silently.
 - The exempt suites no longer appear in the coverage report's file list of contributors; their correctness signal lives solely in the uninstrumented gate's pass/fail.

+ 3 - 3
.agents/notes/implemented/process/2026-07-31-coverage-exempt-heavy-suites.zh.md

@@ -19,11 +19,11 @@ Web Worker 转换语料库在原生 Windows 上暴露了同一类浪费:`trans
 - **插桩 gate**(`test:coverage`):设 `DSH_COVERAGE_EXEMPT_HEAVY=1`,`vitest.config.ts` 据此从两个 project 的 exclude 中剔除豁免套件,其余全部文件照旧插桩并承担全部阈值证明。经 gate 自带 env 注入(既有 `Gate.env` 机制),不进 workflow 全局环境,因此并排的无插桩 gate 和本地直跑 `vitest run` 都看不到该变量、行为不变。
 - **无插桩 gate**(`test:coverage-exempt-heavy`):用配对的 positional filter 恰好运行豁免套件,保证正确性信号不缩水。
 
-Linux 覆盖率 CI 与原生 Windows CI 在插桩门禁内部使用 [job 内分区覆盖率](2026-08-18-in-job-partitioned-coverage.zh.md)。其合并报告承担相同的阈值证明;豁免门禁及其成员资格规则保持不变。Linux 让两道门禁重叠运行。原生 Windows 在插桩报告合并后运行豁免门禁,同时让轻量观测性清单与豁免工作重叠,因此完整语料库子进程不会与十六个覆盖率进程争用资源。
+Linux 覆盖率 CI 与原生 Windows CI 在插桩门禁内部使用 [job 内分区覆盖率](2026-08-18-in-job-partitioned-coverage.zh.md)。其合并报告承担相同的阈值证明;豁免门禁及其成员资格规则保持不变。Linux 会让 4 个分区子进程、2 个豁免 worker 与最多 8 个语料库子进程重叠,因此该通道变慢时应先检查这组并发。原生 Windows 在插桩报告合并后运行豁免门禁,同时让轻量观测性清单与豁免工作重叠,因此完整语料库子进程不会与 16 个覆盖率进程争用资源。Oxlint 约定套件让包内临时探针满足并发源码检查,并把只属于脚本的探针对 glob 发现隐藏。
 
 `scripts/coverage-exempt.ts` 是唯一名单点,集中持有成员资格约定与 filter/exclude 配对,防止两侧漂移。
 
-`transform-corpus.spec.ts` 只发现一次完整的已构建 bundle 集合,把每条路径恰好分配给八个 Node loader 子进程之一,并在启动前断言分片并集。test-support 对与 ACL/win32-process 对在同一分片内保留原始顺序,因为它们固定的 Vitest 状态与 Koffi 豁免依赖前序模块状态。
+`transform-corpus.spec.ts` 只发现一次完整的已构建 bundle 集合,把每条路径恰好分配给最多 8 个非空 Node loader 子进程之一,并在启动前断言分片并集。`client-runtime` 会为固定的 Vitest 状态豁免跟在 `acp-snapshot` 之后,`win32-process` 则会为固定的 Koffi 豁免跟在 `sandbox-windows-acl` 之后。
 
 ### 豁免名单与逐项对账
 
@@ -69,7 +69,7 @@ Web Worker 语料库条目由分区聚合固定:它执行全部 15,250 个测
 
 - 豁免套件在执行时不会向阈值门禁叠加插桩开销;分区墙钟数据由 [job 内分区决策](2026-08-18-in-job-partitioned-coverage.zh.md)负责记录。
 - 原生 Windows 在插桩覆盖率后调度豁免套件,并让它们与观测性检查重叠;Linux 保留并行覆盖率拆分。
-- 语料库套件使用八个 Node loader 子进程,但只产生一个阻断性测试结果;其亲和名单属于豁免判定器,受影响 bundle 移动时必须同步更新。
+- 语料库套件使用最多 8 个非空 Node loader 子进程,但只产生一个阻断性测试结果;其亲和名单属于豁免判定器,受影响 bundle 移动时必须同步更新。
 - `DSH_GATE_CONCURRENCY` 在本 lane 重新拥有两个可调度对象,聚合调度器不再是直通。
 - 向名单新增重型套件必须完成上述成员资格对账;错误条目会让插桩 gate 大声失败,而不是静默侵蚀覆盖率。
 - 豁免套件不再出现在覆盖率报告的贡献文件列表中;其正确性信号完全由无插桩 gate 的红绿承载。

+ 2 - 2
.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: 1fbfe6615ac72e5d9ca4017208cbbdf684f4c43e
-2026-08-08-native-windows-pull-request-ci.zh.md: 7d7c5234807daa8c41647aeafb5c93d16eca7f9b
+2026-08-08-native-windows-pull-request-ci.md: 6a39503f68316e3d9687645248ffa7ed8b453216
+2026-08-08-native-windows-pull-request-ci.zh.md: 07db589a3f0537306c1d2f206504097920411dd5

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md


+ 5 - 2
packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts

@@ -28,8 +28,10 @@ const runner = fileURLToPath(new URL('./transform-corpus-check.ts', import.meta.
 const repositoryRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
 const corpusShards = 8
 const shardAffinity = new Set([
+  // client-runtime needs acp-snapshot to establish Vitest's internal state.
   'packages/test-support/acp-snapshot/lib/index.js',
   'packages/test-support/client-runtime/lib/index.js',
+  // win32-process observes Koffi's duplicate type names after the ACL bundle.
   'packages/sandbox/sandbox-windows-acl/lib/index.js',
   'packages/subprocess/win32-process/lib/index.js',
 ])
@@ -48,14 +50,14 @@ function discoverBuiltBundles(): string[] {
   ].map(path => path.replaceAll('\\', '/')).sort()
 }
 
-/** @returns Every bundle assigned exactly once while preserving Koffi loader affinity. */
+/** @returns Non-empty shards with every bundle assigned once and loader affinity preserved. */
 function partitionBundles(files: readonly string[], count: number): string[][] {
   const partitions = Array.from({ length: count }, () => [] as string[])
   files.forEach((file, index) => {
     const assigned = shardAffinity.has(file) ? 0 : index % count
     partitions[assigned]?.push(file)
   })
-  return partitions
+  return partitions.filter(partition => partition.length > 0)
 }
 
 /** @returns One isolated Node-loader corpus shard. */
@@ -90,6 +92,7 @@ test('partitions every bundle once while retaining loader-state affinity', () =>
   ]
   const shards = partitionBundles(files, corpusShards)
 
+  expect(shards.every(shard => shard.length > 0)).toBe(true)
   expect(shards.flat().sort()).toEqual([...files].sort())
   expect(shards[0]?.filter(file => shardAffinity.has(file))).toEqual(files.filter(file => shardAffinity.has(file)))
 })

+ 13 - 7
scripts/oxlint-contract.spec.ts

@@ -39,6 +39,11 @@ function normalizedOutput(result: ReturnType<typeof runOxlint>): string {
   return `${result.stdout}${result.stderr}`.replaceAll('\\', '/')
 }
 
+/** @returns A transient filename excluded from concurrent repository-wide glob discovery. */
+function hiddenProbeName(prefix: string, suffix: string, extension = '.ts'): string {
+  return `.${prefix}-${suffix}${extension}`
+}
+
 async function writeContractConfig(suffix: string): Promise<string> {
   const path = join(repositoryRoot, `.oxlintrc.contract-${suffix}.json`)
   await writeFile(path, JSON.stringify({ extends: ['./.oxlintrc.json'], ignorePatterns: [] }))
@@ -59,7 +64,8 @@ describe('Oxlint executable contract', () => {
       ['example', 'examples/headless-agent/tests', 'tsconfig.host.json'],
       ['website', 'website', 'tsconfig.host.json'],
     ] as const
-    const source = `export function probePromise(): Promise<void> {
+    const source = `/** Produce a settled promise for type-aware linting. */
+export function probePromise(): Promise<void> {
   return Promise.resolve()
 }
 
@@ -88,7 +94,7 @@ probePromise()
       expect(result.error).toBeUndefined()
       expect(result.status, output).toBe(1)
       for (const [label, path, tsconfig] of paths) {
-        expect(output, label).toContain(`${path.replaceAll('\\', '/')}:5:1: Promises must be awaited`)
+        expect(output, label).toContain(`${path.replaceAll('\\', '/')}:6:1: Promises must be awaited`)
         expect(output, `${label} project`).toContain(
           `Got tsconfig for file ${join(repositoryRoot, path).replaceAll('\\', '/')}: ${join(repositoryRoot, tsconfig).replaceAll('\\', '/')}`,
         )
@@ -110,7 +116,7 @@ probePromise()
   it('runs JavaScript compatibility and nursery rules', async () => {
     const suffix = randomUUID()
     const configPath = await writeContractConfig(suffix)
-    const path = join(repositoryRoot, 'scripts', `oxlint-contract-${suffix}.ts`)
+    const path = join(repositoryRoot, 'scripts', hiddenProbeName('oxlint-contract', suffix))
     const source = `export function firstProbe(): number {
   const first = 1
   const second = 2
@@ -230,7 +236,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
   it('reports an unused suppression', async () => {
     const suffix = randomUUID()
     const configPath = await writeContractConfig(suffix)
-    const path = join(repositoryRoot, 'scripts', `oxlint-contract-${suffix}.ts`)
+    const path = join(repositoryRoot, 'scripts', hiddenProbeName('oxlint-contract', suffix))
 
     try {
       await writeFile(path, '// oxlint-disable-next-line no-console\nexport const value = 1\n')
@@ -280,7 +286,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
     expect(stagedConfig.ignorePatterns).not.toContain('packages/typert/generator/tests/fixtures/type-model/**')
 
     const suffix = randomUUID()
-    const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
+    const path = join(repositoryRoot, 'scripts', hiddenProbeName('staged-lint-probe', suffix))
     try {
       await writeFile(path, 'export const value={answer:1};\n')
       const lint = runOxlint([
@@ -303,7 +309,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
 
   it('preserves successful fix output channels', async () => {
     const suffix = randomUUID()
-    const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
+    const path = join(repositoryRoot, 'scripts', hiddenProbeName('staged-lint-probe', suffix))
 
     try {
       await writeFile(path, '// oxlint-disable-next-line no-console\nexport const value = 1\n')
@@ -327,7 +333,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
 
   it('prints only the final diagnostics when a fix retry still fails', async () => {
     const suffix = randomUUID()
-    const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
+    const path = join(repositoryRoot, 'scripts', hiddenProbeName('staged-lint-probe', suffix))
 
     try {
       await writeFile(path, `export const longProbe = ${'1 + '.repeat(80)}1\n`)

Некоторые файлы не были показаны из-за большого количества измененных файлов