From 4b7a84533f7223274eaca3f5ba59ad00433e0e4d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 20:26:29 +0000 Subject: [PATCH] test(cli): three e2e spawners name the entrypoint they actually reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The identical `bin/run.js` — the SHIPPED entrypoint` comment in serve-mcp-stdio-answers, serve-mcp-capability-collision and serve-stdio-stdout-purity was false: all three pin NODE_ENV=development on the child for the --dev admin seed, and @oclif/core 4.13.3 reroutes command resolution from ./dist/commands to ./src/commands for exactly that value, so packages/cli/dist was never consulted. Spawn bin/run-dev.js through tsx like the ~20 sibling e2e files here, and replace the comment with the mechanism. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR --- ...serve-mcp-capability-collision.e2e.test.ts | 27 ++++++++++++++++--- .../test/serve-mcp-stdio-answers.e2e.test.ts | 27 ++++++++++++++++--- .../serve-stdio-stdout-purity.e2e.test.ts | 27 ++++++++++++++++--- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/packages/cli/test/serve-mcp-capability-collision.e2e.test.ts b/packages/cli/test/serve-mcp-capability-collision.e2e.test.ts index 311d6f4b39..349f1aa05b 100644 --- a/packages/cli/test/serve-mcp-capability-collision.e2e.test.ts +++ b/packages/cli/test/serve-mcp-capability-collision.e2e.test.ts @@ -48,11 +48,27 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { E2E_SECRET_KEY, childEnv, randomPort } from './helpers/serve-process.js'; +import { E2E_SECRET_KEY, TSX, childEnv, randomPort } from './helpers/serve-process.js'; const HERE = resolve(fileURLToPath(import.meta.url), '..'); -/** `bin/run.js` — the SHIPPED entrypoint, i.e. the one the card's repro names. */ -const CLI = resolve(HERE, '../bin/run.js'); +/** + * `bin/run-dev.js` through `tsx` — the SOURCE entrypoint, like the ~20 sibling + * e2e files in this directory. + * + * ⛔ NOT `bin/run.js`. This file used to spell that one and call it "the SHIPPED + * entrypoint"; the claim was never true here (#11317). The boot below pins + * `NODE_ENV=development` on the child for the `--dev` admin seed, and + * @oclif/core 4.13.3 skips its TypeScript path lookup only when `isProd()` — + * `!['development', 'test'].includes(process.env.NODE_ENV ?? '')`. Under that + * value oclif rewrites the command target from the declared `./dist/commands` + * to `./src/commands` and transpiles, so `packages/cli/dist` is never consulted + * whichever stub is named. `serve-node-env-production-default.e2e.test.ts` is + * the file that genuinely reaches the built artifact, and it gets there by + * leaving `NODE_ENV` UNSET — the value that disables the reroute. Restoring + * `bin/run.js` here without dropping the `NODE_ENV` pin below is a no-op with a + * false comment attached. + */ +const CLI = resolve(HERE, '../bin/run-dev.js'); /** The consumer's real identity — see `serve-capability-identity.test.ts`. */ const CONSUMER_PLUGIN_ID = 'com.objectstack.connector.mcp'; @@ -106,7 +122,7 @@ const children: ChildProcessWithoutNullStreams[] = []; function boot(env: Record, waitFor: RegExp): Promise { return new Promise((resolveBoot, rejectBoot) => { - const child = spawn(process.execPath, [CLI, 'serve', '-p', port, '--dev'], { + const child = spawn(TSX, [CLI, 'serve', '-p', port, '--dev'], { cwd: dir, stdio: ['pipe', 'pipe', 'pipe'], // `childEnv`, not a bare `...process.env`: the vitest worker exports @@ -127,6 +143,9 @@ function boot(env: Record, waitFor: RegExp): Promise OS_SECRET_KEY: E2E_SECRET_KEY, // The dev-admin seed the key mint signs in as is gated on this, and // vitest exports `test`. + // Still passed explicitly although `bin/run-dev.js` assigns it too: the + // shim's assignment runs after its own static imports have evaluated, + // so only the child env pins the value for the whole process lifetime. NODE_ENV: 'development', ...env, }), diff --git a/packages/cli/test/serve-mcp-stdio-answers.e2e.test.ts b/packages/cli/test/serve-mcp-stdio-answers.e2e.test.ts index eb3bd9f5bf..cf081b401b 100644 --- a/packages/cli/test/serve-mcp-stdio-answers.e2e.test.ts +++ b/packages/cli/test/serve-mcp-stdio-answers.e2e.test.ts @@ -47,11 +47,27 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { E2E_SECRET_KEY, childEnv, randomPort } from './helpers/serve-process.js'; +import { E2E_SECRET_KEY, TSX, childEnv, randomPort } from './helpers/serve-process.js'; const HERE = resolve(fileURLToPath(import.meta.url), '..'); -/** `bin/run.js` — the SHIPPED entrypoint, i.e. the one the card's repro names. */ -const CLI = resolve(HERE, '../bin/run.js'); +/** + * `bin/run-dev.js` through `tsx` — the SOURCE entrypoint, like the ~20 sibling + * e2e files in this directory. + * + * ⛔ NOT `bin/run.js`. This file used to spell that one and call it "the SHIPPED + * entrypoint"; the claim was never true here (#11317). The boot below pins + * `NODE_ENV=development` on the child for the `--dev` admin seed, and + * @oclif/core 4.13.3 skips its TypeScript path lookup only when `isProd()` — + * `!['development', 'test'].includes(process.env.NODE_ENV ?? '')`. Under that + * value oclif rewrites the command target from the declared `./dist/commands` + * to `./src/commands` and transpiles, so `packages/cli/dist` is never consulted + * whichever stub is named. `serve-node-env-production-default.e2e.test.ts` is + * the file that genuinely reaches the built artifact, and it gets there by + * leaving `NODE_ENV` UNSET — the value that disables the reroute. Restoring + * `bin/run.js` here without dropping the `NODE_ENV` pin below is a no-op with a + * false comment attached. + */ +const CLI = resolve(HERE, '../bin/run-dev.js'); const CONFIG = ` export default { @@ -101,7 +117,7 @@ interface Booted { */ function boot(env: Record, waitFor: RegExp): Promise { return new Promise((resolveBoot, rejectBoot) => { - const child = spawn(process.execPath, [CLI, 'serve', '-p', port, '--dev'], { + const child = spawn(TSX, [CLI, 'serve', '-p', port, '--dev'], { cwd: dir, stdio: ['pipe', 'pipe', 'pipe'], // `childEnv`, not a bare `...process.env`: the vitest worker exports @@ -123,6 +139,9 @@ function boot(env: Record, waitFor: RegExp): Promise // Explicit, not inherited: the dev-admin seed this fixture signs in as // is hard-gated on `NODE_ENV === 'development'`, and vitest exports // `test`, which would leave the DB user-less and the mint unauthorized. + // Still passed explicitly although `bin/run-dev.js` assigns it too: the + // shim's assignment runs after its own static imports have evaluated, + // so only the child env pins the value for the whole process lifetime. NODE_ENV: 'development', ...env, }), diff --git a/packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts b/packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts index b557e0a81b..d4a299edcf 100644 --- a/packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts +++ b/packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts @@ -44,11 +44,27 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { E2E_SECRET_KEY, childEnv, randomPort } from './helpers/serve-process.js'; +import { E2E_SECRET_KEY, TSX, childEnv, randomPort } from './helpers/serve-process.js'; const HERE = resolve(fileURLToPath(import.meta.url), '..'); -/** `bin/run.js` — the SHIPPED entrypoint, i.e. the one the card's repro names. */ -const CLI = resolve(HERE, '../bin/run.js'); +/** + * `bin/run-dev.js` through `tsx` — the SOURCE entrypoint, like the ~20 sibling + * e2e files in this directory. + * + * ⛔ NOT `bin/run.js`. This file used to spell that one and call it "the SHIPPED + * entrypoint"; the claim was never true here (#11317). The boot below pins + * `NODE_ENV=development` on the child for the `--dev` admin seed, and + * @oclif/core 4.13.3 skips its TypeScript path lookup only when `isProd()` — + * `!['development', 'test'].includes(process.env.NODE_ENV ?? '')`. Under that + * value oclif rewrites the command target from the declared `./dist/commands` + * to `./src/commands` and transpiles, so `packages/cli/dist` is never consulted + * whichever stub is named. `serve-node-env-production-default.e2e.test.ts` is + * the file that genuinely reaches the built artifact, and it gets there by + * leaving `NODE_ENV` UNSET — the value that disables the reroute. Restoring + * `bin/run.js` here without dropping the `NODE_ENV` pin below is a no-op with a + * false comment attached. + */ +const CLI = resolve(HERE, '../bin/run-dev.js'); const CONFIG = ` export default { @@ -93,7 +109,7 @@ interface Booted { */ function boot(env: Record, waitFor: RegExp): Promise { return new Promise((resolveBoot, rejectBoot) => { - const child = spawn(process.execPath, [CLI, 'serve', '-p', port, '--dev'], { + const child = spawn(TSX, [CLI, 'serve', '-p', port, '--dev'], { cwd: dir, stdio: ['pipe', 'pipe', 'pipe'], // `childEnv`, not a bare `...process.env`: the vitest worker exports @@ -117,6 +133,9 @@ function boot(env: Record, waitFor: RegExp): Promise OS_SECRET_KEY: E2E_SECRET_KEY, // Explicit, not inherited: the dev-admin seed the mint signs in as is // hard-gated on `NODE_ENV === 'development'`, and vitest exports `test`. + // Still passed explicitly although `bin/run-dev.js` assigns it too: the + // shim's assignment runs after its own static imports have evaluated, + // so only the child env pins the value for the whole process lifetime. NODE_ENV: 'development', ...env, }),