mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #3063 from deepseek-harness/fix/window0825
perf(ci): optimize coverage / snapshot parameter
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`) })
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user