diff --git a/config/scripts/ci-unit-files.mjs b/config/scripts/ci-unit-files.mjs index e1c265dbcd6..4187cfde1a1 100644 --- a/config/scripts/ci-unit-files.mjs +++ b/config/scripts/ci-unit-files.mjs @@ -13,6 +13,14 @@ export const UNIT_INCLUDE = [ 'tests/e2e/**/*.unit.test.ts' ] +export const UNIT_SETUP_FILES = [ + 'config/scripts/vitest-real-agent-home-write-guard.ts', + 'config/scripts/vitest-bun-node-builtins.ts', + 'config/scripts/vitest-dom-setup.ts', + 'config/scripts/vitest-host-ports-setup.ts', + 'config/scripts/vitest-caller-identity-env-setup.ts' +] + export const UNIT_EXCLUDE = [ ...defaultExclude, 'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts', diff --git a/config/scripts/ci-unit-selection.mjs b/config/scripts/ci-unit-selection.mjs index 5a71c5f87fe..2e06f1d3e38 100644 --- a/config/scripts/ci-unit-selection.mjs +++ b/config/scripts/ci-unit-selection.mjs @@ -1,5 +1,19 @@ import { unitConsumers } from './ci-unit-dependency-graph.mjs' import { balanceFiles } from './ci-shard-assignment.mjs' +import { UNIT_SETUP_FILES } from './ci-unit-files.mjs' + +const RUNTIME_ENTRY_POINTS = new Set([ + 'config/vitest.config.ts', + 'config/scripts/run-vitest.mjs', + ...UNIT_SETUP_FILES +]) + +function isUnitRuntimeEntryPoint(file) { + return ( + RUNTIME_ENTRY_POINTS.has(file) || + (/^config\/scripts\/vitest[^/]*\.(?:ts|mjs)$/.test(file) && !file.includes('.test.')) + ) +} export function selectUnitFiles(files, changed, graphInput) { const full = (reason) => ({ files, reason, full: true }) @@ -15,6 +29,9 @@ export function selectUnitFiles(files, changed, graphInput) { } for (const file of changed) { const consumers = unitConsumers([file], graph.reverse) + if ([...consumers].some(isUnitRuntimeEntryPoint)) { + return full('Shared unit runtime input') + } if (!files.some((test) => consumers.has(test))) { return full('No proven test coverage for changed inputs') } diff --git a/config/scripts/ci-unit-selection.test.mjs b/config/scripts/ci-unit-selection.test.mjs index f3764a54c1b..601e3c269b2 100644 --- a/config/scripts/ci-unit-selection.test.mjs +++ b/config/scripts/ci-unit-selection.test.mjs @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { buildUnitDependencyGraph } from './ci-unit-dependency-graph.mjs' +import { UNIT_SETUP_FILES } from './ci-unit-files.mjs' import { FULL_SHARD_COUNT, auditUnitSelection, @@ -26,6 +27,52 @@ const graph = { ...buildUnitDependencyGraph(sources), files: new Set(sources.key const files = [...sources.keys()].filter((file) => file.endsWith('.test.ts')).sort() describe('conservative unit selection', () => { + it.each([ + 'config/vitest.config.ts', + 'config/scripts/run-vitest.mjs', + ...UNIT_SETUP_FILES, + 'config/scripts/vitest-node-runtime-worker.mjs' + ])('retains every test for a transitive dependency of %s', (runtime) => { + const sources = new Map([ + ['src/input.ts', 'export const value = 1'], + ['config/scripts/setup-forward.ts', "export * from '../../src/input'"], + [ + runtime, + `import '${runtime === 'config/vitest.config.ts' ? './scripts' : '.'}/setup-forward'` + ], + ['src/direct.test.ts', "import './input'"], + ['src/unrelated.test.ts', 'export const value = 1'] + ]) + const files = ['src/direct.test.ts', 'src/unrelated.test.ts'] + const graph = { ...buildUnitDependencyGraph(sources), files: new Set(sources.keys()) } + expect(selectUnitFiles(files, ['src/input.ts'], graph)).toMatchObject({ full: true, files }) + expect( + planUnitSelection({ + files, + changed: ['src/input.ts'], + graph, + timings: {}, + mode: 'selected', + event: { pull_request: { draft: true } } + }) + ).toMatchObject({ mode: 'shadow', executionFiles: files, selectionAvailable: false }) + }) + + it('does not mistake a Vitest harness test for a shared runtime entry point', () => { + const sources = new Map([ + ['src/input.ts', 'export const value = 1'], + ['config/scripts/vitest-harness.test.mjs', "import '../../src/input'"], + ['src/direct.test.ts', "import './input'"], + ['src/unrelated.test.ts', 'export const value = 1'] + ]) + expect( + selectUnitFiles(['src/direct.test.ts', 'src/unrelated.test.ts'], ['src/input.ts'], { + ...buildUnitDependencyGraph(sources), + files: new Set(sources.keys()) + }) + ).toMatchObject({ full: false, files: ['src/direct.test.ts'] }) + }) + it('follows re-exports, literal dynamic imports and requires, retaining indirect readers', () => { expect(selectUnitFiles(files, ['src/leaf.ts'], graph).files).toEqual([ 'src/consumer.test.ts', diff --git a/config/vitest.config.ts b/config/vitest.config.ts index aac0a83051f..b080ce434b1 100644 --- a/config/vitest.config.ts +++ b/config/vitest.config.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' import { createRequire } from 'node:module' import { resolve } from 'node:path' import { defaultExclude, defineConfig } from 'vitest/config' -import { UNIT_INCLUDE, UNIT_EXCLUDE } from './scripts/ci-unit-files.mjs' +import { UNIT_INCLUDE, UNIT_EXCLUDE, UNIT_SETUP_FILES } from './scripts/ci-unit-files.mjs' import TimingSequencer from './scripts/ci-unit-sequencer.mjs' import RuntimeSequencer from './scripts/vitest-runtime-sequencer.mjs' import { getNodeRuntimeInclude } from './scripts/vitest-node-runtime-files.mjs' @@ -64,13 +64,7 @@ const testOptions = { server: { deps: { inline: ['zod'] } }, // Node's storage globals and V8 retention checks require the existing child flags. execArgv: ['--no-experimental-webstorage', '--expose-gc'], - setupFiles: [ - resolve('config/scripts/vitest-real-agent-home-write-guard.ts'), - resolve('config/scripts/vitest-bun-node-builtins.ts'), - resolve('config/scripts/vitest-dom-setup.ts'), - resolve('config/scripts/vitest-host-ports-setup.ts'), - resolve('config/scripts/vitest-caller-identity-env-setup.ts') - ], + setupFiles: UNIT_SETUP_FILES.map((file) => resolve(file)), include: UNIT_INCLUDE, exclude: balancedShards ? UNIT_EXCLUDE : defaultExclude, hookTimeout: 60_000,