Преглед изворни кода

fix(examples): run coverage subprocesses from built libs

imccyu пре 2 месеци
родитељ
комит
de19c6ca08

+ 3 - 0
packages/support/acp-snapshot/src/harness.ts

@@ -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: {

+ 4 - 2
packages/support/acp-snapshot/tests/harness.spec.ts

@@ -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)),
 }
 

+ 4 - 2
packages/support/acp-snapshot/tests/suite.spec.ts

@@ -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)),
 }
 

+ 15 - 8
packages/support/loader-smoke/src/index.ts

@@ -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,

+ 16 - 1
packages/support/loader-smoke/tests/example-launch.spec.ts

@@ -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/)
   })

+ 2 - 0
packages/support/loader-smoke/tests/loader-smoke.spec.ts

@@ -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,

+ 3 - 0
scripts/run-gates.ts

@@ -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'],
   })
 }