@@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process";
44import { tmpdir } from "node:os" ;
55import { dirname , join } from "node:path" ;
66import { fileURLToPath } from "node:url" ;
7+ import { commandInvocation } from "../src/lib/win-exec" ;
78
89setDefaultTimeout ( 30_000 ) ;
910
@@ -216,7 +217,10 @@ describe("release helper", () => {
216217 test ( "preflight runs typecheck, test suite, and privacy scan before version bump on main dry-runs" , ( ) => {
217218 const { calls, result } = runRelease ( "9.9.9" ) ;
218219
219- expect ( result . status ) . toBe ( 0 ) ;
220+ // Report what the script actually said. A bare status assertion turned a
221+ // Windows-only spawn failure into "Expected: 0 Received: 1" with no cause,
222+ // which cost a full CI round to diagnose.
223+ expect ( `${ result . status } \n${ result . stderr ?? "" } ` . trim ( ) ) . toBe ( "0" ) ;
220224
221225 const typecheckIndex = findCallIndex ( calls , "bun" , call => call . args . join ( " " ) === "x tsc --noEmit" ) ;
222226 const testIndex = findCallIndex ( calls , "bun" , call => call . args . join ( " " ) === "test --isolate tests" ) ;
@@ -282,4 +286,39 @@ describe("release helper", () => {
282286 expect ( result . stderr + result . stdout ) . toContain ( "moved while waiting for CI" ) ;
283287 expect ( findCallIndex ( calls , "gh" , call => call . args [ 0 ] === "workflow" && call . args [ 1 ] === "run" ) ) . toBe ( - 1 ) ;
284288 } ) ;
289+
290+ /**
291+ * The preflight's `runQuiet` callers (`npm view`, `git ls-remote`, `gh release
292+ * view`) are the first commands a release runs. On Windows they are `.cmd`
293+ * shims, and a shell-less spawn of a bare `npm` neither consults PATHEXT nor
294+ * accepts a `.cmd` target — so the script died before invoking anything and
295+ * the four tests above failed with an empty call log on windows-latest only.
296+ *
297+ * The rest of this suite runs on the host platform, so on macOS/Linux it can
298+ * never exercise that path. Pin the win32 resolution directly instead of
299+ * waiting for CI to tell us.
300+ */
301+ test ( "preflight commands resolve through the Windows .cmd launcher" , ( ) => {
302+ const env = { PATH : "C:\\shims" , PATHEXT : ".COM;.EXE;.BAT;.CMD" } ;
303+ const cmdShim = ( name : string ) => ( path : string ) => path . toLowerCase ( ) === `c:\\shims\\${ name } .cmd` ;
304+
305+ const npm = commandInvocation ( "npm" , [ "view" , "pkg@9.9.9" , "version" ] , "win32" , { env, exists : cmdShim ( "npm" ) } ) ;
306+ expect ( npm . file ) . toBe ( "cmd.exe" ) ;
307+ expect ( npm . options . windowsVerbatimArguments ) . toBe ( true ) ;
308+ expect ( npm . args . join ( " " ) ) . toContain ( "npm.cmd" ) ;
309+ // A bare name would have survived unresolved and ENOENT'd at spawn time.
310+ expect ( npm . args . join ( " " ) ) . not . toBe ( "npm" ) ;
311+
312+ const gh = commandInvocation ( "gh" , [ "release" , "view" , "v9.9.9" ] , "win32" , { env, exists : cmdShim ( "gh" ) } ) ;
313+ expect ( gh . file ) . toBe ( "cmd.exe" ) ;
314+ expect ( gh . args . join ( " " ) ) . toContain ( "gh.cmd" ) ;
315+
316+ // A real `.exe` (git) must NOT be wrapped: direct spawn keeps arg boundaries.
317+ const git = commandInvocation ( "git" , [ "ls-remote" , "origin" ] , "win32" , {
318+ env,
319+ exists : ( path : string ) => path . toLowerCase ( ) === "c:\\shims\\git.exe" ,
320+ } ) ;
321+ expect ( git . file . toLowerCase ( ) ) . toBe ( "c:\\shims\\git.exe" ) ;
322+ expect ( git . options . windowsVerbatimArguments ) . toBeUndefined ( ) ;
323+ } ) ;
285324} ) ;
0 commit comments