test(webworker-runtime): keep the corpus gate as a Node import sweep

This commit is contained in:
imccyu
2026-08-26 12:35:05 +08:00
parent 6d9cc6ab96
commit 8793cd477b
3 changed files with 175 additions and 10 deletions
@@ -0,0 +1,139 @@
/**
* Full-corpus import gate: every built bundle in the workspace —
* `packages/<group>/<package>/lib/index.js` and `vendor/<package>/lib/index.js`
* — must be importable by Node's ESM loader. A bundle that stops importing (a
* stray `.css` import, an emitted module Node cannot parse, a dependency that
* throws at module scope) is reported by name.
*
* Baseline exemptions are a pinned list, not a count: an unlisted import
* failure is a real finding (a bundle that stopped being importable), and it
* must not hide inside a total. A listed file that becomes importable also
* fails, so the list cannot rot.
*
* Cost: this walks the whole build output and imports every bundle serially in
* one process, so it takes minutes on loaded runners and needs
* `pnpm run build:lib:host` to have run. It is a heavyweight suite, not part
* of a default aggregator run.
*
* Run: tsx tests/compile/transform-corpus-check.ts [files...]
* With no arguments it discovers the corpus itself.
*/
import { readdirSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const repositoryRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
/**
* Files Node's ESM loader cannot import in this repository. None is a finding:
* each is listed with the reason the import fails, and the run refuses a
* listed file that imports cleanly so the list stays current in both
* directions. The koffi entry depends on corpus order: sandbox-windows-acl
* imports the win32-process package earlier in the serial sweep (a distinct
* module instance under its node_modules URL), so win32-process's own file-URL
* import re-registers koffi's type names and fails as the second load.
*/
const BASELINE_EXEMPT: ReadonlyMap<string, string> = new Map([
['packages/client/ui-primitives/lib/index.js', 'imports .css, which bare Node cannot load'],
['packages/client/web/lib/index.js', 'imports .css, which bare Node cannot load'],
['packages/subprocess/win32-process/lib/index.js', 'koffi type-name collision on a second load'],
['packages/test-support/client-runtime/lib/index.js', "needs vitest's internal state"],
])
let failures = 0
const report: string[] = []
const log = (line: string): void => {
report.push(line)
process.stdout.write(`${line}\n`)
}
const fail = (line: string): void => {
failures += 1
log(line)
}
/** @returns Built bundles under a two-level package directory, in stable order. */
function discover(): string[] {
const found: string[] = []
/** @returns Sorted subdirectory names, or none when the path is not a readable directory. */
const subdirectories = (path: string): string[] => {
try {
return readdirSync(path, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name)
.sort()
} catch {
return []
}
}
for (const group of ['packages', 'vendor']) {
const groupDirectory = join(repositoryRoot, group)
for (const entry of subdirectories(groupDirectory)) {
// `packages/<group>/<package>/lib/index.js`, `vendor/<package>/lib/index.js`.
const candidates = group === 'vendor'
? [join(groupDirectory, entry, 'lib', 'index.js')]
: subdirectories(join(groupDirectory, entry))
.map(child => join(groupDirectory, entry, child, 'lib', 'index.js'))
for (const candidate of candidates) {
try {
if (statSync(candidate).isFile()) found.push(candidate)
} catch {
// No bundle for this package: it may not build a runtime artifact.
}
}
}
}
return found
}
/**
* @returns Path relative to the repository root, for stable diagnostics.
* Always POSIX-separated: the exemption table keys on one form, and a win32
* walk would otherwise miss every entry.
*/
const relative = (path: string): string => path.slice(repositoryRoot.length).replaceAll('\\', '/')
const files = process.argv.slice(2).length > 0
? process.argv.slice(2).map(path => (path.startsWith('/') ? path : join(process.cwd(), path)))
: discover()
if (files.length === 0) {
process.stdout.write('transform-corpus-check: no built bundles found; run `pnpm run build:lib:host` first\n')
process.exitCode = 1
} else {
const verdicts = { ok: 0, exempt: 0, unexpectedBaseline: 0 }
for (const file of files) {
const key = relative(file)
const exemption = BASELINE_EXEMPT.get(key)
try {
await import(pathToFileURL(file).href)
} catch (reason) {
if (exemption === undefined) {
// A bundle that stopped being importable is a real finding, so it
// fails rather than joining a tolerated total.
fail(`- UNEXPECTED BASELINE FAILURE ${key}: ${(reason as Error).message.split('\n')[0]}`)
verdicts.unexpectedBaseline += 1
} else {
verdicts.exempt += 1
}
continue
}
if (exemption !== undefined) {
// The exemption list must stay honest in the other direction too: a file
// that became importable should leave the list.
fail(`- STALE EXEMPTION ${key}: imports fine now (${exemption}); remove it from BASELINE_EXEMPT`)
continue
}
verdicts.ok += 1
}
log('')
log(`files=${String(files.length)} ok=${String(verdicts.ok)} baselineExempt=${String(verdicts.exempt)} `
+ `unexpectedBaselineFailure=${String(verdicts.unexpectedBaseline)}`)
process.stdout.write(failures === 0
? `\ntransform-corpus-check: ${String(verdicts.ok)} bundles import under Node, `
+ `${String(verdicts.exempt)} exempt\n`
: `\ntransform-corpus-check: ${String(failures)} finding(s)\n`)
process.exitCode = failures === 0 ? 0 : 1
}
@@ -0,0 +1,32 @@
/**
* Runs the full-corpus import gate (`transform-corpus-check.ts`) in the
* launcher it is written for, and reports its findings as this suite's failure.
*
* Spawned rather than imported, because the gate's oracle is NODE's ESM loader:
* whether a built bundle imports is judged by `await import(file)` there.
* Vitest replaces that loader with vite's module runner, which imports files
* Node cannot — a `.css` import resolves, and koffi loads a second time — so an
* in-process run measures a different loader and reports the pinned baseline
* exemptions as stale. A gate whose verdict depends on how it was launched is
* not a gate.
*
* The corpus is the build output, so this skips on a tree that has none.
*/
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { expect, test } from 'vitest'
const runner = fileURLToPath(new URL('./transform-corpus-check.ts', import.meta.url))
test('every built bundle imports under Node', (context) => {
const finished = spawnSync(process.execPath, ['--import', 'tsx/esm', runner], { encoding: 'utf8' })
const output = `${finished.stdout}${finished.stderr}`
if (output.includes('no built bundles found')) {
context.skip('the workspace has no build output to sweep')
return
}
// The runner prefixes every finding with '- ', so a failure reads as the
// findings themselves rather than as a diff of its whole report.
expect(output.split('\n').filter(line => line.startsWith('- ')).join('\n')).toBe('')
expect(finished.status, output).toBe(0)
}, 900_000)
+4 -10
View File
@@ -36,7 +36,10 @@ export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [
},
// The webworker-runtime package is outside the coverage requirement by
// decision: vitest.config.ts threshold-excludes its src, so every suite
// runs uninstrumented.
// runs uninstrumented. This tree includes the full-corpus import gate, a
// single 900s-budget case that spawns a child sweep over every built
// bundle; inside an instrumented partition it exceeds the Windows
// partition budget under load.
{
filter: 'packages/experimental/webworker-runtime/tests/',
exclude: 'packages/experimental/webworker-runtime/tests/**',
@@ -46,15 +49,6 @@ export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [
{ filter: 'scripts/oxlint-contract.spec.ts', exclude: 'scripts/oxlint-contract.spec.ts' },
{ filter: 'scripts/change-scope.spec.ts', exclude: 'scripts/change-scope.spec.ts' },
{ filter: 'scripts/translation-pairing-merge.spec.ts', exclude: 'scripts/translation-pairing-merge.spec.ts' },
// Spawns the full-corpus transform gate in a child process (Node's ESM
// loader is its oracle), so no measured file executes in-process; the
// package src is threshold-excluded in vitest.config.ts. A single
// 900s-budget case; running it inside an instrumented partition exceeds
// the Windows partition budget under load.
{
filter: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts',
exclude: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts',
},
// Built-artifact proof. Packer/runtime src is threshold-excluded, and the
// native Windows aggregate makes this uninstrumented gate wait for build so
// the suite never observes a partially emitted workspace closure.