Merge pull request #3063 from deepseek-harness/fix/window0825

perf(ci): optimize coverage / snapshot parameter
This commit is contained in:
imccyu
2026-08-26 13:40:33 +08:00
committed by GitHub
14 changed files with 173 additions and 360 deletions
+21 -2
View File
@@ -159,7 +159,7 @@ jobs:
|| 'dsh-ubuntu-24-04-16core' }}
name: node 24 / snapshots and artifacts
env:
DSH_GATE_CONCURRENCY: '8'
DSH_GATE_CONCURRENCY: '10'
DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
DSH_OXLINT_THREADS: '8'
DSH_PUBLINT_CONCURRENCY: '8'
@@ -421,6 +421,13 @@ jobs:
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
# Best-effort: a torn-down job on this self-hosted pool can leave a
# locked @reflink native module under the action's install destination,
# and pnpm/action-setup's self-installer then fails its unlink with
# EPERM. Clearing the destination gives every attempt fresh state.
- name: Clear stale pnpm setup state
shell: pwsh
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
@@ -445,7 +452,7 @@ jobs:
timeout-minutes: 120
env:
DSH_COVERAGE_MAX_WORKERS: '6'
DSH_COVERAGE_PARTITIONS: '4'
DSH_COVERAGE_PARTITIONS: '6'
DSH_COVERAGE_TEST_TIMEOUT_MS: '30000'
DSH_GATE_CONCURRENCY: '3'
steps:
@@ -457,6 +464,10 @@ jobs:
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
# Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy.
- name: Clear stale pnpm setup state
shell: pwsh
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
@@ -491,6 +502,10 @@ jobs:
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
# Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy.
- name: Clear stale pnpm setup state
shell: pwsh
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
@@ -533,6 +548,10 @@ jobs:
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
# Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy.
- name: Clear stale pnpm setup state
shell: pwsh
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
+3 -1
View File
@@ -71,8 +71,10 @@ jobs:
- name: Build
run: pnpm run build:lib:host
# Concurrency here is rehearsal-only: the credentialed publish workflows
# invoke release:pack without the flag and keep the strictly serial path.
- name: Pack release tarballs
run: pnpm run release:pack --family vendor --out dist/npm-vendor
run: pnpm run release:pack --family vendor --out dist/npm-vendor --concurrency 8
- name: Verify packed install
run: pnpm run release:verify-packed-install --family vendor --from dist/npm-vendor
+4 -2
View File
@@ -68,8 +68,10 @@ jobs:
- name: Build
run: pnpm run build:official
# Concurrency here is rehearsal-only: the credentialed publish workflows
# invoke release:pack without the flag and keep the strictly serial path.
- name: Pack release tarballs
run: pnpm run release:pack --family dsh --out dist/npm
run: pnpm run release:pack --family dsh --out dist/npm --concurrency 8
# The harness packages declare the vendored framework as a peer, and this
# verification must not depend on the registry already carrying matching
@@ -77,7 +79,7 @@ jobs:
# publishes — so it installs that family's pack output too. Only dist/npm
# is published.
- name: Pack the vendored framework for verification
run: pnpm run release:pack --family vendor --out dist/npm-vendor
run: pnpm run release:pack --family vendor --out dist/npm-vendor --concurrency 8
# dsh-sandbox-local declares the Landlock entry as a runtime dependency, so
# the verification needs its tarball. Its platform packages stay out: they
@@ -1,45 +1,37 @@
/**
* Full-corpus regression for the worker module transform: every built bundle in
* the workspace is transformed, executed through the real wrapper contract, and
* its export shape compared against what Node's own ESM loader produces for the
* same file.
* 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.
*
* This is the harness that answers "does the transform hold on real output",
* which no hand-written case can: the corpus is whatever the build currently
* emits, so a rolldown upgrade that starts emitting an unseen module form shows
* up here first.
* 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.
*
* Module-syntax statistics are counted from the acorn AST, so the check has no
* separate lexer dependency. Baseline exemptions are a pinned list, not a count:
* four files cannot be imported by Node in this repository for reasons unrelated
* to the transform, and an unexpected member fails the run.
*
* Cost: this walks the whole build output and imports every bundle, so it takes
* tens of seconds and needs `pnpm run build:lib:host` to have run. It is a
* heavyweight suite, not part of a default aggregator run.
* 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, readFileSync, statSync } from 'node:fs'
import { createRequire } from 'node:module'
import { readdirSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { parse } from 'acorn'
import { createAlsRuntime } from '../../src/polyfill/async-context/als-runtime.ts'
import { lowerModuleSource } from '../../src/compile/transform.ts'
import { WRAPPER_PARAMS } from '../../src/image-layout.ts'
const repositoryRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
/**
* Files Node's ESM loader cannot import in this repository, so no baseline
* export shape exists to compare against. None is a transform failure: each is
* checked to still TRANSFORM cleanly, only the comparison is skipped.
*
* Named rather than counted: an unlisted baseline 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.
* 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'],
@@ -48,25 +40,6 @@ const BASELINE_EXEMPT: ReadonlyMap<string, string> = new Map([
['packages/test-support/client-runtime/lib/index.js', "needs vitest's internal state"],
])
/**
* Bundles whose own SOURCE contains the double-lowering sentinels, so the
* transform's guard refuses them by design.
*
* This package is the only such case and the refusal is correct: its bundle
* carries `transform.ts`'s own template literals (`` `__als$${n}` `` from
* `alsTemp`, and the `${ALS}.pause(` fragments), which is exactly the text the
* guard looks for. A self-referential false positive is the right trade: the
* guard exists because a mis-wired image manifest would otherwise show up only
* as "slower", and no roster row transforms this package.
*
* Listed rather than skipped silently, and asserted to keep refusing: if the
* guard stopped tripping here, either the guard or this bundle's contents
* changed, and both are worth knowing about.
*/
const DOUBLE_LOWERING_SENTINEL: ReadonlySet<string> = new Set([
'packages/experimental/webworker-runtime/lib/index.js',
])
let failures = 0
const report: string[] = []
const log = (line: string): void => {
@@ -114,188 +87,11 @@ function discover(): string[] {
/**
* @returns Path relative to the repository root, for stable diagnostics.
* Always POSIX-separated: the exemption table and the recorded findings key
* on one form, and a win32 walk would otherwise miss every entry.
* 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('\\', '/')
/**
* Present a Node ESM namespace the way the worker loader hands one over, so a
* real dependency and a transformed one look the same to the module body.
* @param value - A module namespace, or whatever `require` returned.
* @returns The value, or an `__esModule`-marked projection of a Module namespace.
*/
function asLoaderExports(value: unknown): unknown {
if (value === null || typeof value !== 'object') return value
if ((value as { [Symbol.toStringTag]?: string })[Symbol.toStringTag] !== 'Module') return value
const out: Record<string, unknown> = {}
Object.defineProperty(out, '__esModule', { value: true })
for (const key of Object.keys(value)) {
Object.defineProperty(out, key, { enumerable: true, get: () => (value as Record<string, unknown>)[key] })
}
return out
}
/**
* Specifiers a transformed body will request, read straight out of the emitted
* code. The transform emits every static import as `require(<string literal>)`
* (`transform.ts` builds them with `JSON.stringify`), so a literal scan finds
* exactly the set that must be resolvable before the body runs. A dynamic
* `import(expr)` is not found and does not need to be: it resolves lazily,
* after the body has already produced its exports.
* @param code - Emitted CommonJS body.
* @returns The requested specifiers, deduplicated.
*/
function requestedSpecifiers(code: string): string[] {
const found = new Set<string>()
for (const match of code.matchAll(/require\("((?:[^"\\]|\\.)*)"\)/g)) {
const raw = match[1]
if (raw !== undefined) found.add(JSON.parse(`"${raw}"`) as string)
}
return [...found]
}
/**
* Load a dependency through the same loader that produces this check's baseline.
*
* This matters more than it looks. The baseline every file is compared against is
* `await import(file)` — Node's ESM loader. A dependency fetched with
* `createRequire` instead goes through the CommonJS resolver, which selects the
* `require` condition of a package's `exports` map: for a dual-build package that
* is a DIFFERENT ARTIFACT with a different interop shape. `@deepseek-ai/schemastery`
* is the case that exposed it — `require` yields `lib/index.cjs`, whose
* `module.exports` is the `Schema` function with no `default` and no `__esModule`,
* while `import` yields `lib/index.mjs`, a namespace with `default`. A body
* written against the second shape misbehaves when handed the first.
*
* That divergence also made the whole check runner-dependent: under the `tsx` CLI
* `require` was patched to return the ESM view and all 228 passed, while under
* `node --import tsx/esm` three files failed. A gate whose verdict depends on how
* it was launched is not a gate, so dependencies now come from `import()` and the
* CommonJS path is only a fallback.
* @param specifier - Module specifier as the transformed body requests it.
* @param path - Absolute path of the importing bundle.
* @returns The dependency in loader-facing form, or undefined when neither loader can supply it.
*/
async function loadDependency(specifier: string, path: string): Promise<unknown> {
const real = createRequire(pathToFileURL(path))
try {
// Resolve through the importer so relative and bare specifiers both work, then
// import the resolved file: resolution is CommonJS's, delivery is ESM's.
const resolved = specifier.startsWith('node:') ? specifier : pathToFileURL(real.resolve(specifier)).href
return asLoaderExports(await import(resolved))
} catch {
// Not importable as ESM (a genuine CommonJS-only dependency, or unresolvable).
try {
return asLoaderExports(real(specifier))
} catch {
return undefined
}
}
}
/** A stand-in for a dependency Node cannot load here: every access answers something callable. */
function fakeModule(): unknown {
const target: Record<string, unknown> = {}
return new Proxy(target, {
get: (holder, key) => {
if (key === '__esModule') return true
if (key === 'default') return function fakeDefault() {}
if (typeof key === 'symbol') return undefined
if (!(key in holder)) holder[key] = function fakeNamed() {}
return holder[key]
},
has: () => true,
})
}
const als = createAlsRuntime()
/**
* Execute a transformed body under the real wrapper contract.
*
* Dependencies are loaded BEFORE the body runs, because the body's `require` is
* synchronous while faithful delivery ({@link loadDependency}) is not. A
* dependency neither loader can supply falls back to a permissive stand-in: the
* subject under test is this file's own export shape, not its dependencies'.
* @param code - Emitted CommonJS body.
* @param path - Absolute path of the bundle, used for resolution and diagnostics.
* @returns The populated `exports` object.
*/
async function runTransformed(code: string, path: string): Promise<Record<string, unknown>> {
const exports: Record<string, unknown> = {}
const module = { exports }
const loaded = new Map<string, unknown>()
await Promise.all(requestedSpecifiers(code).map(async (specifier) => {
const delivered = await loadDependency(specifier, path)
if (delivered !== undefined) loaded.set(specifier, delivered)
}))
const fakes = new Map<string, unknown>()
const require = (specifier: string): unknown => {
const delivered = loaded.get(specifier)
if (delivered !== undefined) return delivered
if (!fakes.has(specifier)) fakes.set(specifier, fakeModule())
return fakes.get(specifier)
}
// eslint-disable-next-line @typescript-eslint/no-implied-eval -- the wrapper contract under test is a `new Function` body
const factory = new Function(...WRAPPER_PARAMS, code) as (...args: unknown[]) => void
const metaRequire = createRequire(pathToFileURL(path))
factory(exports, require, module, path, path.replace(/\/[^/]*$/, ''), {
url: pathToFileURL(path).href,
// Path-anchored like the worker loader; an import-only export face falls
// back to this check file's own resolver.
resolve: (specifier: string) => {
try {
return pathToFileURL(metaRequire.resolve(specifier)).href
} catch {
return import.meta.resolve(specifier)
}
},
}, als)
return exports
}
/** Module-syntax counts read from the AST. */
interface Counts {
staticImports: number
dynamicImports: number
importMeta: number
awaitExpressions: number
}
/** @returns Occurrence counts of the forms the transform rewrites. */
function countForms(source: string, _path: string): Counts {
const counts: Counts = { staticImports: 0, dynamicImports: 0, importMeta: 0, awaitExpressions: 0 }
let program: unknown
try {
program = parse(source, { ecmaVersion: 'latest', sourceType: 'module', allowAwaitOutsideFunction: true })
} catch {
// Counting is reporting only; a parse failure is the transform's to report.
return counts
}
const walk = (node: unknown): void => {
if (node === null || typeof node !== 'object') return
if (Array.isArray(node)) {
for (const child of node) walk(child)
return
}
const record = node as Record<string, unknown>
if (typeof record.type !== 'string') return
if (record.type === 'ImportDeclaration') counts.staticImports += 1
if (record.type === 'ImportExpression') counts.dynamicImports += 1
if (record.type === 'AwaitExpression') counts.awaitExpressions += 1
if (record.type === 'MetaProperty' && (record.meta as { name?: string } | undefined)?.name === 'import') {
counts.importMeta += 1
}
for (const [key, value] of Object.entries(record)) {
if (key === 'type' || key === 'start' || key === 'end') continue
walk(value)
}
}
walk(program)
return counts
}
const files = process.argv.slice(2).length > 0
? process.argv.slice(2).map(path => (path.startsWith('/') ? path : join(process.cwd(), path)))
: discover()
@@ -304,67 +100,17 @@ 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, mismatch: 0, transformFailed: 0, execFailed: 0, exempt: 0, unexpectedBaseline: 0, sentinelRefused: 0,
}
const totals = { bytesIn: 0, bytesOut: 0, lowered: 0, unchanged: 0, lineDrift: 0 }
const counts: Counts = { staticImports: 0, dynamicImports: 0, importMeta: 0, awaitExpressions: 0 }
const verdicts = { ok: 0, exempt: 0, unexpectedBaseline: 0 }
for (const file of files) {
const key = relative(file)
const source = readFileSync(file, 'utf8')
const observed = countForms(source, file)
counts.staticImports += observed.staticImports
counts.dynamicImports += observed.dynamicImports
counts.importMeta += observed.importMeta
counts.awaitExpressions += observed.awaitExpressions
totals.bytesIn += source.length
let code: string
try {
code = lowerModuleSource({ filename: file, source }).code
} catch (reason) {
const message = (reason as Error).message
if (DOUBLE_LOWERING_SENTINEL.has(key)) {
// Expected: this bundle's own text contains the sentinels the guard
// matches. Assert it is really the guard talking, not some other refusal.
if (message.includes('already lowered')) {
verdicts.sentinelRefused += 1
} else {
fail(`- WRONG REFUSAL ${key}: expected the double-lowering guard, got: ${message}`)
}
continue
}
fail(`- TRANSFORM FAILED ${key}: ${message}`)
verdicts.transformFailed += 1
continue
}
if (DOUBLE_LOWERING_SENTINEL.has(key)) {
fail(`- STALE SENTINEL ${key}: the double-lowering guard no longer refuses it; `
+ 'remove it from DOUBLE_LOWERING_SENTINEL or check whether the guard still works')
}
totals.bytesOut += code.length
if (code === source) totals.unchanged += 1
else totals.lowered += 1
// The debugging contract, over the whole corpus: a transformed body has the
// same line count as its source, so a stack frame still points at the right
// line.
const sourceLines = source.split('\n').length
const codeLines = code.split('\n').length
if (sourceLines !== codeLines) {
fail(`- LINE DRIFT ${key}: source ${String(sourceLines)} lines, transformed ${String(codeLines)}`)
totals.lineDrift += 1
}
const exemption = BASELINE_EXEMPT.get(key)
let expected: string[]
try {
expected = Object.keys(await import(pathToFileURL(file).href) as object).sort()
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.
// 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 {
@@ -376,42 +122,18 @@ if (files.length === 0) {
// 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`)
}
let actual: string[]
try {
actual = Object.keys(await runTransformed(code, file)).sort()
} catch (reason) {
fail(`- EXEC FAILED ${key}: ${(reason as Error).message.split('\n')[0]}`)
verdicts.execFailed += 1
continue
}
const missing = expected.filter(name => !actual.includes(name))
const extra = actual.filter(name => !expected.includes(name))
if (missing.length === 0 && extra.length === 0) {
verdicts.ok += 1
continue
}
fail(`- EXPORT MISMATCH ${key}: missing=[${missing.join(',')}] extra=[${extra.join(',')}]`)
verdicts.mismatch += 1
verdicts.ok += 1
}
const growth = totals.bytesIn === 0 ? 0 : ((totals.bytesOut - totals.bytesIn) / totals.bytesIn) * 100
log('')
log(`files=${String(files.length)} ok=${String(verdicts.ok)} exportMismatch=${String(verdicts.mismatch)} `
+ `transformFailed=${String(verdicts.transformFailed)} execFailed=${String(verdicts.execFailed)} `
+ `lineDrift=${String(totals.lineDrift)} baselineExempt=${String(verdicts.exempt)} `
+ `sentinelRefused=${String(verdicts.sentinelRefused)} `
log(`files=${String(files.length)} ok=${String(verdicts.ok)} baselineExempt=${String(verdicts.exempt)} `
+ `unexpectedBaselineFailure=${String(verdicts.unexpectedBaseline)}`)
log(`lowered=${String(totals.lowered)} packedAsIs=${String(totals.unchanged)} `
+ `bytes ${String(totals.bytesIn)} -> ${String(totals.bytesOut)} (${growth.toFixed(1)}%)`)
log(`forms: staticImport=${String(counts.staticImports)} dynamicImport=${String(counts.dynamicImports)} `
+ `importMeta=${String(counts.importMeta)} await=${String(counts.awaitExpressions)}`)
process.stdout.write(failures === 0
? `\ntransform-corpus-check: ${String(verdicts.ok)} bundles match their ESM baseline, `
+ `${String(verdicts.exempt)} exempt, ${String(verdicts.sentinelRefused)} sentinel-refused, no drift\n`
? `\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
}
@@ -1,15 +1,14 @@
/**
* Runs the full-corpus transform gate (`transform-corpus-check.ts`) in the
* 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:
* every built bundle's transformed export shape is compared against what
* `await import(file)` produces 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 corpus run measures the transform
* against a different loader and reports three of the four pinned baseline
* exemptions as stale. The gate's own note applies to itself: a gate whose
* verdict depends on how it was launched is not a gate.
* 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.
*/
@@ -19,7 +18,7 @@ import { expect, test } from 'vitest'
const runner = fileURLToPath(new URL('./transform-corpus-check.ts', import.meta.url))
test('every built bundle transforms to the export shape Node loads', (context) => {
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')) {
@@ -4,8 +4,8 @@
* are rewritten, that line numbers survive, which forms are refused, and that
* every covered trap form stays fixed.
*
* Scope boundary: this file checks the transform itself; the image collector's
* loop around it is covered by the packer's `transform-image.spec.ts`.
* Scope boundary: this file checks the transform itself; the pack-time loop
* around it is covered end-to-end by the packer's `image-loadable.spec.ts`.
* Emitted-code assertions are deliberately written against substrings
* of the real output rather than whole-file goldens: a golden would fail on every
* helper reordering, which is not the contract. The contract is the observable
@@ -22,6 +22,11 @@ interface TypertPlugin {
const DECORATOR_SYNTAX = /^\s*@[A-Za-z_$][\w$]*/m
// This plugin consumes tsc-emitted `lib/types` output, so every project it
// would re-diagnose has already passed the workspace tsc build in the same
// orchestration; the generator skips its per-package diagnostic pass here.
const TSC_VERIFIED_INPUT = { checkDiagnostics: false } as const
/** Generation scope selected by a tsdown build phase. */
export interface TypertPluginOptions {
/** Package mode emits only the package being bundled; workspace mode emits every explicit contributor once. */
@@ -78,7 +83,7 @@ export function typertPlugin(pluginOptions: TypertPluginOptions = {}): TypertPlu
if (manifest.name === undefined || !hasTypertExport(manifest.exports)) return
let artifacts = artifactsByRoot.get(root)
if (artifacts === undefined) {
const generator = new WorkspaceTypertGenerator(root)
const generator = new WorkspaceTypertGenerator(root, TSC_VERIFIED_INPUT)
artifacts = pluginOptions.faces === undefined
? generator.generate()
: generator.generate(undefined, pluginOptions.faces)
@@ -89,7 +94,7 @@ export function typertPlugin(pluginOptions: TypertPluginOptions = {}): TypertPlu
}
function emitWorkspace(root: string, faces: readonly TypertFace[] | undefined): void {
const generator = new WorkspaceTypertGenerator(root)
const generator = new WorkspaceTypertGenerator(root, TSC_VERIFIED_INPUT)
const packages = generator.discover(faces)
.filter(candidate => hasTypertExport(readManifest(join(root, candidate.root)).exports))
.map(candidate => candidate.package)
+24 -2
View File
@@ -5,7 +5,7 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { TypertAnalysisError, WorkspaceAnalyzer } from './analyzer.ts'
import { TypertAnalysisError, WorkspaceAnalyzer, WorkspaceCaches } from './analyzer.ts'
import type { DiscoveredTypertPackage } from './analyzer.ts'
import { FaceModelEmitter } from './emitter.ts'
import type { ModelEmitResult } from './emitter.ts'
@@ -16,13 +16,32 @@ export interface WorkspaceEmitResult extends ModelEmitResult {
readonly packageRoot: string
}
/** Behavior switches for one {@link WorkspaceTypertGenerator}. */
export interface WorkspaceTypertGeneratorOptions {
/**
* Run the per-package syntactic/semantic diagnostic pass before analysis
* (default true). Pass false only when the same orchestration already
* verified the workspace with tsc; the Typert-specific analysis checks
* (annotation coverage, private cross-package references, unretainable
* merges) run regardless.
*/
readonly checkDiagnostics?: boolean
}
/** Discover, analyze, and emit package reflection from independent faces. */
export class WorkspaceTypertGenerator {
/** Parsed-config and program-host state shared by every analyzer this generator creates. */
private readonly caches = new WorkspaceCaches()
/**
* Bind generation to one workspace root.
* @param root - directory containing face aggregate tsconfigs.
* @param options - behavior switches applied to every pass of this generator.
*/
constructor(private readonly root: string) {}
constructor(
private readonly root: string,
private readonly options: WorkspaceTypertGeneratorOptions = {},
) {}
/**
* Find public package faces that contribute Cordis services/events or
@@ -33,6 +52,7 @@ export class WorkspaceTypertGenerator {
discover(faces?: readonly TypertFace[]): DiscoveredTypertPackage[] {
return new WorkspaceAnalyzer({
root: this.root,
caches: this.caches,
...(faces === undefined ? {} : { faces }),
}).discoverPackages()
}
@@ -48,7 +68,9 @@ export class WorkspaceTypertGenerator {
const workspace = new WorkspaceAnalyzer({
root: this.root,
packages: selected,
caches: this.caches,
...(faces === undefined ? {} : { faces }),
...(this.options.checkDiagnostics === undefined ? {} : { checkDiagnostics: this.options.checkDiagnostics }),
}).analyze()
const artifacts: WorkspaceEmitResult[] = []
for (const face of workspace.faces) {
@@ -23,11 +23,13 @@ function fakeParent(): Agent {
vi.setConfig({ testTimeout: 30_000 })
/**
* Wait up to 10 seconds for CPU-bound worker startup or cross-thread delivery on contended CI.
* Host reactions after an observed event use explicit tight overrides, so this generous startup
* Wait up to 60 seconds for CPU-bound worker startup or cross-thread delivery on contended CI:
* startup is the only environment-sensitive phase of a same-process worker exchange, and the
* loaded self-hosted Windows pool stretches the tsx-in-worker boot past 10 seconds. Host
* reactions after an observed event use explicit tight overrides, so this generous startup
* allowance cannot hide multi-second reap regressions.
*/
function waitFor(assertion: () => void, timeout = 10_000): Promise<void> {
function waitFor(assertion: () => void, timeout = 60_000): Promise<void> {
return vi.waitFor(assertion, { timeout, interval: 50 })
}
@@ -181,7 +183,9 @@ async function run(ctx: Context, parent: Agent, source: { script: string; meta:
}
}
describe('dsh-workflow-worker-thread', () => {
// The per-test cap leaves room for one generous startup wait plus the tight
// post-event assertions; explicit narrower timeouts inside stay authoritative.
describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
describe('script execution over a real worker thread', () => {
it('runs a script end-to-end: agent() text results, phases, log, args, return value, events', async () => {
const { ctx, parent, provider } = await setup({ reply: (_request, index) => text(`answer-${index}`) })
+2 -2
View File
@@ -98,9 +98,9 @@ describe('CI workflow', () => {
))
expect(buildCommands.map(step => step.run)).toContain('pnpm run check:ci:windows-blocking')
// windows-coverage uses the lower 4-partition profile.
// windows-coverage runs the 6-partition profile.
expect(windowsCoverage.name).toBe('windows node 24 / coverage')
expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '4' })
expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '6' })
const coverageSteps = windowsCoverage.steps as unknown[]
const coverageCommands = coverageSteps.filter((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
+10 -9
View File
@@ -34,20 +34,21 @@ export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [
filter: 'packages/typert/generator/tests/',
exclude: 'packages/typert/generator/tests/**',
},
// The webworker-runtime package is outside the coverage requirement by
// decision: vitest.config.ts threshold-excludes its src, so every suite
// 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/**',
},
// Real child-process fixtures over scripts/ sources, which coverage never measures.
{ filter: 'scripts/install-lefthook.spec.ts', exclude: 'scripts/install-lefthook.spec.ts' },
{ 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.
+35 -9
View File
@@ -11,7 +11,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
import { isEntry, run } from './process.ts'
import { isEntry, runConcurrent } from './process.ts'
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
/** Where pack output lands when `--out` is omitted. */
@@ -24,8 +24,8 @@ const DEFAULT_OUTPUT = 'dist/npm'
* @param destination - absolute output directory.
* @returns The tarball filename.
*/
function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): string {
run('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
async function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): Promise<string> {
await runConcurrent('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
const filename = tarballName(member)
const tarball = join(destination, filename)
@@ -34,13 +34,27 @@ function packMember(family: ReleaseFamily, member: ReleaseMember, destination: s
return filename
}
/**
* @returns The validated `--concurrency` value; 1 (the default) packs the
* members one at a time, exactly as the credentialed publish workflows run it.
*/
function parseConcurrency(raw: string | undefined): number {
if (raw === undefined) return 1
const parsed = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
throw new Error(`--concurrency must be a positive integer, got ${JSON.stringify(raw)}`)
}
return parsed
}
/** Pack the family named by `--family` into `--out`. */
function main(): void {
async function main(): Promise<void> {
const { values } = parseArgs({
options: { family: { type: 'string' }, out: { type: 'string' } },
options: { family: { type: 'string' }, out: { type: 'string' }, concurrency: { type: 'string' } },
allowPositionals: false,
})
if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm]')
if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm] [--concurrency 1]')
const concurrency = parseConcurrency(values.concurrency)
const family = releaseFamily(values.family)
const root = process.cwd()
@@ -52,11 +66,23 @@ function main(): void {
rmSync(destination, { recursive: true, force: true })
mkdirSync(destination, { recursive: true })
const order: string[] = []
for (const member of members) order.push(packMember(family, member, destination))
// Members pack in a bounded pool; the recorded publish order stays the
// members' order regardless of completion order, because each worker writes
// its result at the member's own position.
const order = new Array<string>(members.length)
let cursor = 0
await Promise.all(Array.from({ length: Math.min(concurrency, members.length) }, async () => {
while (cursor < members.length) {
const index = cursor
cursor += 1
const member = members[index]
if (member === undefined) break
order[index] = await packMember(family, member, destination)
}
}))
writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`)
console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`)
}
if (isEntry(import.meta.url)) main()
if (isEntry(import.meta.url)) await main()
+14 -7
View File
@@ -3,7 +3,7 @@
* `pnpm`, `npm`, and `tar`, and each needs one of three failure behaviours.
*/
import { spawnSync } from 'node:child_process'
import { spawn, spawnSync } from 'node:child_process'
import { realpathSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
@@ -71,16 +71,23 @@ export function capture(command: string, args: readonly string[], options: RunOp
}
/**
* Run a command with inherited streams, so its progress reaches the log, and
* fail on a non-zero exit.
* Run a command with inherited streams without blocking the event loop, so a
* caller can hold several commands in flight, and fail on a non-zero exit.
* Concurrent children interleave their output at line granularity.
* @param command - executable name.
* @param args - command arguments.
* @param options - working directory and environment.
* @returns Resolves when the command exits with status zero.
*/
export function run(command: string, args: readonly string[], options: RunOptions = {}): void {
const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' })
if (result.error !== undefined) throw result.error
if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}`)
export function runConcurrent(command: string, args: readonly string[], options: RunOptions = {}): Promise<void> {
return new Promise((resolveRun, rejectRun) => {
const child = spawn(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' })
child.once('error', rejectRun)
child.once('close', (status, signal) => {
if (status === 0) resolveRun()
else rejectRun(new Error(`${command} ${args.join(' ')} exited with ${String(status ?? signal)}`))
})
})
}
/**
+4
View File
@@ -306,6 +306,10 @@ export default defineConfig({
// would put whole-workspace compiler analysis under v8
// instrumentation — the coverage lane's longest tail.
'packages/typert/generator/src/*.ts',
// Experimental webworker-runtime is outside the coverage requirement
// by decision: its correctness signal is its uninstrumented suite and
// the packer's end-to-end image spec.
'packages/experimental/webworker-runtime/src/**/*.ts',
'packages/host/apiproxy/src/index.ts',
'packages/host/apiproxy/src/invariant.ts',
'packages/host/apiproxy/src/api-proxy.ts',