diff --git a/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts b/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts new file mode 100644 index 0000000000..6c041b93c1 --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts @@ -0,0 +1,139 @@ +/** + * Full-corpus import gate: every built bundle in the workspace — + * `packages///lib/index.js` and `vendor//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 = 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///lib/index.js`, `vendor//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 +} diff --git a/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts b/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts new file mode 100644 index 0000000000..43d5f3dd66 --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts @@ -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) diff --git a/scripts/coverage-exempt.ts b/scripts/coverage-exempt.ts index 38b295512c..e7dc90bb29 100644 --- a/scripts/coverage-exempt.ts +++ b/scripts/coverage-exempt.ts @@ -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.