Explorar el Código

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

imccyu hace 4 semanas
padre
commit
8793cd477b

+ 139 - 0
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/<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
+}

+ 32 - 0
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)

+ 4 - 10
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.