mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
* fix(llm): generate provider metadata and clarify credential hints * fix(settings): shorten credential hints and mark detected sources in green * fix(llm): verify staged provider metadata and document regeneration * fix(provider-metadata): reconcile authentication hints and key controls * test(user-text): calibrate ratio headroom across unmatched workloads * test(gates): include provider metadata in hygiene expectations * fix(models): hide authentication hints for entered keys
1372 lines
59 KiB
TypeScript
1372 lines
59 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi, type MockInstance } from 'vitest'
|
|
import {
|
|
cliGateOptions,
|
|
ciWorkerEnvironment,
|
|
collectDescendants,
|
|
defaultConcurrency,
|
|
formatGateResultReason,
|
|
gatesForMode,
|
|
parsePidPpidLines,
|
|
runGate,
|
|
runGates,
|
|
taskkillArgs,
|
|
type Gate,
|
|
type GateResult,
|
|
} from './run-gates.ts'
|
|
|
|
// Graph fixtures select their own browser pool instead of inheriting the CI
|
|
// host's pool, and coverage gates take their shape from the values each case
|
|
// sets, not from the lane that runs this file (a partitioned lane exports
|
|
// DSH_COVERAGE_PARTITIONS to every child).
|
|
beforeEach(() => {
|
|
vi.stubEnv('DSH_WEB_SNAPSHOT_WORKERS', undefined)
|
|
vi.stubEnv('DSH_COVERAGE_MAX_WORKERS', undefined)
|
|
vi.stubEnv('DSH_COVERAGE_PARTITIONS', undefined)
|
|
})
|
|
afterEach(() => vi.unstubAllEnvs())
|
|
|
|
/**
|
|
* Capture output a gate streams through runGate's streamOutput path.
|
|
* @param onOutput - optional observer called after each captured chunk.
|
|
* @returns the accumulated chunks and the stdout spy to restore in finally.
|
|
*/
|
|
function captureStreamedOutput(onOutput?: (chunks: readonly string[]) => void): { writes: string[]; write: MockInstance } {
|
|
const writes: string[] = []
|
|
const write = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
writes.push(String(chunk))
|
|
onOutput?.(writes)
|
|
return true
|
|
})
|
|
return { writes, write }
|
|
}
|
|
|
|
/**
|
|
* A process has stopped executing when its /proc entry is gone, or when it
|
|
* lingers as a zombie ('Z') — an un-reaped but dead entry still answers
|
|
* kill(pid, 0), so existence is not a liveness check. Non-Linux falls back to
|
|
* kill(pid, 0), whose ESRCH means the process is gone.
|
|
*/
|
|
function procStopped(pid: number): boolean {
|
|
if (process.platform === 'linux') {
|
|
try {
|
|
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8')
|
|
return /\)\s+Z\s/.test(stat)
|
|
} catch {
|
|
return true
|
|
}
|
|
}
|
|
try {
|
|
process.kill(pid, 0)
|
|
return false
|
|
} catch {
|
|
return true
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait until the captured output contains `marker` and the grandchild pid the
|
|
* gate printed, then return that pid.
|
|
* @param writes - chunks captured from the gate's streamed stdout.
|
|
* @param marker - the output line that proves the gate reached the abort point.
|
|
* @param deadline - fail the wait when exceeded.
|
|
* @returns the grandchild pid printed by the gate script.
|
|
*/
|
|
async function waitForGrandchildPid(writes: string[], marker: string, deadline: number): Promise<number> {
|
|
let pid: number | undefined
|
|
while ((pid === undefined || !writes.join('').includes(marker)) && Date.now() < deadline) {
|
|
const match = writes.join('').match(/grandchild:(\d+)/)
|
|
if (match !== null) pid = Number(match[1])
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
}
|
|
expect(pid ?? 0).toBeGreaterThan(0)
|
|
expect(writes.join('')).toContain(marker)
|
|
return pid!
|
|
}
|
|
|
|
/**
|
|
* Abort the run and assert it settles marked aborted with the grandchild no
|
|
* longer executing — the abort path must have signalled it from the captured
|
|
* descendant list rather than settling over a live orphan.
|
|
* @param promise - the pending `runGate` promise.
|
|
* @param controller - the signal source to abort.
|
|
* @param pid - the grandchild pid the gate script printed.
|
|
*/
|
|
async function abortAndExpectTreeStopped(promise: Promise<GateResult>, controller: AbortController, pid: number): Promise<void> {
|
|
controller.abort()
|
|
const result = await promise
|
|
expect(result.aborted).toBe(true)
|
|
const stopDeadline = Date.now() + 8000
|
|
while (!procStopped(pid) && Date.now() < stopDeadline) {
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
}
|
|
expect(procStopped(pid)).toBe(true)
|
|
}
|
|
|
|
|
|
function gate(id: string, options: Partial<Gate> = {}): Gate {
|
|
return {
|
|
id,
|
|
label: id,
|
|
displayCommand: `run ${id}`,
|
|
command: process.execPath,
|
|
args: ['-e', ''],
|
|
...options,
|
|
}
|
|
}
|
|
|
|
function resultFor(subject: Gate, status: GateResult['status'] = 'passed'): GateResult {
|
|
return {
|
|
gate: subject,
|
|
status,
|
|
durationMs: 10,
|
|
output: [],
|
|
exitCode: status === 'passed' ? 0 : 1,
|
|
signalCode: null,
|
|
}
|
|
}
|
|
|
|
function withPnpmEntrypoint<T>(action: () => T, entrypoint = '/private/pnpm.cjs'): T {
|
|
const previous = process.env.npm_execpath
|
|
process.env.npm_execpath = entrypoint
|
|
try {
|
|
return action()
|
|
} finally {
|
|
if (previous === undefined) Reflect.deleteProperty(process.env, 'npm_execpath')
|
|
else process.env.npm_execpath = previous
|
|
}
|
|
}
|
|
|
|
function withEnv<T>(name: string, value: string | undefined, action: () => T): T {
|
|
const previous = process.env[name]
|
|
if (value === undefined) Reflect.deleteProperty(process.env, name)
|
|
else process.env[name] = value
|
|
try {
|
|
return action()
|
|
} finally {
|
|
if (previous === undefined) Reflect.deleteProperty(process.env, name)
|
|
else process.env[name] = previous
|
|
}
|
|
}
|
|
|
|
// Mutates worker-global state: only use for synchronous, non-concurrent graph inspection.
|
|
function withPlatform<T>(platform: NodeJS.Platform, action: () => T): T {
|
|
const original = Object.getOwnPropertyDescriptor(process, 'platform')!
|
|
Object.defineProperty(process, 'platform', { ...original, value: platform })
|
|
try { return action() } finally { Object.defineProperty(process, 'platform', original) }
|
|
}
|
|
|
|
// Mutates worker-global state: only use for synchronous, non-concurrent graph inspection.
|
|
function withNodeVersion<T>(version: string, action: () => T): T {
|
|
const original = Object.getOwnPropertyDescriptor(process.versions, 'node')!
|
|
Object.defineProperty(process.versions, 'node', { ...original, value: version })
|
|
try { return action() } finally { Object.defineProperty(process.versions, 'node', original) }
|
|
}
|
|
|
|
describe('CI worker allocation', () => {
|
|
it.each([1, 2, 4, 8, 16, 64])('shares a %i CPU coverage budget without multiplying pools', (cpus) => {
|
|
const env = ciWorkerEnvironment('ci-coverage', { CI: 'true' }, cpus)
|
|
const exempt = Math.max(1, Math.floor(cpus / 3))
|
|
const instrumented = Number(env.DSH_COVERAGE_PARTITIONS ?? 1)
|
|
expect(Number(env.DSH_COVERAGE_MAX_WORKERS)).toBe(cpus)
|
|
expect(instrumented + exempt).toBe(Math.max(2, cpus))
|
|
expect(defaultConcurrency('ci-coverage', 3, cpus, { CI: 'true' }).workers).toBe(Math.min(3, cpus))
|
|
if (cpus <= 2) expect(env.DSH_COVERAGE_PARTITIONS).toBeUndefined()
|
|
})
|
|
|
|
it('bounds overlapping readers and lets the isolated browser pool use the runner', () => {
|
|
const env = ciWorkerEnvironment('ci-consumers', { CI: 'true' }, 16)
|
|
expect(env).toMatchObject({
|
|
DSH_OXLINT_THREADS: '8',
|
|
DSH_PUBLINT_CONCURRENCY: '8',
|
|
DSH_SNAPSHOT_MAX_WORKERS: '1',
|
|
DSH_SNAPSHOT_MAX_CONCURRENCY: '8',
|
|
DSH_WEB_SNAPSHOT_WORKERS: '16',
|
|
})
|
|
})
|
|
|
|
it('caps local defaults below the CPU count Node reports', () => {
|
|
expect(ciWorkerEnvironment('ci-coverage', {}, 256)).toMatchObject({
|
|
DSH_OXLINT_THREADS: '6',
|
|
DSH_COVERAGE_MAX_WORKERS: '48',
|
|
DSH_COVERAGE_PARTITIONS: '32',
|
|
DSH_WEB_SNAPSHOT_WORKERS: '12',
|
|
})
|
|
expect(defaultConcurrency('ci-static', 60, 256, {})).toEqual({
|
|
workers: 12,
|
|
source: '12 local default worker(s) of 256 CPU(s)',
|
|
})
|
|
expect(defaultConcurrency('ci-static', 60, 8, {}).workers).toBe(8)
|
|
})
|
|
|
|
it('preserves serial reference overrides on large hosts', () => {
|
|
const inherited = {
|
|
DSH_GATE_CONCURRENCY: '1',
|
|
DSH_COVERAGE_MAX_WORKERS: '1',
|
|
DSH_OXLINT_THREADS: '1',
|
|
DSH_PUBLINT_CONCURRENCY: '1',
|
|
DSH_SNAPSHOT_MAX_CONCURRENCY: '1',
|
|
DSH_WEB_SNAPSHOT_WORKERS: '1',
|
|
}
|
|
const additions = ciWorkerEnvironment('ci-primary', inherited, 64)
|
|
expect(additions).toEqual({ DSH_SNAPSHOT_MAX_WORKERS: '1' })
|
|
expect(inherited.DSH_COVERAGE_MAX_WORKERS).toBe('1')
|
|
})
|
|
|
|
it('honors an explicit coverage budget and partition override', () => {
|
|
expect(ciWorkerEnvironment('ci-coverage', { DSH_COVERAGE_MAX_WORKERS: '6' }, 16))
|
|
.toHaveProperty('DSH_COVERAGE_PARTITIONS', '4')
|
|
expect(ciWorkerEnvironment('ci-coverage', { DSH_COVERAGE_PARTITIONS: '3' }, 16))
|
|
.not.toHaveProperty('DSH_COVERAGE_PARTITIONS')
|
|
})
|
|
|
|
it('leaves the plain unit inventory with the environment pnpm run test finds', () => {
|
|
// A 3-CPU host would otherwise receive DSH_COVERAGE_PARTITIONS=2, which
|
|
// the inventory's own coverage-gate tests read as a partitioned lane.
|
|
expect(ciWorkerEnvironment('ci-unit', {}, 3)).toEqual({})
|
|
expect(ciWorkerEnvironment('ci-unit', {}, 16)).toEqual({})
|
|
})
|
|
|
|
it.each(['0', '-1', 'NaN', '2.5'])('rejects invalid worker budget %s', (raw) => {
|
|
expect(() => ciWorkerEnvironment('ci-coverage', { DSH_COVERAGE_MAX_WORKERS: raw }, 16))
|
|
.toThrow('DSH_COVERAGE_MAX_WORKERS must be a positive integer')
|
|
})
|
|
|
|
it('keeps local documentation defaults unchanged', () => {
|
|
expect(ciWorkerEnvironment('doc-sync', {}, 64)).toEqual({})
|
|
})
|
|
})
|
|
|
|
describe('gate graph validation', () => {
|
|
it.each([
|
|
'ci-primary',
|
|
'ci-linux-primary',
|
|
'ci-static',
|
|
'ci-lint-contracts-ready',
|
|
'ci-coverage',
|
|
'ci-unit',
|
|
'ci-bench',
|
|
'ci-snapshot',
|
|
'ci-artifacts',
|
|
'ci-consumers',
|
|
'ci-windows-blocking',
|
|
'ci-windows-complete',
|
|
'ci-windows-observational-ready',
|
|
'node-compat',
|
|
'check-all',
|
|
'hygiene',
|
|
'doc-sync',
|
|
'doc-quick',
|
|
] as const)('constructs and executes preflight for a valid non-empty %s graph', async (mode) => {
|
|
const subject = withPnpmEntrypoint(() => gatesForMode(mode))
|
|
const execute = vi.fn(async (item: Gate) => resultFor(item))
|
|
|
|
await expect(runGates(subject, subject.length, execute)).resolves.toHaveLength(subject.length)
|
|
})
|
|
|
|
it('uses the internal CI benchmark runner without changing the public benchmark scripts', () => {
|
|
const subject = withPnpmEntrypoint(() => gatesForMode('ci-bench'))
|
|
const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
|
|
scripts: Record<string, string>
|
|
}
|
|
|
|
expect(scripts['check:ci:bench']).toBe('tsx scripts/run-gates.ts ci-bench')
|
|
expect(subject).toHaveLength(1)
|
|
expect(subject[0]).toMatchObject({
|
|
id: 'bench',
|
|
displayCommand: 'pnpm exec tsx scripts/run-ci-bench.ts',
|
|
args: ['/private/pnpm.cjs', 'exec', 'tsx', 'scripts/run-ci-bench.ts'],
|
|
})
|
|
expect(scripts['test:bench']).toBe('npm run build:bench && npm run build:web && npm run test:bench:built')
|
|
expect(scripts['build:bench']).toBe(
|
|
'npm run build:native-system && npm run build:lib && tsdown --config-loader native --config benchmarks/tsdown.config.ts',
|
|
)
|
|
expect(scripts['build:native-system']).toBe('tsx native/system/scripts/build.ts --host-addon-only')
|
|
expect(scripts['test:bench:built']).toBe('vitest run --config vitest.bench.config.ts')
|
|
})
|
|
|
|
it('checks all maintained repository references locally and in CI', () => {
|
|
const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
|
|
scripts: Record<string, string>
|
|
}
|
|
for (const mode of ['doc-sync', 'doc-quick', 'ci-static'] as const) {
|
|
const gates = withPnpmEntrypoint(() => gatesForMode(mode))
|
|
expect(gates).toContainEqual(expect.objectContaining({
|
|
id: 'repository-references',
|
|
displayCommand: 'pnpm run verify-repository-references',
|
|
}))
|
|
}
|
|
expect(scripts['verify-repository-references']).toBe('tsx scripts/verify-repository-references.ts')
|
|
})
|
|
|
|
it('keeps the public repository link policy in the documentation gate', () => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
|
|
|
expect(ids).toContain('public-repository-links')
|
|
})
|
|
|
|
it('checks the complete CLI reference locally and in the static CI lane', () => {
|
|
for (const mode of ['doc-sync', 'ci-static'] as const) {
|
|
const gates = withPnpmEntrypoint(() => gatesForMode(mode))
|
|
expect(gates.find(gate => gate.id === 'cli-help')).toBeDefined()
|
|
}
|
|
})
|
|
|
|
it('keeps the concrete terminology policy in the documentation gate', () => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
|
|
|
expect(ids).toContain('concrete-terms')
|
|
})
|
|
|
|
it('checks retrospective releases alongside the current persistence history', () => {
|
|
for (const mode of ['doc-sync', 'ci-static'] as const) {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
|
expect(ids).toEqual(expect.arrayContaining(['persistence-changes', 'persistence-releases']))
|
|
}
|
|
})
|
|
|
|
it('requires complete Session format references locally and in CI', () => {
|
|
const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
|
|
scripts: Record<string, string>
|
|
}
|
|
for (const mode of ['doc-sync', 'doc-quick', 'ci-static'] as const) {
|
|
expect(withPnpmEntrypoint(() => gatesForMode(mode))).toContainEqual(expect.objectContaining({
|
|
id: 'persistence-formats',
|
|
displayCommand: 'pnpm run verify-persistence-formats',
|
|
}))
|
|
}
|
|
expect(scripts['verify-persistence-formats']).toBe('tsx scripts/persistence-formats.ts')
|
|
})
|
|
|
|
it('keeps package-group subsystem ownership in the documentation gate', () => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
|
|
|
expect(ids).toContain('subsystem-pages')
|
|
})
|
|
|
|
it('keeps the package README Summary limit in the documentation gate', () => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
|
|
|
expect(ids).toContain('package-readme-summaries')
|
|
})
|
|
|
|
it('derives the quick documentation aggregate from marked doc-sync leaves', () => {
|
|
const full = withPnpmEntrypoint(() => gatesForMode('doc-sync'))
|
|
const quick = withPnpmEntrypoint(() => gatesForMode('doc-quick'))
|
|
|
|
expect(quick).toEqual(full.filter(gate => gate.quick === true))
|
|
})
|
|
|
|
it('checks the recorded npm dependency catalog in both documentation aggregates', () => {
|
|
for (const mode of ['doc-sync', 'doc-quick'] as const) {
|
|
expect(withPnpmEntrypoint(() => gatesForMode(mode).find(gate => gate.id === 'dependency-catalog')))
|
|
.toMatchObject({ args: ['/private/pnpm.cjs', 'run', 'verify-dependency-catalog'] })
|
|
}
|
|
})
|
|
|
|
it('keeps the hygiene aggregate aligned with the package script checks', () => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
|
|
|
|
expect(ids).toEqual([
|
|
'rescope-vendor', 'publint', 'constraints', 'default-product-isolation', 'product-use', 'official-bundle-catalog', 'provider-data', 'package-dependencies', 'application-entrypoints',
|
|
'dsh-package-licenses', 'node-next-types',
|
|
'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'client-route-resolution', 'no-bare-dispatcher',
|
|
'plugin-record-callers', 'no-unknown-casts',
|
|
'cordis-config', 'runtime-closure',
|
|
])
|
|
})
|
|
|
|
it('caps the local hygiene aggregate at eight workers', () => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
|
|
|
|
expect(defaultConcurrency('hygiene', ids.length, 16, { CI: 'true' })).toEqual({
|
|
workers: 8,
|
|
source: '16 available CPU(s), hygiene cap 8',
|
|
})
|
|
expect(defaultConcurrency('doc-sync', 43, 256, {})).toEqual({
|
|
workers: 8,
|
|
source: '12 local default worker(s) of 256 CPU(s), doc-sync cap 8',
|
|
})
|
|
})
|
|
|
|
it('schedules the longest documentation leaves before short checks', () => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
|
|
|
expect(ids.slice(0, 11)).toEqual([
|
|
'doc-typecheck', 'docs-site-build', 'doc-graphs', 'cli-help', 'markdown-links', 'type-equivalence',
|
|
'cordis-catalog', 'cordis-inspect-catalog', 'workflow-guest', 'mermaid', 'translation-pairing',
|
|
])
|
|
})
|
|
|
|
it('launches a native pnpm entrypoint directly', () => {
|
|
const entrypoint = String.raw`C:\Program Files\pnpm\pnpm.exe`
|
|
const subject = withPnpmEntrypoint(() => gatesForMode('ci-windows-blocking')[0], entrypoint)
|
|
|
|
expect(subject).toMatchObject({
|
|
command: entrypoint,
|
|
args: ['run', 'build'],
|
|
})
|
|
})
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
|
|
'keeps the DSH package license policy in %s',
|
|
(mode) => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
|
|
|
expect(ids).toContain('dsh-package-licenses')
|
|
},
|
|
)
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
|
|
'rejects new unknown casts in %s',
|
|
(mode) => {
|
|
const gate = withPnpmEntrypoint(() => gatesForMode(mode).find(subject => subject.id === 'no-unknown-casts'))
|
|
|
|
expect(gate?.args).toContain('verify-no-unknown-casts')
|
|
expect(gate?.allowFailure).not.toBe(true)
|
|
},
|
|
)
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
|
|
'keeps package dependency enforcement in %s',
|
|
(mode) => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
|
|
|
expect(ids).toContain('package-dependencies')
|
|
},
|
|
)
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
|
|
'executes default-product experimental isolation in %s',
|
|
(mode) => {
|
|
const gate = withPnpmEntrypoint(() => gatesForMode(mode)
|
|
.find(subject => subject.id === 'default-product-isolation'))
|
|
|
|
expect(gate?.args).toContain('verify-default-product-isolation')
|
|
expect(gate?.allowFailure).not.toBe(true)
|
|
},
|
|
)
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
|
|
'keeps the client dependency policy in %s',
|
|
(mode) => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
|
|
|
expect(ids).toContain('client-packages')
|
|
},
|
|
)
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
|
|
'keeps weighted approval policy tests in %s',
|
|
(mode) => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
|
|
|
expect(ids).toContain('approval-policy')
|
|
},
|
|
)
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
|
|
'keeps hard-coded Client UI copy enforcement in %s',
|
|
(mode) => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
|
|
|
expect(ids).toContain('client-ui-i18n')
|
|
},
|
|
)
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
|
|
'keeps browser app-route resolution enforcement in %s',
|
|
(mode) => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
|
|
|
expect(ids).toContain('client-route-resolution')
|
|
},
|
|
)
|
|
|
|
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
|
|
'keeps application entrypoint enforcement in %s',
|
|
(mode) => {
|
|
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
|
|
|
expect(ids).toContain('application-entrypoints')
|
|
},
|
|
)
|
|
|
|
it('keeps native Windows coverage blocking and behind the complete build', () => {
|
|
const complete = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
|
|
const observational = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational-ready'))
|
|
.filter(gate => gate.id !== 'docs-site-build' && gate.id !== 'electron-install')
|
|
const byId = new Map(complete.map(subject => [subject.id, subject]))
|
|
|
|
expect(byId.get('coverage')?.allowFailure).not.toBe(true)
|
|
expect(byId.get('coverage')?.needs).toContain('build')
|
|
expect(byId.get('coverage-exempt-heavy')?.allowFailure).not.toBe(true)
|
|
expect(byId.get('coverage')?.needs).toContain('build')
|
|
expect(byId.get('coverage-exempt-heavy')?.needs).toContain('build')
|
|
expect(byId.get('coverage-exempt-heavy')?.args).toContain(
|
|
'packages/experimental/webworker-packer/tests/image-loadable.spec.ts',
|
|
)
|
|
expect(observational).not.toHaveLength(0)
|
|
for (const gate of observational) {
|
|
const completeGate = byId.get(gate.id)
|
|
expect(completeGate?.allowFailure).toBe(true)
|
|
expect(completeGate?.after).toEqual(expect.arrayContaining([
|
|
'coverage',
|
|
'coverage-exempt-heavy',
|
|
]))
|
|
expect((completeGate?.needs ?? []).filter(id => id !== 'build')).toEqual(gate.needs ?? [])
|
|
}
|
|
})
|
|
|
|
it('runs the Windows built-bin smoke after other observational gates settle', () => {
|
|
const observational = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational-ready'))
|
|
const builtBin = observational.find(gate => gate.id === 'built-bin-smoke')
|
|
|
|
expect(builtBin?.after).toEqual(
|
|
observational.filter(gate => gate.id !== 'built-bin-smoke').map(gate => gate.id),
|
|
)
|
|
|
|
const completeBuiltBin = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
|
|
.find(gate => gate.id === 'built-bin-smoke')
|
|
expect(completeBuiltBin?.after).toContain('windows-site')
|
|
expect(completeBuiltBin?.after).not.toContain('docs-site-build')
|
|
})
|
|
|
|
it('reuses the Windows build without dropping diagnostics or rebuilding the workspace', () => {
|
|
const ready = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational-ready'))
|
|
const complete = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
|
|
const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
|
|
scripts: Record<string, string>
|
|
}
|
|
|
|
expect(scripts['check:ci:windows-observational-ready']).toBe('tsx scripts/run-gates.ts ci-windows-observational-ready')
|
|
expect(scripts).not.toHaveProperty('check:ci:windows-observational')
|
|
const completeOnly = new Set(['build', 'windows-site', 'native-system', 'coverage', 'coverage-exempt-heavy'])
|
|
const shared = complete.filter(gate => !completeOnly.has(gate.id))
|
|
expect(ready.map(gate => gate.id).sort()).toEqual([...shared.map(gate => gate.id), 'docs-site-build'].sort())
|
|
expect(ready.some(gate => gate.id === 'build')).toBe(false)
|
|
expect(ready.find(gate => gate.id === 'docs-site-build')?.displayCommand).toBe('pnpm run docs:build:mpa')
|
|
for (const diagnostic of shared) {
|
|
expect(ready.find(gate => gate.id === diagnostic.id)).toMatchObject({
|
|
command: diagnostic.command,
|
|
args: diagnostic.args,
|
|
})
|
|
expect(ready.find(gate => gate.id === diagnostic.id)?.env).toEqual(diagnostic.env)
|
|
}
|
|
expect(complete.find(gate => gate.id === 'build')).toBeDefined()
|
|
expect(complete.find(gate => gate.id === 'coverage')).toBeDefined()
|
|
})
|
|
|
|
it('runs built Windows smoke after failed diagnostics settle on an existing build', async () => {
|
|
const ready = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational-ready'))
|
|
const settled = new Set<string>()
|
|
const results = await runGates(ready, 8, async (subject) => {
|
|
if (subject.id === 'built-bin-smoke') {
|
|
expect(settled.size).toBe(ready.length - 1)
|
|
expect(settled.has('doc-graphs')).toBe(true)
|
|
}
|
|
settled.add(subject.id)
|
|
return resultFor(subject, subject.id === 'doc-graphs' ? 'failed' : 'passed')
|
|
})
|
|
|
|
expect(results.filter(result => result.status === 'failed').map(result => result.gate.id)).toEqual(['doc-graphs'])
|
|
expect(results.find(result => result.gate.id === 'built-bin-smoke')?.status).toBe('passed')
|
|
expect(results.some(result => result.status === 'skipped')).toBe(false)
|
|
})
|
|
|
|
it.each([undefined, '3'])('provisions Electron before Windows coverage with partition count %s', async (partitions) => {
|
|
const gates = withEnv('DSH_COVERAGE_PARTITIONS', partitions, () =>
|
|
withPnpmEntrypoint(() => gatesForMode('ci-windows-complete')))
|
|
const installers = gates.filter(gate => gate.id === 'electron-install')
|
|
expect(installers).toHaveLength(1)
|
|
expect(installers[0]?.allowFailure).not.toBe(true)
|
|
expect(installers[0]?.needs ?? []).not.toContain('build')
|
|
expect(gates.find(gate => gate.id === 'coverage')?.needs).toContain('electron-install')
|
|
let installed = false
|
|
await runGates(gates, 8, async (subject) => {
|
|
if (subject.id === 'electron-install') installed = true
|
|
if (subject.id === 'coverage') expect(installed).toBe(true)
|
|
return resultFor(subject, 'passed')
|
|
})
|
|
})
|
|
|
|
it.each(['ci-windows-complete', 'ci-windows-observational-ready'] as const)(
|
|
'provisions the locked Electron binary before built smoke in %s', (mode) => {
|
|
const gates = withPnpmEntrypoint(() => gatesForMode(mode))
|
|
const install = gates.find(gate => gate.id === 'electron-install')
|
|
expect(install).toMatchObject({
|
|
command: process.execPath,
|
|
args: ['/private/pnpm.cjs', '--filter', '@deepseek-ai/dsh-desktop', 'exec', 'install-electron'],
|
|
env: { ELECTRON_GET_USE_PROXY: '1' },
|
|
})
|
|
expect(gates.find(gate => gate.id === 'built-bin-smoke')?.needs).toContain('electron-install')
|
|
expect(gates.filter(gate => gate.id !== 'electron-install').every(gate => gate.env?.ELECTRON_GET_USE_PROXY === undefined)).toBe(true)
|
|
},
|
|
)
|
|
|
|
it.each(['ci-windows-complete', 'ci-windows-observational-ready'] as const)(
|
|
'keeps failed Electron provisioning visible without stopping unrelated diagnostics in %s', async (mode) => {
|
|
const gates = withPnpmEntrypoint(() => gatesForMode(mode))
|
|
const attempted: string[] = []
|
|
const results = await runGates(gates, 8, async (subject) => {
|
|
attempted.push(subject.id)
|
|
return resultFor(subject, subject.id === 'electron-install' ? 'failed' : 'passed')
|
|
})
|
|
expect(results.filter(result => result.status === 'failed').map(result => result.gate.id)).toEqual(['electron-install'])
|
|
expect(attempted).not.toContain('built-bin-smoke')
|
|
expect(results.find(result => result.gate.id === 'built-bin-smoke')?.status).toBe('skipped')
|
|
expect(results.find(result => result.gate.id === 'doc-graphs')?.status).toBe('passed')
|
|
if (mode === 'ci-windows-complete') {
|
|
expect(results.filter(result => result.status === 'failed' && !result.gate.allowFailure).map(result => result.gate.id))
|
|
.toEqual(['electron-install'])
|
|
expect(attempted).not.toContain('coverage')
|
|
expect(results.find(result => result.gate.id === 'coverage')?.status).toBe('skipped')
|
|
expect(results.find(result => result.gate.id === 'coverage-exempt-heavy')?.status).toBe('passed')
|
|
}
|
|
},
|
|
)
|
|
|
|
it.each(['ci-coverage', 'ci-artifacts', 'ci-windows-blocking'] as const)(
|
|
'only provisions Electron for native Windows coverage in %s', (mode) => {
|
|
const original = Object.getOwnPropertyDescriptor(process, 'platform')
|
|
for (const platform of ['linux', 'darwin', 'win32'] as const) {
|
|
const gates = withPlatform(platform, () => withPnpmEntrypoint(() => gatesForMode(mode)))
|
|
const needed = platform === 'win32' && mode === 'ci-coverage'
|
|
expect(gates.filter(gate => gate.id === 'electron-install')).toHaveLength(needed ? 1 : 0)
|
|
if (needed) {
|
|
expect(gates.find(gate => gate.id === 'coverage')?.needs).toContain('electron-install')
|
|
expect(gates.find(gate => gate.id === 'electron-install')?.env).toEqual({ ELECTRON_GET_USE_PROXY: '1' })
|
|
expect(gates.filter(gate => gate.id !== 'electron-install').every(gate => gate.env?.ELECTRON_GET_USE_PROXY === undefined)).toBe(true)
|
|
}
|
|
}
|
|
expect(Object.getOwnPropertyDescriptor(process, 'platform')).toEqual(original)
|
|
},
|
|
)
|
|
|
|
it('restores the platform descriptor when coverage configuration is rejected', () => {
|
|
const original = Object.getOwnPropertyDescriptor(process, 'platform')
|
|
expect(() => withEnv('DSH_COVERAGE_PARTITIONS', '1', () =>
|
|
withPlatform('win32', () => withPnpmEntrypoint(() => gatesForMode('ci-coverage')))))
|
|
.toThrow('DSH_COVERAGE_PARTITIONS must be an integer greater than 1')
|
|
expect(Object.getOwnPropertyDescriptor(process, 'platform')).toEqual(original)
|
|
})
|
|
|
|
it('leaves the lane test budget to the inherited environment on both coverage gates', () => {
|
|
// vitest.config.ts reads DSH_COVERAGE_TEST_TIMEOUT_MS per inline project
|
|
// (coverageTestTimeoutOptions); a gate argument would add a second owner.
|
|
for (const budget of ['15000', undefined]) {
|
|
const gates = withEnv('DSH_COVERAGE_TEST_TIMEOUT_MS', budget, () =>
|
|
withPnpmEntrypoint(() => gatesForMode('ci-windows-complete')))
|
|
for (const id of ['coverage', 'coverage-exempt-heavy']) {
|
|
const gate = gates.find(subject => subject.id === id)
|
|
if (gate === undefined) throw new Error(`ci-windows-complete must define the ${id} gate`)
|
|
expect(gate.args).not.toEqual(expect.arrayContaining([
|
|
expect.stringMatching(/^--(?:testTimeout|expect\.poll\.timeout|hookTimeout)=/),
|
|
]))
|
|
expect(gate.env ?? {}).not.toHaveProperty('DSH_COVERAGE_TEST_TIMEOUT_MS')
|
|
}
|
|
}
|
|
})
|
|
|
|
it('runs the plain unit inventory after the native build and leaves its budget to the inherited environment', () => {
|
|
const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
|
|
scripts: Record<string, string>
|
|
}
|
|
expect(scripts['check:ci:unit']).toBe('tsx scripts/run-gates.ts ci-unit')
|
|
|
|
// The aggregate is the `test` package script's two segments, in order,
|
|
// so a step added to one cannot silently leave the other. The lane sets
|
|
// DSH_COVERAGE_TEST_TIMEOUT_MS; vitest.config.ts reads it per inline
|
|
// project (coverageTestTimeoutOptions), so the gate passes no budget flag.
|
|
const [nativeBuild, unitRun, ...rest] = (scripts.test ?? '').split(' && ')
|
|
expect(rest).toEqual([])
|
|
for (const budget of ['15000', undefined]) {
|
|
const gates = withEnv('DSH_COVERAGE_TEST_TIMEOUT_MS', budget, () =>
|
|
withPnpmEntrypoint(() => gatesForMode('ci-unit')))
|
|
expect(gates.map(gate => gate.id)).toEqual(['native-system', 'unit'])
|
|
expect(gates[0]).toMatchObject({ displayCommand: nativeBuild })
|
|
expect(gates[1]?.displayCommand).toBe(`pnpm exec ${unitRun}`)
|
|
expect(gates[1]).toMatchObject({
|
|
label: 'test',
|
|
needs: ['native-system'],
|
|
streamOutput: true,
|
|
args: ['/private/pnpm.cjs', 'exec', 'vitest', 'run'],
|
|
})
|
|
expect(gates[1]?.env ?? {}).not.toHaveProperty('DSH_COVERAGE_TEST_TIMEOUT_MS')
|
|
}
|
|
})
|
|
|
|
it('selects partitioned coverage only when explicitly configured', () => {
|
|
const coverage = withEnv('DSH_COVERAGE_PARTITIONS', '3', () =>
|
|
withPnpmEntrypoint(() => gatesForMode('ci-windows-complete').find(subject => subject.id === 'coverage')))
|
|
|
|
expect(coverage).toMatchObject({
|
|
displayCommand: 'DSH_COVERAGE_PARTITIONS=3 pnpm run test:coverage:partitioned',
|
|
args: ['/private/pnpm.cjs', 'run', 'test:coverage:partitioned'],
|
|
env: { DSH_COVERAGE_EXEMPT_HEAVY: '1' },
|
|
streamOutput: true,
|
|
})
|
|
})
|
|
|
|
it('rejects an invalid coverage partition count before starting a gate', () => {
|
|
expect(() => withEnv('DSH_COVERAGE_PARTITIONS', '1', () =>
|
|
withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))))
|
|
.toThrow('DSH_COVERAGE_PARTITIONS must be an integer greater than 1')
|
|
})
|
|
|
|
it.each([
|
|
['empty', [], /gate graph has no gates/],
|
|
['duplicate ids', [gate('same'), gate('same')], /duplicate gate id "same"/],
|
|
['unknown dependencies', [gate('subject', { needs: ['missing'] })], /depends on unknown gate "missing"/],
|
|
['unknown ordering predecessors', [gate('subject', { after: ['missing'] })], /waits for unknown gate "missing"/],
|
|
['cycles', [gate('first', { needs: ['second'] }), gate('second', { needs: ['first'] })], /dependency cycle: first -> second -> first/],
|
|
['mixed cycles', [gate('first', { after: ['second'] }), gate('second', { needs: ['first'] })], /dependency cycle: first -> second -> first/],
|
|
] as const)('rejects %s before starting a child', async (_label, invalid, message) => {
|
|
const execute = vi.fn(async (subject: Gate) => resultFor(subject))
|
|
|
|
await expect(runGates([...invalid], 1, execute)).rejects.toThrow(message)
|
|
expect(execute).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects an invalid worker count before starting a child', async () => {
|
|
const execute = vi.fn(async (subject: Gate) => resultFor(subject))
|
|
|
|
await expect(runGates([gate('subject')], 0, execute)).rejects.toThrow('max concurrency must be a positive integer')
|
|
expect(execute).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('skips dependents after their prerequisite fails', async () => {
|
|
const dependent = gate('dependent', { needs: ['root'] })
|
|
const root = gate('root')
|
|
const execute = vi.fn(async (subject: Gate) => resultFor(subject, 'failed'))
|
|
|
|
const results = await runGates([dependent, root], 1, execute)
|
|
|
|
expect(execute).toHaveBeenCalledOnce()
|
|
expect(execute).toHaveBeenCalledWith(root, undefined)
|
|
expect(results[0]).toMatchObject({ gate: dependent, status: 'skipped', error: 'dependency failed or skipped: root' })
|
|
})
|
|
|
|
it('runs an ordered follower after its predecessor fails', async () => {
|
|
const follower = gate('follower', { after: ['root'] })
|
|
const root = gate('root')
|
|
const execute = vi.fn(async (subject: Gate) => resultFor(subject, subject === root ? 'failed' : 'passed'))
|
|
|
|
const results = await runGates([follower, root], 2, execute)
|
|
|
|
expect(execute.mock.calls.map(([subject]) => subject.id)).toEqual(['root', 'follower'])
|
|
expect(results.map(result => result.status)).toEqual(['passed', 'failed'])
|
|
})
|
|
|
|
it('runs an ordered follower after its predecessor is skipped', async () => {
|
|
const follower = gate('follower', { after: ['dependent'] })
|
|
const dependent = gate('dependent', { needs: ['root'] })
|
|
const root = gate('root')
|
|
const execute = vi.fn(async (subject: Gate) => resultFor(subject, subject === root ? 'failed' : 'passed'))
|
|
|
|
const results = await runGates([follower, dependent, root], 2, execute)
|
|
|
|
expect(execute.mock.calls.map(([subject]) => subject.id)).toEqual(['root', 'follower'])
|
|
expect(results.map(result => result.status)).toEqual(['passed', 'skipped', 'failed'])
|
|
})
|
|
})
|
|
|
|
describe('Oxlint gate', () => {
|
|
it('uses the package script when no worker bound is configured', () => {
|
|
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
|
|
withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready')[0]))
|
|
|
|
expect(subject).toMatchObject({
|
|
id: 'lint',
|
|
displayCommand: 'pnpm run lint:contracts-ready',
|
|
command: process.execPath,
|
|
args: ['/private/pnpm.cjs', 'run', 'lint:contracts-ready'],
|
|
})
|
|
})
|
|
|
|
it('surfaces the configured worker bound on the shared package script', () => {
|
|
const subject = withEnv('DSH_OXLINT_THREADS', '4', () =>
|
|
withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready')[0]))
|
|
|
|
expect(subject).toMatchObject({
|
|
id: 'lint',
|
|
displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint:contracts-ready',
|
|
command: process.execPath,
|
|
args: ['/private/pnpm.cjs', 'run', 'lint:contracts-ready'],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Typert contract preparation', () => {
|
|
it('prepares primary source consumers once before they run', () => {
|
|
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
|
|
withPnpmEntrypoint(() => gatesForMode('ci-primary')))
|
|
|
|
expect(subject.find(item => item.id === 'typert-contracts')).toMatchObject({
|
|
displayCommand: 'pnpm exec tsx scripts/compile-referenced-projects.ts host-contracts',
|
|
args: ['/private/pnpm.cjs', 'exec', 'tsx', 'scripts/compile-referenced-projects.ts', 'host-contracts'],
|
|
})
|
|
for (const [id, script] of [
|
|
['typecheck', 'typecheck:contracts-ready'],
|
|
['lint', 'lint:contracts-ready'],
|
|
['doc-typecheck', 'doc-typecheck:contracts-ready'],
|
|
] as const) {
|
|
expect(subject.find(item => item.id === id)).toMatchObject({
|
|
displayCommand: `pnpm run ${script}`,
|
|
args: ['/private/pnpm.cjs', 'run', script],
|
|
needs: ['typert-contracts'],
|
|
})
|
|
}
|
|
expect(subject.find(item => item.id === 'build')?.needs).toEqual([
|
|
'typecheck',
|
|
'lint',
|
|
'doc-typecheck',
|
|
])
|
|
})
|
|
|
|
it('reuses contracts from the validated consumer build', () => {
|
|
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
|
|
|
|
expect(subject.find(item => item.id === 'lint-and-duplication')).toMatchObject({
|
|
displayCommand: 'pnpm run check:ci:lint:contracts-ready',
|
|
args: ['/private/pnpm.cjs', 'run', 'check:ci:lint:contracts-ready'],
|
|
})
|
|
expect(subject.find(item => item.id === 'doc-typecheck')).toMatchObject({
|
|
displayCommand: 'pnpm run doc-typecheck:contracts-ready',
|
|
args: ['/private/pnpm.cjs', 'run', 'doc-typecheck:contracts-ready'],
|
|
})
|
|
})
|
|
|
|
it('keeps standalone doc sync responsible for preparation', () => {
|
|
const docTypecheck = withPnpmEntrypoint(() =>
|
|
gatesForMode('doc-sync').find(item => item.id === 'doc-typecheck'))
|
|
|
|
expect(docTypecheck?.displayCommand).toBe('pnpm run doc-typecheck')
|
|
})
|
|
})
|
|
|
|
describe('Node compatibility graph', () => {
|
|
it.each(['22.19.0', '24.9.0', '26.0.0'])('runs all source compatibility smokes serially on Node %s', (version) => {
|
|
const subject = withNodeVersion(version, () =>
|
|
withPnpmEntrypoint(() => gatesForMode('node-compat')))
|
|
|
|
expect(subject.filter(item => item.id === 'source-compat-smokes')).toMatchObject([{
|
|
label: 'source compatibility smokes',
|
|
args: [
|
|
'/private/pnpm.cjs',
|
|
'exec',
|
|
'vitest',
|
|
'run',
|
|
'packages/workflow/workflow-ptc/tests/source-runtime.compat.spec.ts',
|
|
'packages/session/session-persistence-jsonl/tests/zstd.compat.spec.ts',
|
|
'apps/cli/tests/source-launch.compat.spec.ts',
|
|
'scripts/vitest-environment.compat.spec.ts',
|
|
'packages/boot/app-boot/tests/profile-resolution.spec.ts',
|
|
'packages/boot/app-boot/tests/profile-resolution-service.spec.ts',
|
|
'packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts',
|
|
'--no-file-parallelism',
|
|
'--maxWorkers=1',
|
|
],
|
|
env: { VITEST_MAX_WORKERS: '1' },
|
|
}])
|
|
})
|
|
|
|
it.each(['1', '8'])('keeps source compatibility smokes serial with inherited VITEST_MAX_WORKERS=%s', (workers) => {
|
|
const subject = withEnv('VITEST_MAX_WORKERS', workers, () =>
|
|
withPnpmEntrypoint(() => gatesForMode('node-compat')))
|
|
|
|
expect(subject.find(item => item.id === 'source-compat-smokes')?.env).toEqual({ VITEST_MAX_WORKERS: '1' })
|
|
})
|
|
|
|
it.each([undefined, '1'])('builds the complete Node 22 artifacts once with skip-typecheck=%s', (skipTypecheck) => {
|
|
const subject = withNodeVersion('22.19.0', () =>
|
|
withEnv('DSH_NODE_COMPAT_SKIP_TYPECHECK', skipTypecheck, () =>
|
|
withPnpmEntrypoint(() => gatesForMode('node-compat'))))
|
|
|
|
expect(subject.filter(item => item.displayCommand === 'pnpm run build:artifacts')).toHaveLength(1)
|
|
expect(subject.find(item => item.id === 'build')?.args).toEqual([
|
|
'/private/pnpm.cjs', 'run', 'build:artifacts',
|
|
])
|
|
expect(subject.map(item => item.id)).not.toContain('build:web')
|
|
expect(subject.find(item => item.id === 'cli-lazy-search-startup-smoke')).toMatchObject({
|
|
needs: ['build'],
|
|
env: { DSH_REQUIRE_BUILT_CLI_SMOKE: '1' },
|
|
})
|
|
})
|
|
|
|
it.each(['passed', 'failed'] as const)('runs the Node 22 lazy-search smoke only after a passed build (%s)', async (buildStatus) => {
|
|
const subject = withNodeVersion('22.19.0', () =>
|
|
withEnv('DSH_NODE_COMPAT_SKIP_TYPECHECK', '1', () =>
|
|
withPnpmEntrypoint(() => gatesForMode('node-compat'))))
|
|
let buildCompleted = false
|
|
let smokeStarted = false
|
|
const results = await runGates(subject, 2, async (item) => {
|
|
if (item.id === 'build') {
|
|
buildCompleted = true
|
|
return resultFor(item, buildStatus)
|
|
}
|
|
if (item.id === 'cli-lazy-search-startup-smoke') {
|
|
expect(buildCompleted).toBe(true)
|
|
smokeStarted = true
|
|
}
|
|
return resultFor(item)
|
|
})
|
|
|
|
expect(smokeStarted).toBe(buildStatus === 'passed')
|
|
expect(results.find(item => item.gate.id === 'cli-lazy-search-startup-smoke')?.status)
|
|
.toBe(buildStatus === 'passed' ? 'passed' : 'skipped')
|
|
})
|
|
})
|
|
|
|
describe('Node 24 lane ownership', () => {
|
|
it('keeps the static lane source-only', () => {
|
|
const subject = withPnpmEntrypoint(() => gatesForMode('ci-static'))
|
|
|
|
expect(subject.map(item => item.id)).not.toContain('build')
|
|
expect(subject.map(item => item.id)).not.toContain('doc-typecheck')
|
|
})
|
|
|
|
it('owns the build and orders its artifact consumers', () => {
|
|
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
|
|
|
|
expect(defaultConcurrency('ci-consumers', subject.length, 4, { CI: 'true' })).toEqual({
|
|
workers: 4,
|
|
source: '4 available CPU(s)',
|
|
})
|
|
expect(subject.map(item => item.id)).toEqual([
|
|
'build',
|
|
'node-compat',
|
|
'publint',
|
|
'lint-and-duplication',
|
|
'snapshot',
|
|
'expected-output',
|
|
'web-snapshot',
|
|
'doc-typecheck',
|
|
'node-next-types',
|
|
'built-bin-smoke',
|
|
])
|
|
expect(subject.find(item => item.id === 'publint')?.needs).toEqual(['build'])
|
|
expect(subject.find(item => item.id === 'build')?.env).toEqual({
|
|
DSH_BUILD_CLIENT_PROFILE: 'official',
|
|
})
|
|
expect(subject.find(item => item.id === 'node-compat')?.env).toEqual({
|
|
DSH_BUILD_CLIENT_PROFILE: 'official',
|
|
})
|
|
expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['build'])
|
|
for (const id of [
|
|
'snapshot',
|
|
'expected-output',
|
|
'web-snapshot',
|
|
'doc-typecheck',
|
|
'node-next-types',
|
|
'built-bin-smoke',
|
|
]) {
|
|
expect(subject.find(item => item.id === id)?.needs).toEqual(['build'])
|
|
}
|
|
expect(subject.find(item => item.id === 'snapshot')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
|
|
expect(subject.find(item => item.id === 'expected-output')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
|
|
expect(subject.find(item => item.id === 'built-bin-smoke')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
|
|
expect(subject.find(item => item.id === 'doc-typecheck')?.env).toEqual({
|
|
DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1',
|
|
})
|
|
expect(subject.find(item => item.id === 'built-bin-smoke')?.args).toEqual(
|
|
expect.arrayContaining([
|
|
'apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts',
|
|
'apps/desktop/tests/acl-skill.built.e2e.ts',
|
|
'packages/subprocess/subprocess-local/tests/spawn-runner-built.e2e.ts',
|
|
'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
|
|
'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
|
|
'packages/experimental/agent-team/tests/built-lib.e2e.ts',
|
|
]),
|
|
)
|
|
expect(subject.find(item => item.id === 'web-snapshot')).toMatchObject({
|
|
displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
|
|
env: { DSH_SNAPSHOT: 'replay' },
|
|
after: [
|
|
'publint',
|
|
'lint-and-duplication',
|
|
'snapshot',
|
|
'expected-output',
|
|
'doc-typecheck',
|
|
'node-next-types',
|
|
'built-bin-smoke',
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Linux primary graph', () => {
|
|
it('adds the same compare-only web gate after built client artifacts', () => {
|
|
const subject = withPnpmEntrypoint(() => gatesForMode('ci-linux-primary'))
|
|
const web = subject.find(item => item.id === 'web-snapshot')
|
|
|
|
expect(web).toMatchObject({
|
|
displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
|
|
env: { DSH_SNAPSHOT: 'replay' },
|
|
needs: ['build'],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('gate process outcomes', () => {
|
|
it('streams selected gate output without retaining it', async () => {
|
|
const write = vi.spyOn(process.stdout, 'write').mockReturnValue(true)
|
|
try {
|
|
const result = await runGate(gate('streamed', {
|
|
args: ['-e', "process.stdout.write('live output')"],
|
|
streamOutput: true,
|
|
}))
|
|
|
|
expect(result.status).toBe('passed')
|
|
expect(result.output).toEqual([])
|
|
expect(write).toHaveBeenCalledWith('live output')
|
|
} finally {
|
|
write.mockRestore()
|
|
}
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')('reports signal termination independently from exit status', async () => {
|
|
const result = await runGate(gate('terminated', {
|
|
args: ['-e', "process.kill(process.pid, 'SIGTERM')"],
|
|
}))
|
|
|
|
expect(result.status).toBe('failed')
|
|
expect(result.exitCode).toBeNull()
|
|
expect(result.signalCode).toBe('SIGTERM')
|
|
expect(formatGateResultReason(result)).toBe('signal SIGTERM')
|
|
})
|
|
})
|
|
|
|
describe('fail-fast scheduling', () => {
|
|
it('aborts the aggregate at the first blocking failure', async () => {
|
|
const slow = gate('slow')
|
|
const fast = gate('fast')
|
|
const dependent = gate('dependent', { needs: ['slow'] })
|
|
const execute = vi.fn(async (subject: Gate, signal?: AbortSignal) => {
|
|
if (subject.id === 'fast') {
|
|
return new Promise<GateResult>((resolve) => {
|
|
signal?.addEventListener('abort', () => {
|
|
// The real runGate marks a gate the abort terminated; the drain
|
|
// must then record it skipped rather than keep the failure.
|
|
resolve({ ...resultFor(subject, 'failed'), aborted: true })
|
|
}, { once: true })
|
|
})
|
|
}
|
|
return resultFor(subject, subject.id === 'slow' ? 'failed' : 'passed')
|
|
})
|
|
|
|
const results = await runGates([slow, fast, dependent], 2, execute, () => {}, { failFast: true })
|
|
|
|
expect(execute.mock.calls.map(([subject]) => subject.id)).toEqual(['slow', 'fast'])
|
|
expect(results.map(result => result.status)).toEqual(['failed', 'skipped', 'skipped'])
|
|
expect(results[1]).toMatchObject({
|
|
status: 'skipped',
|
|
error: 'aborted by fail-fast: slow failed',
|
|
})
|
|
expect(results[2]).toMatchObject({
|
|
status: 'skipped',
|
|
error: 'aborted by fail-fast: slow failed',
|
|
})
|
|
})
|
|
|
|
it('does not abort on a non-blocking gate failure', async () => {
|
|
const observational = gate('observational', { allowFailure: true })
|
|
const root = gate('root')
|
|
const execute = vi.fn(async (subject: Gate) => (
|
|
resultFor(subject, subject.id === 'observational' ? 'failed' : 'passed')
|
|
))
|
|
|
|
const results = await runGates([observational, root], 2, execute, () => {}, { failFast: true })
|
|
|
|
expect(execute).toHaveBeenCalledTimes(2)
|
|
expect(results.map(result => result.status)).toEqual(['failed', 'passed'])
|
|
})
|
|
|
|
it('runs independent gates to completion when fail-fast is disabled', async () => {
|
|
const root = gate('root')
|
|
const sibling = gate('sibling')
|
|
const execute = vi.fn(async (subject: Gate) => (
|
|
resultFor(subject, subject.id === 'root' ? 'failed' : 'passed')
|
|
))
|
|
|
|
const results = await runGates([root, sibling], 2, execute, () => {}, { failFast: false })
|
|
|
|
expect(execute).toHaveBeenCalledTimes(2)
|
|
expect(results.map(result => result.status)).toEqual(['failed', 'passed'])
|
|
})
|
|
|
|
it('kills the child when the abort signal fires', async () => {
|
|
const controller = new AbortController()
|
|
const promise = runGate(gate('killable', { args: ['-e', 'setInterval(() => {}, 1000)'] }), controller.signal)
|
|
controller.abort()
|
|
const result = await promise
|
|
|
|
expect(result.status).toBe('failed')
|
|
expect(result.aborted).toBe(true)
|
|
if (process.platform !== 'win32') expect(result.signalCode).toBe('SIGTERM')
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')('marks a zero-exit child as aborted when the signal fired', async () => {
|
|
const marker = 'signal-trap-armed'
|
|
const armed = Promise.withResolvers<undefined>()
|
|
const controller = new AbortController()
|
|
let promise: Promise<GateResult> | undefined
|
|
const { write } = captureStreamedOutput((chunks) => {
|
|
if (chunks.join('').split('\n').slice(0, -1).includes(marker)) armed.resolve(undefined)
|
|
})
|
|
const cleanup = async (): Promise<void> => {
|
|
controller.abort()
|
|
try { await promise } finally { write.mockRestore() }
|
|
}
|
|
onTestFinished(cleanup)
|
|
try {
|
|
// Readiness belongs after native signal-handler registration.
|
|
const script = [
|
|
"process.on('SIGTERM', () => process.exit(0))",
|
|
`process.stdout.write(${JSON.stringify(`${marker}\n`)})`,
|
|
'setInterval(() => {}, 1000)',
|
|
].join(';')
|
|
const child = gate('traps-signal', { args: ['-e', script], streamOutput: true })
|
|
promise = runGate(child, controller.signal)
|
|
await Promise.race([
|
|
armed.promise,
|
|
promise.then((result) => { throw new Error(`signal fixture exited before readiness: ${formatGateResultReason(result)}`) }),
|
|
])
|
|
controller.abort()
|
|
const result = await promise
|
|
|
|
// The child trapped the signal and exited zero; the drain must not
|
|
// report this gate passed, so the raw outcome carries the abort mark.
|
|
expect(result, formatGateResultReason(result)).toMatchObject({
|
|
status: 'passed', exitCode: 0, signalCode: null, aborted: true,
|
|
})
|
|
} finally {
|
|
await cleanup()
|
|
}
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')('kills the whole gate process tree when the abort signal fires', async () => {
|
|
const { writes, write } = captureStreamedOutput()
|
|
const controller = new AbortController()
|
|
let promise: Promise<GateResult> | undefined
|
|
try {
|
|
const script = [
|
|
"const { spawn } = require('node:child_process')",
|
|
// Detached, so the grandchild leads its own process group: the gate
|
|
// group signal cannot reach it, and only the descendant enumeration in
|
|
// treeKill does — the shape of a nested run-gates' leaf gates.
|
|
"const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true })",
|
|
"process.stdout.write('grandchild:' + grandchild.pid + '\\n')",
|
|
'setInterval(() => {}, 1000)',
|
|
].join(';')
|
|
promise = runGate(gate('tree', { args: ['-e', script], streamOutput: true }), controller.signal)
|
|
const deadline = Date.now() + 5000
|
|
let pid: number | undefined
|
|
while (pid === undefined && Date.now() < deadline) {
|
|
const match = writes.join('').match(/grandchild:(\d+)/)
|
|
if (match !== null) pid = Number(match[1])
|
|
else await new Promise(resolve => setTimeout(resolve, 20))
|
|
}
|
|
expect(pid ?? 0).toBeGreaterThan(0)
|
|
controller.abort()
|
|
const result = await promise
|
|
expect(result.status).toBe('failed')
|
|
// The descendant enumeration signals the detached grandchild at the same
|
|
// time as the group signal reaches the direct child; the direct child's
|
|
// own death closes the gate pipes, so poll for the grandchild to stop
|
|
// executing rather than asserting on a fixed instant.
|
|
const stopDeadline = Date.now() + 5000
|
|
while (!procStopped(pid!) && Date.now() < stopDeadline) {
|
|
await new Promise(resolve => setTimeout(resolve, 20))
|
|
}
|
|
expect(procStopped(pid!)).toBe(true)
|
|
} finally {
|
|
// A failed wait or assertion must not leave the forever-looping detached
|
|
// grandchild behind on the host: abort the gate and wait for the
|
|
// process tree to settle before restoring the spy.
|
|
controller.abort()
|
|
await promise
|
|
write.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('forwards host interruption signals to the abort path', async () => {
|
|
const slow = gate('slow')
|
|
const sibling = gate('sibling')
|
|
const execute = vi.fn(async (subject: Gate, signal?: AbortSignal) => {
|
|
if (subject.id === 'slow') {
|
|
return new Promise<GateResult>((resolve) => {
|
|
signal?.addEventListener('abort', () => {
|
|
// A child can trap the signal and exit zero; the drain must still
|
|
// record the gate skipped so the interrupted run fails.
|
|
resolve({ ...resultFor(subject, 'passed'), aborted: true })
|
|
}, { once: true })
|
|
})
|
|
}
|
|
return resultFor(subject)
|
|
})
|
|
|
|
const promise = runGates([slow, sibling], 1, execute, () => {}, { failFast: true, forwardProcessSignals: true })
|
|
// The first loop iteration starts `slow` synchronously, so its abort
|
|
// listener is registered before the signal is emitted.
|
|
process.emit('SIGTERM')
|
|
const results = await promise
|
|
|
|
expect(execute).toHaveBeenCalledOnce()
|
|
expect(results.map(result => result.status)).toEqual(['skipped', 'skipped'])
|
|
expect(results[0]).toMatchObject({
|
|
status: 'skipped',
|
|
error: 'aborted by fail-fast: host interruption',
|
|
})
|
|
})
|
|
|
|
it('pairs host signal forwarding with fail-fast at the CLI entrypoint', () => {
|
|
expect(cliGateOptions(true)).toEqual({ failFast: true, forwardProcessSignals: true })
|
|
expect(cliGateOptions(false)).toEqual({ failFast: false, forwardProcessSignals: false })
|
|
})
|
|
|
|
it('rejects host signal forwarding without fail-fast', async () => {
|
|
const execute = vi.fn(async (subject: Gate) => resultFor(subject))
|
|
|
|
await expect(runGates([gate('subject')], 1, execute, () => {}, { forwardProcessSignals: true }))
|
|
.rejects.toThrow('forwardProcessSignals requires failFast')
|
|
expect(execute).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('leaves an un-aborted child running to completion', async () => {
|
|
const result = await runGate(gate('settles', { args: ['-e', ''] }), new AbortController().signal)
|
|
|
|
expect(result.status).toBe('passed')
|
|
expect(result.aborted).toBe(false)
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')('kills a detached descendant that outlived the child when the abort arrives later', async () => {
|
|
const writes: string[] = []
|
|
const write = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
writes.push(String(chunk))
|
|
return true
|
|
})
|
|
const controller = new AbortController()
|
|
let promise: Promise<GateResult> | undefined
|
|
try {
|
|
const script = [
|
|
"const { spawn } = require('node:child_process')",
|
|
// Detached with inherited stdio: the grandchild leads its own process
|
|
// group (the gate group signal misses it) and holds the gate's
|
|
// stdout write end (so `close` stays pending past the child exit).
|
|
"const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true, stdio: 'inherit' })",
|
|
"process.stdout.write('grandchild:' + grandchild.pid + '\\n')",
|
|
// Outlive the first descendant-sampler tick with margin so the cache
|
|
// holds the grandchild even on a loaded runner, then exit normally
|
|
// before the abort arrives.
|
|
"setTimeout(() => { process.stdout.write('child-exit\\n'); process.exit(0) }, 8000)",
|
|
].join(';')
|
|
promise = runGate(gate('late-abort', { args: ['-e', script], streamOutput: true }), controller.signal)
|
|
const pid = await waitForGrandchildPid(writes, 'child-exit', Date.now() + 10000)
|
|
// terminate must not re-enumerate over the sampler cache now that the
|
|
// child is gone; the detached grandchild is killed from the cached list.
|
|
await abortAndExpectTreeStopped(promise, controller, pid)
|
|
} finally {
|
|
// A failed wait or assertion must not leave the forever-looping detached
|
|
// grandchild behind on the host: abort the gate and wait for the
|
|
// process tree to settle before restoring the spy.
|
|
controller.abort()
|
|
await promise
|
|
write.mockRestore()
|
|
}
|
|
}, 20000)
|
|
|
|
it.skipIf(process.platform === 'win32')('keeps a reparented detached descendant tracked across a sampler tick', async () => {
|
|
const { writes, write } = captureStreamedOutput()
|
|
const controller = new AbortController()
|
|
let promise: Promise<GateResult> | undefined
|
|
try {
|
|
const script = [
|
|
"const { spawn } = require('node:child_process')",
|
|
// Wrapper spawns a detached grandchild with inherited stdio (its own
|
|
// process group, holding the gate's stdout write end), prints the pid,
|
|
// then exits after 7 seconds — after the first sampler tick, before
|
|
// the second. From then on the grandchild is reparented and
|
|
// unreachable by parent id.
|
|
"const wrapper = spawn(process.execPath, ['-e', \"const { spawn } = require('node:child_process'); const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true, stdio: 'inherit' }); process.stdout.write('grandchild:' + grandchild.pid + '\\\\n'); setTimeout(() => process.exit(0), 7000)\"], { stdio: 'inherit' })",
|
|
"wrapper.on('exit', () => process.stdout.write('wrapper-exited\\n'))",
|
|
// Keep the root child alive past the abort with a heartbeat so the
|
|
// test can abort while it is still running.
|
|
"setInterval(() => process.stdout.write('hb\\n'), 1000)",
|
|
].join(';')
|
|
promise = runGate(gate('sampler-merge', { args: ['-e', script], streamOutput: true }), controller.signal)
|
|
const pid = await waitForGrandchildPid(writes, 'wrapper-exited', Date.now() + 15000)
|
|
// Wait past the second sampler tick (t=10) with margin: a replacing tick
|
|
// would drop the reparented grandchild from the cache, after which the
|
|
// abort cannot reach it. The root child keeps running throughout.
|
|
const tickDeadline = Date.now() + 10000
|
|
const wrapperExitedAt = Date.now()
|
|
while (Date.now() - wrapperExitedAt < 5000 && Date.now() < tickDeadline) {
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
}
|
|
expect(Date.now() - wrapperExitedAt).toBeGreaterThanOrEqual(5000)
|
|
await abortAndExpectTreeStopped(promise, controller, pid)
|
|
} finally {
|
|
// A failed wait or assertion must not leave the forever-looping detached
|
|
// grandchild behind on the host: abort the gate and wait for the
|
|
// process tree to settle before restoring the spy.
|
|
controller.abort()
|
|
await promise
|
|
write.mockRestore()
|
|
}
|
|
}, 30000)
|
|
})
|
|
|
|
describe('process-table parsing', () => {
|
|
it('excludes the root when a parent link returns to it', () => {
|
|
expect(collectDescendants(100, [[200, 100], [100, 200], [300, 200]]))
|
|
.toEqual([200, 300])
|
|
})
|
|
|
|
it('visits duplicate and cyclic descendant links only once', () => {
|
|
expect(collectDescendants(100, [
|
|
[200, 100], [200, 100], [300, 100], [200, 200], [400, 200], [200, 400], [500, 300], [900, 800],
|
|
])).toEqual([200, 300, 400, 500])
|
|
})
|
|
|
|
it('returns no descendants for an isolated or self-parented root', () => {
|
|
expect(collectDescendants(100, [])).toEqual([])
|
|
expect(collectDescendants(100, [[100, 100]])).toEqual([])
|
|
})
|
|
|
|
it('walks a wide child set without spreading it into call arguments', () => {
|
|
const children = Array.from({ length: 150_000 }, (_, i): [number, number] => [i + 3, 2])
|
|
const descendants = collectDescendants(1, [[2, 1], ...children])
|
|
expect(descendants).toHaveLength(children.length + 1)
|
|
expect(descendants[0]).toBe(2)
|
|
expect(descendants.at(-1)).toBe(150_002)
|
|
})
|
|
|
|
it('parses `pid ppid` rows from a POSIX ps dump', () => {
|
|
expect(parsePidPpidLines(' 123 1\n456 123\n 789 456\n')).toEqual([[123, 1], [456, 123], [789, 456]])
|
|
})
|
|
|
|
it('parses Windows PowerShell Get-CimInstance output of the same shape', () => {
|
|
expect(parsePidPpidLines(' 123 1\r\n456 123\r\n')).toEqual([[123, 1], [456, 123]])
|
|
})
|
|
|
|
it('drops blank and malformed lines', () => {
|
|
expect(parsePidPpidLines(' 123 1\n\ncommand not found\n999 abc\n')).toEqual([[123, 1]])
|
|
})
|
|
})
|
|
|
|
describe('Windows tree termination', () => {
|
|
it('targets the root first and each captured descendant after it', () => {
|
|
expect(taskkillArgs(100, [201, 302, 403])).toEqual([
|
|
['/PID', '100', '/T', '/F'],
|
|
['/PID', '201', '/T', '/F'],
|
|
['/PID', '302', '/T', '/F'],
|
|
['/PID', '403', '/T', '/F'],
|
|
])
|
|
})
|
|
|
|
it('terminates the root alone when no descendant was captured', () => {
|
|
expect(taskkillArgs(100, [])).toEqual([['/PID', '100', '/T', '/F']])
|
|
})
|
|
})
|