perf(test): faster local test loop and cold-worktree changed runs (#33421)

- test:changed exits before starting Vitest when Git reports no changed files.
- Unrecorded tests beyond 100 in scope use Vitest's static import graph instead
  of all running, so a fresh worktree's first changed run no longer runs the
  whole suite (patched RecordedModulesResolver; CI does not record, unchanged).
- Bun's slow config load was the SQLite import scan statting every unit test;
  filtered runs now classify only the tests their filters can select, and the
  scan walks directories instead of fs.globSync.
- Silence the native config loader notice on every run.
- Zod's test bundle is content-addressed and reused, so a killed run leaks
  nothing; leaked per-run directories older than 6h are swept.
- build:packages skips compiling when package inputs, compiler, and dist are
  unchanged; test scripts no longer nest pnpm invocations.
This commit is contained in:
Neil
2026-10-24 23:15:51 -07:00
committed by GitHub
parent ee38c3047b
commit e4bbbe045f
17 changed files with 630 additions and 67 deletions
+12 -5
View File
@@ -78,14 +78,16 @@ pnpm test src/shared/app-version.test.ts --maxWorkers=2
`test:changed` considers staged, unstaged, and untracked files. `--changed=origin/main`
also includes committed changes since the branch's merge base. Update that ref when
you need a newer baseline. Both accept the same path filters and options as `pnpm test`.
When Git reports no changed files at all, the launcher exits before starting Vitest.
Local runs enable Vitest's [dependency recording](https://vitest.dev/config/experimental#experimental-recorddependencies).
Ordinary test runs populate the cache; later changed-file runs read it instead of
transforming every test's import graph. Records are separate for the Bun and Node
projects. A test without a record always runs, so a cold cache runs everything in
the requested scope. Start with a narrow path to avoid a full cold run. Keep Vite's
local test cache between runs. Use `--experimental.recordDependencies=false` to
return to Vitest's static import graph.
projects and live in each checkout's `node_modules/.vite`. A test without a record
runs when at most 100 such tests are in scope, which records them. Beyond that, as in
a new worktree, unrecorded tests are selected with Vitest's static import graph
instead of running all of them. Keep Vite's local test cache between runs. Use
`--experimental.recordDependencies=false` to use the static import graph for every test.
This is local feedback, not a complete regression check. Vitest records loaded
imports, including executed dynamic imports, but cannot infer arbitrary filesystem
@@ -94,7 +96,12 @@ for changed fixtures, generated files, CLI entry points, and other indirect inpu
After changing imports, run the affected suites before committing those changes so
later edits use current records. Configuration, lockfile, patch, and workspace-package
changes rerun every test within the requested scope. CI does not enable recording;
the existing CI selection policy is unchanged.
the existing CI selection policy is unchanged. The static graph has the same blind spots
and also misses dynamic imports with computed specifiers, so the first changed-file run
in a new worktree is a little less complete than later ones.
`pnpm build:packages` (which `pnpm test` runs first) skips compiling workspace packages
when their sources, build script, and compiler are unchanged and `dist` is intact.
Vitest also supports [`related --run <source-files>`](https://vitest.dev/guide/cli#vitest-related)
and dependency-aware watch mode. `pnpm test --changed` uses Git to provide the source
+74
View File
@@ -0,0 +1,74 @@
diff --git a/dist/chunks/cli-api.De07GUeT.js b/dist/chunks/cli-api.De07GUeT.js
index ff6e63043ff8fa0b821d4b0b382d1fd998d85dbb..a95172c2a451f615f9472a526d7ed8a670256b47 100644
--- a/dist/chunks/cli-api.De07GUeT.js
+++ b/dist/chunks/cli-api.De07GUeT.js
@@ -10250,6 +10250,7 @@ function groupFilters(filters) {
* Finds test specifications that loaded any of the changed files during their last recorded run.
* A specification without a record is always affected.
*/
+const UNRECORDED_RUN_LIMIT = 100;
class RecordedModulesResolver {
vitest;
related;
@@ -10267,13 +10268,30 @@ class RecordedModulesResolver {
projectSpecs.push(spec);
}
const affected = /* @__PURE__ */ new Set();
- for (const [project, projectSpecs] of specsByProject) this.findAffectedInProject(project, projectSpecs).forEach((spec) => affected.add(spec));
+ const unrecorded = [];
+ for (const [project, projectSpecs] of specsByProject) {
+ const result = this.findAffectedInProject(project, projectSpecs);
+ result.affected.forEach((spec) => affected.add(spec));
+ unrecorded.push(...result.unrecorded);
+ }
+ // Orca: running a few unrecorded files records them; many (a fresh checkout) use the static graph instead of running everything.
+ const unrecordedAffected = unrecorded.length > UNRECORDED_RUN_LIMIT ? await new AffectedModulesResolver(this.vitest, this.related).resolve(unrecorded) : unrecorded;
+ unrecordedAffected.forEach((spec) => affected.add(spec));
return specs.filter((spec) => affected.has(spec));
}
findAffectedInProject(project, specs) {
- if (this.dependsOnConfig(project.vite.config.configFileDependencies) || this.dependsOnConfig(getEnvFiles(project))) return specs;
+ if (this.dependsOnConfig(project.vite.config.configFileDependencies) || this.dependsOnConfig(getEnvFiles(project))) return {
+ affected: specs,
+ unrecorded: []
+ };
const dependencies = this.vitest.cache._dependencies.get(project);
- if (!dependencies) return specs;
+ if (!dependencies) return project.config.globalSetup.length ? {
+ affected: specs,
+ unrecorded: []
+ } : {
+ affected: [],
+ unrecorded: specs
+ };
const changed = /* @__PURE__ */ new Set();
for (const file of this.related) {
const index = dependencies.moduleIndex.get(file);
@@ -10281,13 +10299,23 @@ class RecordedModulesResolver {
}
if (project.config.globalSetup.length) {
const globalSetup = dependencies.globalSetup;
- if (!globalSetup || globalSetup.some((index) => changed.has(index))) return specs;
+ if (!globalSetup || globalSetup.some((index) => changed.has(index))) return {
+ affected: specs,
+ unrecorded: []
+ };
}
const root = this.vitest.config.root;
- return specs.filter((spec) => {
+ const affected = [];
+ const unrecorded = [];
+ for (const spec of specs) {
const loaded = dependencies.files.get(getRootRelativePath(root, spec.moduleId));
- return !loaded || loaded.some((index) => changed.has(index));
- });
+ if (!loaded) unrecorded.push(spec);
+ else if (loaded.some((index) => changed.has(index))) affected.push(spec);
+ }
+ return {
+ affected,
+ unrecorded
+ };
}
dependsOnConfig(configDependencies) {
return !!configDependencies?.some((file) => this.related.includes(file));
+122
View File
@@ -0,0 +1,122 @@
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
mkdirSync,
readdirSync,
readFileSync,
realpathSync,
renameSync,
statSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, relative, resolve } from 'node:path'
const root = resolve(import.meta.dirname, '../..')
const PACKAGES = [{ name: '@orca/process-host', dir: 'src/packages/process-host' }]
// Outputs and build scratch space, not inputs.
const NON_INPUTS = new Set(['dist', 'node_modules', '.dist-build.lock'])
const COMPILER_PACKAGES = ['typescript', 'tsx', '@types/node']
function listFiles(directory, base = directory) {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
if (directory === base && (NON_INPUTS.has(entry.name) || entry.name.startsWith('.dist-'))) {
return []
}
const path = join(directory, entry.name)
return entry.isDirectory()
? listFiles(path, base)
: [relative(base, path).replaceAll('\\', '/')]
})
}
/** Sources, manifest, build script, and the exact installed compiler and Node types. */
export function hashPackageInputs(packageDir) {
const hash = createHash('sha256')
for (const file of listFiles(packageDir).sort()) {
hash
.update(`${file}\0`)
.update(readFileSync(join(packageDir, file)))
.update('\0')
}
const require = createRequire(join(packageDir, 'package.json'))
for (const name of COMPILER_PACKAGES) {
const manifest = realpathSync(require.resolve(`${name}/package.json`))
hash.update(`${manifest}\0`).update(readFileSync(manifest)).update('\0')
}
return hash.digest('hex')
}
/** Size and mtime of every output, so a deleted or edited dist file forces a rebuild. */
function describeOutputs(packageDir) {
const dist = join(packageDir, 'dist')
try {
return listFiles(dist)
.sort()
.map((file) => {
const { size, mtimeMs } = statSync(join(dist, file))
return `${file}:${size}:${mtimeMs}`
})
} catch {
return []
}
}
function stampPath(packageDir) {
return join(packageDir, 'node_modules', '.cache', 'orca-dist-stamp.json')
}
export function isPackageBuildCurrent(packageDir, inputs) {
try {
const stamp = JSON.parse(readFileSync(stampPath(packageDir), 'utf8'))
const outputs = describeOutputs(packageDir)
return (
stamp.inputs === inputs &&
outputs.length > 0 &&
JSON.stringify(stamp.outputs) === JSON.stringify(outputs)
)
} catch {
return false
}
}
function writeStamp(packageDir, inputs) {
const path = stampPath(packageDir)
mkdirSync(dirname(path), { recursive: true })
const temporary = `${path}.${process.pid}`
writeFileSync(temporary, JSON.stringify({ inputs, outputs: describeOutputs(packageDir) }))
renameSync(temporary, path)
}
function runPackageBuild(name) {
// Why not @orca/process-host: this builds it, so its dist may not exist yet.
const pnpmEntry = process.env.npm_execpath
const [program, args] = /\.[cm]?js$/.test(pnpmEntry ?? '')
? [process.execPath, [pnpmEntry, '--filter', name, 'build']]
: ['pnpm', ['--filter', name, 'build']]
const result = spawnSync(program, args, {
cwd: root,
stdio: 'inherit',
shell: program === 'pnpm' && process.platform === 'win32'
})
if (result.error) {
throw result.error
}
return result.status ?? 1
}
if (process.argv[1] && realpathSync(process.argv[1]) === import.meta.filename) {
for (const { name, dir } of PACKAGES) {
const packageDir = join(root, dir)
// Hashed before building: an edit during the build leaves a stale stamp, never a skipped build.
const inputs = hashPackageInputs(packageDir)
if (isPackageBuildCurrent(packageDir, inputs)) {
continue
}
const status = runPackageBuild(name)
if (status !== 0) {
process.exit(status)
}
writeStamp(packageDir, inputs)
}
}
+31 -24
View File
@@ -1,5 +1,7 @@
import { resolve } from 'node:path'
import { runProcessSync } from '@orca/process-host'
import { runProcess, runProcessSync } from '@orca/process-host'
import { hasNoChangedFiles } from './vitest-changed-precheck.mjs'
import { RUN_ARGS_ENV } from './vitest-run-filters.mjs'
import { createVitestZodBundle } from './vitest-zod-bundle.mjs'
import {
checkBunVersion,
@@ -9,43 +11,48 @@ import {
} from './pinned-bun-executable.mjs'
const root = resolve(import.meta.dirname, '../..')
const pinnedBun = resolvePinnedBunExecutable(root, runProcessSync)
const bunCheck = pinnedBun
? { ok: true }
: checkBunVersion(probeBunVersion('bun', root, runProcessSync), readPinnedBunVersion(root))
if (bunCheck.message) {
console.error(bunCheck.message)
}
if (!bunCheck.ok) {
process.exitCode = 1
} else {
let zodBundle
const vitestArgs = process.argv.slice(2)
function runVitest() {
const pinnedBun = resolvePinnedBunExecutable(root, runProcessSync)
const bunCheck = pinnedBun
? { ok: true }
: checkBunVersion(probeBunVersion('bun', root, runProcessSync), readPinnedBunVersion(root))
if (bunCheck.message) {
console.error(bunCheck.message)
}
if (!bunCheck.ok) {
return 1
}
try {
zodBundle = createVitestZodBundle(root)
const zodBundle = createVitestZodBundle(root)
const result = runProcessSync({
program: pinnedBun ?? 'bun',
args: ['--bun', resolve(root, 'node_modules/vitest/vitest.mjs'), ...process.argv.slice(2)],
args: ['--bun', resolve(root, 'node_modules/vitest/vitest.mjs'), ...vitestArgs],
cwd: root,
env: {
...process.env,
ORCA_BACKGROUND_LAUNCH: '1',
ORCA_TEST_NODE_EXECUTABLE: process.execPath,
ORCA_TEST_NODE_VERSION: process.versions.node,
ORCA_UNIT_ZOD_BUNDLE: zodBundle.path
ORCA_UNIT_ZOD_BUNDLE: zodBundle.path,
[RUN_ARGS_ENV]: JSON.stringify(vitestArgs),
// The config's TypeScript and CommonJS package scope are intended; the notice is noise.
VITE_CONFIG_NATIVE_IGNORE_WARNING: 'true'
},
stdio: 'inherit',
timeoutMs: null
})
process.exitCode = result.code ?? 1
return result.code ?? 1
} catch (error) {
console.error('Could not prepare or start Vitest:', error)
process.exitCode = 1
} finally {
try {
zodBundle?.cleanup()
} catch (error) {
console.warn('Could not remove the temporary Vitest Zod bundle:', error)
}
return 1
}
}
if (await hasNoChangedFiles(vitestArgs, root, runProcess)) {
// Vitest's own result for this case, without paying its startup.
console.log('No changed files found, exiting with code 0')
} else {
process.exitCode = runVitest()
}
@@ -0,0 +1,46 @@
const WATCH_ARGS = new Set(['-w', '--watch'])
/** The `--changed` ref of a one-shot run: true for the bare flag, undefined when absent or ambiguous. */
export function parseChangedArg(args) {
if (args[0] !== 'run' || args.some((arg) => WATCH_ARGS.has(arg.split('=')[0]))) {
return undefined
}
const index = args.findIndex((arg) => arg === '--changed' || arg.startsWith('--changed='))
if (index === -1) {
return undefined
}
const arg = args[index]
if (arg !== '--changed') {
return arg.slice('--changed='.length) || undefined
}
// Vitest would read a following positional as the ref; leave that form to Vitest.
const next = args[index + 1]
return next === undefined || next.startsWith('-') ? true : undefined
}
/**
* Whether Git reports no changed files for Vitest's `--changed`, which then runs nothing, so the
* caller can skip starting it. Any doubt returns false.
*/
export async function hasNoChangedFiles(args, root, runProcess) {
const changed = parseChangedArg(args)
if (changed === undefined) {
return false
}
const queries = [
// Covers Vitest's staged, modified, and untracked queries. Unlike `ls-files --modified`, it
// refreshes stale index stat data instead of rehashing files on every run.
['status', '--porcelain', '-z', '--untracked-files=all', '--ignore-submodules=none'],
...(typeof changed === 'string' ? [['diff', '--name-only', '-z', `${changed}...HEAD`]] : [])
]
try {
const results = await Promise.all(
queries.map((args) =>
runProcess({ program: 'git', args, cwd: root, timeoutMs: 30_000, maxOutputBytes: 4096 })
)
)
return results.every((result) => result.code === 0 && result.stdout === '')
} catch {
return false
}
}
@@ -0,0 +1,42 @@
import { describe, expect, it, vi } from 'vitest'
import { hasNoChangedFiles } from './vitest-changed-precheck.mjs'
const clean = () => vi.fn(async () => ({ code: 0, stdout: '' }))
describe('hasNoChangedFiles', () => {
it('also checks commits since the ref', async () => {
const runProcess = clean()
await expect(
hasNoChangedFiles(['run', '--changed=origin/main'], '/repo', runProcess)
).resolves.toBe(true)
expect(runProcess.mock.calls.map(([spec]) => spec.args)).toContainEqual([
'diff',
'--name-only',
'-z',
'origin/main...HEAD'
])
})
it('starts Vitest when Git reports a file or cannot answer', async () => {
for (const result of [
{ code: 0, stdout: '?? src/a.ts\0' },
{ code: 128, stdout: '' }
]) {
const runProcess = vi.fn(async () => result)
await expect(hasNoChangedFiles(['run', '--changed'], '/repo', runProcess)).resolves.toBe(
false
)
}
})
it.each([
[['run', 'src/a']],
[['run', '--changed', 'src/a']],
[['run', '--changed', '--watch']],
[['related', '--changed']]
])('leaves %j to Vitest', async (args) => {
const runProcess = clean()
await expect(hasNoChangedFiles(args, '/repo', runProcess)).resolves.toBe(false)
expect(runProcess).not.toHaveBeenCalled()
})
})
+3 -2
View File
@@ -197,6 +197,7 @@ const NODE_RUNTIME_INCLUDE = [
'config/scripts/vitest-node-runtime-pool.test.ts'
]
export function getNodeRuntimeInclude() {
return [...NODE_RUNTIME_INCLUDE, ...findSqliteRuntimeTests()]
/** `filters` limits the SQLite import scan to the tests a filtered CLI run can select. */
export function getNodeRuntimeInclude({ filters } = {}) {
return [...NODE_RUNTIME_INCLUDE, ...findSqliteRuntimeTests({ filters })]
}
+62
View File
@@ -0,0 +1,62 @@
// Options that always consume the next argument. Anything else is treated as a flag, so an
// unlisted option's value becomes an extra filter: a wider scan, never a missed file.
const VALUE_OPTIONS = new Set([
'-c',
'--config',
'--maxWorkers',
'--project',
'--reporter',
'--outputFile',
'--shard',
'--pool',
'--environment',
'-t',
'--testNamePattern',
'--testTimeout',
'--hookTimeout',
'--retry',
'--bail',
'--sequence.seed'
])
// Modes where Vitest can select files beyond the command-line filters.
const UNSCOPED_OPTIONS = new Set(['-w', '--watch', '--dir', '-r', '--root', '--related'])
export const RUN_ARGS_ENV = 'ORCA_VITEST_RUN_ARGS'
/** Positional filters of a one-shot `vitest run`, or undefined when it may select any file. */
export function parseRunFilters(args) {
if (args[0] !== 'run') {
return undefined
}
const filters = []
for (let index = 1; index < args.length; index += 1) {
const arg = args[index]
if (arg === '--') {
return undefined
}
if (!arg.startsWith('-')) {
filters.push(arg)
continue
}
const name = arg.split('=')[0]
if (UNSCOPED_OPTIONS.has(name)) {
return undefined
}
if (!arg.includes('=') && VALUE_OPTIONS.has(name)) {
index += 1
}
}
return filters.length ? filters : undefined
}
/**
* Filters of the Vitest CLI run that run-vitest.mjs launched in this process. The argv check
* keeps a nested Vitest that inherits the environment from using its parent's filters.
*/
export function readRunFilters(env = process.env, argv = process.argv) {
const launched = env[RUN_ARGS_ENV]
if (!launched || launched !== JSON.stringify(argv.slice(2))) {
return undefined
}
return parseRunFilters(argv.slice(2))
}
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import { parseRunFilters, readRunFilters, RUN_ARGS_ENV } from './vitest-run-filters.mjs'
import { matchesRunFilter } from './vitest-sqlite-runtime-scan.mjs'
describe('parseRunFilters', () => {
it('returns the positional filters of a one-shot run', () => {
expect(
parseRunFilters([
'run',
'--config',
'config/vitest.config.ts',
'src/main/runtime',
'--maxWorkers=2',
'-t',
'name',
'src/shared/a.test.ts:12'
])
).toEqual(['src/main/runtime', 'src/shared/a.test.ts:12'])
})
it('treats an unknown option value as a filter, widening rather than narrowing the scan', () => {
expect(parseRunFilters(['run', '--changed', 'src/a', '--unknown', '7'])).toEqual(['src/a', '7'])
})
it.each([
[['run', '--config', 'x']],
[['watch', 'src/a']],
[['related', 'src/a.ts']],
[['run', '--watch', 'src/a']],
[['run', '--dir', 'src', 'a']],
[['run', 'src/a', '--', 'b']]
])('scans every file for %j', (args) => {
expect(parseRunFilters(args)).toBeUndefined()
})
it('ignores inherited arguments that this process was not launched with', () => {
const args = ['run', 'src/a']
const env = { [RUN_ARGS_ENV]: JSON.stringify(args) }
expect(readRunFilters(env, ['bun', 'vitest.mjs', ...args])).toEqual(['src/a'])
expect(readRunFilters(env, ['bun', 'vitest.mjs', 'run', 'src/b'])).toBeUndefined()
})
})
describe('matchesRunFilter', () => {
it('matches every file a Vitest substring or line filter selects', () => {
const file = 'src/shared/worktree/Branch-Name.test.ts'
expect(matchesRunFilter(file, ['src/shared/worktree'])).toBe(true)
expect(matchesRunFilter(file, ['branch-name'])).toBe(true)
expect(matchesRunFilter(file, [`${file}:12`])).toBe(true)
expect(matchesRunFilter(file, [`${process.cwd()}/src/shared/`])).toBe(true)
expect(matchesRunFilter(file, ['src/main'])).toBe(false)
})
})
@@ -7,14 +7,17 @@ it('re-derives SQLite assignments from malformed disposable cache data', async (
const version = createHash('sha256')
.update(readFileSync(new URL('./vitest-sqlite-runtime-scan.mjs', import.meta.url)))
.digest('hex')
for (const files of [null, { 'fixture.test.ts': [1, 2, 'false'] }]) {
for (const files of [null, { 'src/fixture.test.ts': [1, 2, 'false'] }]) {
for (const [source, expected] of [
['export {}', []],
["import './src/main/sqlite/sync-database'", ['fixture.test.ts']]
["import './main/sqlite/sync-database'", ['src/fixture.test.ts']]
] as const) {
vi.doMock('node:fs', async (original) => ({
...(await original<typeof FileSystem>()),
globSync: () => ['fixture.test.ts'],
readdirSync: (path: string) =>
String(path).endsWith('src')
? [{ name: 'fixture.test.ts', isDirectory: () => false }]
: [],
statSync: () => ({ mtimeMs: 1, size: 2 }),
readFileSync: (path: Parameters<typeof readFileSync>[0]) =>
String(path).endsWith('.json')
+63 -9
View File
@@ -1,8 +1,7 @@
import { createHash } from 'node:crypto'
import { globSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { basename, dirname, join, resolve } from 'node:path'
import { defaultExclude } from 'vitest/config'
import { basename, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path'
import { UNIT_INCLUDE } from './ci-unit-files.mjs'
const require = createRequire(import.meta.url)
@@ -88,17 +87,70 @@ function readCache(version) {
}
}
/** Unit tests that import a SQLite module or harness directly, as repo-relative paths. */
export function findSqliteRuntimeTests() {
function walkFiles(directory, files) {
let entries
try {
entries = readdirSync(resolve(root, directory), { withFileTypes: true })
} catch {
return
}
for (const entry of entries) {
const path = `${directory}/${entry.name}`
if (!entry.isDirectory()) {
files.push(path)
} else if (entry.name !== 'node_modules' && entry.name !== '.git') {
// Vitest's defaultExclude, pruned instead of matched per file.
walkFiles(path, files)
}
}
}
/** UNIT_INCLUDE files as repo-relative paths; a plain walk is several times faster than fs.globSync. */
export function listUnitTestFiles() {
const files = []
for (const base of new Set(UNIT_INCLUDE.map((pattern) => pattern.split('/**')[0]))) {
walkFiles(base, files)
}
return files.filter((file) => UNIT_INCLUDE.some((pattern) => posix.matchesGlob(file, pattern)))
}
/**
* Vitest's CLI filter match (a case-insensitive path substring). Also tries each filter without
* a `:line` suffix, so the selection is never narrower than the one Vitest runs.
*/
export function matchesRunFilter(file, filters) {
const testFile = file.toLowerCase()
const absolute = resolve(root, file).replaceAll('\\', '/')
return filters.some((raw) =>
[raw, raw.replace(/(?::\d+)+$/, '')].some((filter) => {
const slashed = filter.replaceAll('\\', '/')
const relativePath = relative(process.cwd(), filter).replaceAll('\\', '/')
return (
(isAbsolute(filter) && absolute.startsWith(slashed)) ||
testFile.includes(slashed.toLowerCase()) ||
testFile.includes(relativePath.toLowerCase())
)
})
)
}
/**
* Unit tests that import a SQLite module or harness directly, as repo-relative paths. With CLI
* `filters`, only the tests those filters can select are classified.
*/
export function findSqliteRuntimeTests({ filters } = {}) {
// Why: reading every test costs about a second; unchanged files reuse their last verdict.
const version = createHash('sha256')
.update(readFileSync(import.meta.filename))
.digest('hex')
const cached = readCache(version)
const scoped = Boolean(filters?.length)
const candidates = listUnitTestFiles()
const files = {}
let changed = false
for (const file of globSync(UNIT_INCLUDE, { cwd: root, exclude: defaultExclude })) {
const path = file.replaceAll('\\', '/')
for (const path of scoped
? candidates.filter((file) => matchesRunFilter(file, filters))
: candidates) {
const { mtimeMs, size } = statSync(resolve(root, path))
const entry = cached[path]
if (
@@ -117,11 +169,13 @@ export function findSqliteRuntimeTests() {
]
changed = true
}
if (changed || Object.keys(cached).length !== Object.keys(files).length) {
if (changed || (!scoped && Object.keys(cached).length !== Object.keys(files).length)) {
try {
mkdirSync(dirname(CACHE_FILE), { recursive: true })
const temporary = `${CACHE_FILE}.${process.pid}`
writeFileSync(temporary, JSON.stringify({ version, files }))
// Only a full scan knows which files were deleted; a scoped one keeps verdicts it skipped.
const merged = scoped ? { ...cached, ...files } : files
writeFileSync(temporary, JSON.stringify({ version, files: merged }))
// Why: parallel runs each rename a whole file, so a reader never sees a partial write.
renameSync(temporary, CACHE_FILE)
} catch {
+74 -13
View File
@@ -1,28 +1,89 @@
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
statSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { buildSync } from 'esbuild'
import { buildSync, version as esbuildVersion } from 'esbuild'
const CACHE_DIRECTORY = 'vitest-zod-cache'
// No run lasts this long, so an older bundle directory is not in use.
const STALE_AGE_MS = 6 * 60 * 60 * 1000
function removeIfStale(path, now) {
try {
if (now - statSync(path).mtimeMs > STALE_AGE_MS) {
rmSync(path, { recursive: true, force: true, maxRetries: 3 })
}
} catch {
// Another launcher may be removing it too.
}
}
/** Per-run directories that older launchers leaked when killed, and superseded cache keys. */
function sweepStaleBundles(temporary, cache, key) {
const now = Date.now()
for (const name of readdirSync(temporary)) {
if (name.startsWith('vitest-zod-') && name !== CACHE_DIRECTORY) {
removeIfStale(join(temporary, name), now)
}
}
for (const name of readdirSync(cache)) {
if (name !== key && !name.startsWith(`.${key}-`)) {
removeIfStale(join(cache, name), now)
}
}
}
/**
* Builds Zod into one ESM module that every Vitest worker aliases. The output is keyed by the
* installed Zod, esbuild, and this builder, so repeat runs reuse it and a killed run leaks nothing.
*/
export function createVitestZodBundle(root) {
const entry = fileURLToPath(import.meta.resolve('zod'))
const manifest = createRequire(entry).resolve('zod/package.json')
const key = createHash('sha256')
.update(JSON.stringify([entry, readFileSync(manifest, 'utf8'), esbuildVersion]))
.update(readFileSync(import.meta.filename))
.digest('hex')
.slice(0, 16)
const temporary = join(root, '.tmp')
mkdirSync(temporary, { recursive: true })
const directory = mkdtempSync(join(temporary, 'vitest-zod-'))
const path = join(directory, 'zod.mjs')
const cleanup = () => rmSync(directory, { recursive: true, force: true, maxRetries: 3 })
const cache = join(temporary, CACHE_DIRECTORY)
const path = join(cache, key, 'zod.mjs')
mkdirSync(cache, { recursive: true })
sweepStaleBundles(temporary, cache, key)
if (existsSync(path)) {
return { path }
}
// Built beside the final directory, then renamed, so readers never see a partial bundle.
const staging = join(cache, `.${key}-${process.pid}-${Date.now()}`)
try {
buildSync({
absWorkingDir: root,
entryPoints: [fileURLToPath(import.meta.resolve('zod'))],
outfile: path,
entryPoints: [entry],
outfile: join(staging, 'zod.mjs'),
bundle: true,
platform: 'node',
format: 'esm',
target: 'node24',
sourcemap: true
})
return { path, cleanup }
} catch (error) {
cleanup()
throw error
try {
renameSync(staging, dirname(path))
} catch (error) {
if (!existsSync(path)) {
throw error
}
}
return { path }
} finally {
rmSync(staging, { recursive: true, force: true, maxRetries: 3 })
}
}
+26
View File
@@ -0,0 +1,26 @@
import { existsSync, mkdirSync, mkdtempSync, rmSync, utimesSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it } from 'vitest'
import { createVitestZodBundle } from './vitest-zod-bundle.mjs'
let root
afterEach(() => rmSync(root, { recursive: true, force: true }))
it('reuses one bundle across runs and removes directories leaked by killed runs', () => {
root = mkdtempSync(join(tmpdir(), 'orca-zod-bundle-'))
const leaked = join(root, '.tmp', 'vitest-zod-leaked')
const active = join(root, '.tmp', 'vitest-zod-active')
mkdirSync(leaked, { recursive: true })
mkdirSync(active)
const hoursAgo = new Date(Date.now() - 7 * 60 * 60 * 1000)
utimesSync(leaked, hoursAgo, hoursAgo)
const first = createVitestZodBundle(root)
const second = createVitestZodBundle(root)
expect(second.path).toBe(first.path)
expect(existsSync(first.path)).toBe(true)
expect(existsSync(leaked)).toBe(false)
expect(existsSync(active)).toBe(true)
})
+4 -1
View File
@@ -7,9 +7,12 @@ 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'
import { nodeRuntimePool } from './scripts/vitest-node-runtime-pool'
import { readRunFilters } from './scripts/vitest-run-filters.mjs'
import { createWorkspaceSourceResolver } from './scripts/workspace-source-exports.mjs'
const nodeRuntimeInclude = process.versions.bun ? getNodeRuntimeInclude() : []
const nodeRuntimeInclude = process.versions.bun
? getNodeRuntimeInclude({ filters: readRunFilters() })
: []
const balancedShards = process.env.ORCA_BALANCE_UNIT_SHARDS === '1'
const measurementFile = 'src/main/foreign-sqlite-readers/foreign-sqlite-reader-event-loop.test.ts'
const paletteMeasurementFile =
+3 -3
View File
@@ -31,8 +31,8 @@
"lint:react-doctor": "oxlint --config config/oxlint-react-doctor.json",
"lint:react-doctor:changed": "node config/scripts/lint-react-doctor-changed.mjs",
"prepare": "husky",
"test": "pnpm run build:packages && node config/scripts/ensure-native-runtime.mjs --runtime=node && node config/scripts/run-vitest.mjs run --config config/vitest.config.ts",
"test:changed": "pnpm test --changed=HEAD",
"test": "node config/scripts/build-workspace-packages.mjs && node config/scripts/ensure-native-runtime.mjs --runtime=node && node config/scripts/run-vitest.mjs run --config config/vitest.config.ts",
"test:changed": "node config/scripts/build-workspace-packages.mjs && node config/scripts/ensure-native-runtime.mjs --runtime=node && node config/scripts/run-vitest.mjs run --config config/vitest.config.ts --changed=HEAD",
"test:node": "pnpm run build:packages && node config/scripts/ensure-native-runtime.mjs --runtime=node && vitest run --config config/vitest.config.ts",
"test:node-server": "node config/scripts/run-node-server-tests.mjs",
"test:skill-sharing:release": "vitest run --config config/vitest.config.ts src/main/skills src/main/runtime/rpc/methods/skills.test.ts src/shared/skill-bundle-install-contract.test.ts src/shared/skill-install-contract.test.ts src/shared/skill-install-failure.test.ts src/shared/skill-package-manifest.test.ts",
@@ -195,7 +195,7 @@
"repro:live-remote-realistic-freeze": "node config/scripts/live-remote-realistic-freeze-repro.mjs",
"audit:anti-slop": "node config/scripts/sync-anti-slop-plugin.mjs && oxlint --config config/oxlint-anti-slop.json src config tests mobile --deny-warnings",
"sync:anti-slop-plugin": "node config/scripts/sync-anti-slop-plugin.mjs",
"build:packages": "pnpm --filter @orca/process-host build",
"build:packages": "node config/scripts/build-workspace-packages.mjs",
"check:process-host-imports": "node config/scripts/check-process-host-imports.mjs",
"check:main-sync-spawn-ratchet": "node config/scripts/check-main-sync-spawn-ratchet.mjs",
"check:compat-boundary-imports": "node config/scripts/check-compat-boundary-imports.mjs"
+8 -7
View File
@@ -187,6 +187,7 @@ patchedDependencies:
node-pty@1.1.0: d3e7df08a1b7c11400cf9db039ece2b7c8a8d0ce4e2f79d81d52c10a67dc0440
oxlint-plugin-anti-slop@0.1.2: c10c871702365a0c2c59e9f1c5fb92df97cae5755d534c0a3d83c6368f6feafe
sprintf-js@1.1.3: 05316a8b01f155fe19a1e0cb4042c81d43d798a76755c407684e1199a18b1e08
vitest@5.0.5: e53d360918437da97f766d322f487aabfbe10d1450ed98543491573b4d500cd2
ws@8.22.0: 9393e04dd9a90b01eb7aa35fcd5b1431b5038ce999fbbd82651136193ba51f14
importers:
@@ -322,7 +323,7 @@ importers:
version: 3.14.13(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(@types/react@19.2.18)(react@19.2.8))(react@19.2.8)
'@testing-library/jest-dom':
specifier: ^6.9.1
version: 6.9.1(vitest@5.0.5(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))
version: 6.9.1(vitest@5.0.5(patch_hash=e53d360918437da97f766d322f487aabfbe10d1450ed98543491573b4d500cd2)(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))
'@testing-library/react':
specifier: ^16.3.3
version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(@types/react@19.2.18)(react@19.2.8))(react@19.2.8)
@@ -619,7 +620,7 @@ importers:
version: 8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)
vitest:
specifier: 5.0.5
version: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
version: 5.0.5(patch_hash=e53d360918437da97f766d322f487aabfbe10d1450ed98543491573b4d500cd2)(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
vscode-oniguruma:
specifier: ^2.0.1
version: 2.0.1
@@ -680,7 +681,7 @@ importers:
version: 7.0.2
vitest:
specifier: 5.0.5
version: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@18.19.130)(happy-dom@21.0.4)(msw@2.14.3(@types/node@18.19.130)(typescript@7.0.2))(vite@8.3.4(@types/node@18.19.130)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
version: 5.0.5(patch_hash=e53d360918437da97f766d322f487aabfbe10d1450ed98543491573b4d500cd2)(@opentelemetry/api@1.9.1)(@types/node@18.19.130)(happy-dom@21.0.4)(msw@2.14.3(@types/node@18.19.130)(typescript@7.0.2))(vite@8.3.4(@types/node@18.19.130)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
packages:
@@ -10366,7 +10367,7 @@ snapshots:
picocolors: 1.1.1
pretty-format: 27.5.1
'@testing-library/jest-dom@6.9.1(vitest@5.0.5(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))':
'@testing-library/jest-dom@6.9.1(vitest@5.0.5(patch_hash=e53d360918437da97f766d322f487aabfbe10d1450ed98543491573b4d500cd2)(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))':
dependencies:
'@adobe/css-tools': 4.5.0
'@types/aria-query': 5.0.4
@@ -10376,7 +10377,7 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
optionalDependencies:
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
vitest: 5.0.5(patch_hash=e53d360918437da97f766d322f487aabfbe10d1450ed98543491573b4d500cd2)(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
'@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(@types/react@19.2.18)(react@19.2.8))(react@19.2.8)':
dependencies:
@@ -15099,7 +15100,7 @@ snapshots:
transitivePeerDependencies:
- '@types/picomatch'
vitest@5.0.5(@opentelemetry/api@1.9.1)(@types/node@18.19.130)(happy-dom@21.0.4)(msw@2.14.3(@types/node@18.19.130)(typescript@7.0.2))(vite@8.3.4(@types/node@18.19.130)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)):
vitest@5.0.5(patch_hash=e53d360918437da97f766d322f487aabfbe10d1450ed98543491573b4d500cd2)(@opentelemetry/api@1.9.1)(@types/node@18.19.130)(happy-dom@21.0.4)(msw@2.14.3(@types/node@18.19.130)(typescript@7.0.2))(vite@8.3.4(@types/node@18.19.130)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)):
dependencies:
'@types/chai': 5.2.3
'@vitest/mocker': 5.0.5(msw@2.14.3(@types/node@18.19.130)(typescript@7.0.2))(vite@8.3.4(@types/node@18.19.130)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
@@ -15124,7 +15125,7 @@ snapshots:
- '@types/picomatch'
- msw
vitest@5.0.5(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)):
vitest@5.0.5(patch_hash=e53d360918437da97f766d322f487aabfbe10d1450ed98543491573b4d500cd2)(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(happy-dom@21.0.4)(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)):
dependencies:
'@types/chai': 5.2.3
'@vitest/mocker': 5.0.5(msw@2.14.3(@types/node@24.19.2)(typescript@7.0.2))(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))
+1
View File
@@ -139,3 +139,4 @@ patchedDependencies:
monaco-editor@0.55.1: config/patches/monaco-editor@0.55.1.patch
sprintf-js@1.1.3: config/patches/sprintf-js@1.1.3.patch
'@tiptap/extension-list@3.31.3': config/patches/@tiptap__extension-list@3.31.3.patch
vitest@5.0.5: config/patches/vitest@5.0.5.patch