diff --git a/packages/support/acp-snapshot/src/harness.ts b/packages/support/acp-snapshot/src/harness.ts index 05d79d3330..e2072b6b17 100644 --- a/packages/support/acp-snapshot/src/harness.ts +++ b/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: { diff --git a/packages/support/acp-snapshot/tests/harness.spec.ts b/packages/support/acp-snapshot/tests/harness.spec.ts index 42ccbd7de4..1d953f5e95 100644 --- a/packages/support/acp-snapshot/tests/harness.spec.ts +++ b/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)), } diff --git a/packages/support/acp-snapshot/tests/suite.spec.ts b/packages/support/acp-snapshot/tests/suite.spec.ts index 89af6c6614..4cedc1cdfb 100644 --- a/packages/support/acp-snapshot/tests/suite.spec.ts +++ b/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)), } diff --git a/packages/support/loader-smoke/src/index.ts b/packages/support/loader-smoke/src/index.ts index f39c6d678c..edbe39d8ca 100644 --- a/packages/support/loader-smoke/src/index.ts +++ b/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 (`/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 (`/lib/.js`) from a source bin (`/src/.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 (`/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 { 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/) }) diff --git a/packages/support/loader-smoke/tests/loader-smoke.spec.ts b/packages/support/loader-smoke/tests/loader-smoke.spec.ts index 991a1a64af..b99d810188 100644 --- a/packages/support/loader-smoke/tests/loader-smoke.spec.ts +++ b/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, diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 378cafed90..ed575858a6 100644 --- a/scripts/run-gates.ts +++ b/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'], }) }