fix(examples): run coverage subprocesses from built libs

This commit is contained in:
imccyu
2026-07-17 21:10:04 +08:00
parent b4ab7debb7
commit de19c6ca08
7 changed files with 47 additions and 13 deletions

View File

@@ -33,6 +33,8 @@ import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
export interface AgentUnderTest {
/** The agent bin's SOURCE entry (e.g. `packages/examples/acp-demo/src/bin.ts`); the `lib` bin is derived from it. */
binScript: string
/** Explicit plain-Node entry for `lib` mode; intended for test fixtures outside a package `src/` tree. */
libBinScript?: string | undefined
/**
* The example's live `cordis.yml`. Under `DSH_SNAPSHOT=replay` the bin swaps
* it for the sibling `cordis.snapshot.yml` (the keyless replay overlay), so
@@ -181,6 +183,7 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise
// Node, resolving plugins through the example's workspace node_modules → lib.
const launch = resolveExampleLaunch({
srcBin: opts.agent.binScript,
libBin: opts.agent.libBinScript,
configArgs: ['--config', opts.configPath ?? opts.agent.configPath],
tsconfigPath: opts.agent.tsconfigPath,
env: {

View File

@@ -14,10 +14,12 @@ import { runScenario, type AgentUnderTest, type InputStep } from '../src/harness
* assertions read plain `rawStdout`.
*/
const fakeAgent = fileURLToPath(new URL('./fixtures/fake-acp-agent.ts', import.meta.url))
const AGENT: AgentUnderTest = {
binScript: fileURLToPath(new URL('./fixtures/fake-acp-agent.ts', import.meta.url)),
binScript: fakeAgent,
libBinScript: fakeAgent,
// The fake bin ignores its config argv; any real path documents the shape.
configPath: fileURLToPath(new URL('./fixtures/fake-acp-agent.ts', import.meta.url)),
configPath: fakeAgent,
tsconfigPath: fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url)),
}

View File

@@ -32,9 +32,11 @@ import {
* spec once with `ACP_SNAPSHOT_SPEC_BOOTSTRAP=1`, then review and commit the resulting tree.
*/
const fakeAgent = fileURLToPath(new URL('./fixtures/fake-acp-agent.ts', import.meta.url))
const AGENT = {
binScript: fileURLToPath(new URL('./fixtures/fake-acp-agent.ts', import.meta.url)),
configPath: fileURLToPath(new URL('./fixtures/fake-acp-agent.ts', import.meta.url)),
binScript: fakeAgent,
libBinScript: fakeAgent,
configPath: fakeAgent,
tsconfigPath: fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url)),
}

View File

@@ -17,7 +17,6 @@ import { spawn } from 'node:child_process'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
const DEFAULT_PROCESS_TIMEOUT_MS = 30_000
@@ -54,6 +53,8 @@ export function resolveExampleMode(raw: string | undefined = process.env[EXAMPLE
export interface ExampleLaunchOptions {
/** Absolute path to the example bin's TypeScript source entry (`<pkg>/src/bin.ts`); the `lib` bin is derived from it. */
readonly srcBin: string
/** Explicit plain-Node entry for `lib` mode; test fixtures may point this at Node-type-strippable TypeScript. */
readonly libBin?: string | undefined
/** Arguments passed after the bin — the config, positional (`[configPath]`) or flagged (`['--config', configPath]`). */
readonly configArgs?: readonly string[]
/** The mode to launch in; defaults to {@link resolveExampleMode} of the environment. */
@@ -78,11 +79,14 @@ export interface ExampleLaunch {
/** Derive the built-lib bin (`<pkg>/lib/<name>.js`) from a source bin (`<pkg>/src/<name>.ts`). */
function toLibBin(srcBin: string): string {
const marker = '/src/'
const cut = srcBin.lastIndexOf(marker)
if (cut === -1) throw new Error(`resolveExampleLaunch: expected a "/src/" segment in bin path ${JSON.stringify(srcBin)}.`)
const tail = srcBin.slice(cut + marker.length).replace(/\.ts$/, '.js')
return `${srcBin.slice(0, cut)}/lib/${tail}`
const markerLength = '/src/'.length
const cut = Math.max(srcBin.lastIndexOf('/src/'), srcBin.lastIndexOf('\\src\\'))
if (cut === -1) {
throw new Error(`resolveExampleLaunch: expected a "/src/" segment or Windows equivalent in bin path ${JSON.stringify(srcBin)}.`)
}
const separator = srcBin.slice(cut, cut + 1)
const tail = srcBin.slice(cut + markerLength).replace(/\.ts$/, '.js')
return `${srcBin.slice(0, cut)}${separator}lib${separator}${tail}`
}
/**
@@ -108,12 +112,12 @@ export function resolveExampleLaunch(options: ExampleLaunchOptions): ExampleLaun
if (options.tsconfigPath === undefined) {
throw new Error("resolveExampleLaunch: 'src' mode needs tsconfigPath for the workspace paths map.")
}
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
const tsxLoader = import.meta.resolve('tsx')
env.TSX_TSCONFIG_PATH = options.tsconfigPath
return { command: process.execPath, args: [...flags, '--import', tsxLoader, options.srcBin, ...configArgs], env }
}
return { command: process.execPath, args: [...flags, toLibBin(options.srcBin), ...configArgs], env }
return { command: process.execPath, args: [...flags, options.libBin ?? toLibBin(options.srcBin), ...configArgs], env }
}
/** Inputs that vary between real-Loader example smokes. */
@@ -124,6 +128,8 @@ export interface LoaderSmokeOptions {
readonly tempDirPrefix: string
/** Absolute stdio-agent bin SOURCE path (`<pkg>/src/bin.ts`); the `lib` bin is derived from it. */
readonly binScript: string
/** Explicit plain-Node entry for `lib` mode; intended for test fixtures outside a package `src/` tree. */
readonly libBinScript?: string | undefined
/** Absolute real Loader config path. */
readonly configPath: string
/** Absolute repo tsconfig path used for unbuilt workspace-package resolution (required in `src` mode). */
@@ -158,6 +164,7 @@ export async function runLoaderSmoke(options: LoaderSmokeOptions): Promise<Loade
const processTimeoutMs = options.processTimeoutMs ?? DEFAULT_PROCESS_TIMEOUT_MS
const launch = resolveExampleLaunch({
srcBin: options.binScript,
libBin: options.libBinScript,
configArgs: [options.configPath],
...options.mode !== undefined ? { mode: options.mode } : {},
tsconfigPath: options.tsconfigPath,

View File

@@ -16,7 +16,8 @@ afterEach(() => {
describe('resolveExampleMode', () => {
it('defaults absent/empty/src to src', () => {
expect(resolveExampleMode(undefined)).toBe('src')
Reflect.deleteProperty(process.env, EXAMPLE_MODE_ENV)
expect(resolveExampleMode()).toBe('src')
expect(resolveExampleMode('')).toBe('src')
expect(resolveExampleMode('src')).toBe('src')
})
@@ -71,6 +72,12 @@ describe('resolveExampleLaunch', () => {
expect(env.DSH_HOME).toBe('/tmp/home')
})
it('lib mode: uses an explicit plain-Node bin when provided', () => {
const fixture = '/repo/packages/support/loader-smoke/tests/fixture.ts'
const { args } = resolveExampleLaunch({ srcBin: fixture, libBin: fixture, mode: 'lib' })
expect(args).toContain(fixture)
})
it('prepends --expose-internals when requested', () => {
const { args } = resolveExampleLaunch({ srcBin: SRC_BIN, mode: 'lib', exposeInternals: true })
expect(args[0]).toBe('--expose-internals')
@@ -84,6 +91,14 @@ describe('resolveExampleLaunch', () => {
expect(args).toContain('/repo/src/packages/examples/acp-demo/lib/bin.js')
})
it('lib mode: derives the built bin from a Windows source path', () => {
const { args } = resolveExampleLaunch({
srcBin: String.raw`D:\repo\src\packages\examples\acp-demo\src\bin.ts`,
mode: 'lib',
})
expect(args).toContain(String.raw`D:\repo\src\packages\examples\acp-demo\lib\bin.js`)
})
it('lib mode: throws when the bin has no /src/ segment', () => {
expect(() => resolveExampleLaunch({ srcBin: '/repo/lib/bin.js', mode: 'lib' })).toThrow(/"\/src\/" segment/)
})

View File

@@ -44,6 +44,7 @@ describe('runLoaderSmoke', () => {
label: 'failure fixture',
tempDirPrefix: 'loader-smoke-fail-',
binScript: fixture('fail'),
libBinScript: fixture('fail'),
configPath,
tsconfigPath,
})).rejects.toThrow('failure fixture exited 7. stdout:\n\nstderr:\nfixture failed')
@@ -54,6 +55,7 @@ describe('runLoaderSmoke', () => {
label: 'hanging fixture',
tempDirPrefix: 'loader-smoke-hang-',
binScript: fixture('hang'),
libBinScript: fixture('hang'),
configPath,
tsconfigPath,
processTimeoutMs: 100,

View File

@@ -163,6 +163,7 @@ function gatesForMode(selected: Mode): Gate[] {
]
case 'ci-coverage':
return [
pnpmScript('build', 'build'),
coverageGate(),
]
case 'ci-snapshot':
@@ -282,6 +283,8 @@ function coverageGate(): Gate {
...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'),
], {
label: 'test:coverage',
env: { DSH_EXAMPLE_MODE: 'lib' },
needs: ['build'],
})
}