11// Child adapter tests cover adapting child processes to supervisor runs.
22import type { ChildProcess } from "node:child_process" ;
33import { EventEmitter } from "node:events" ;
4+ import { mkdir , writeFile } from "node:fs/promises" ;
45import path from "node:path" ;
56import { PassThrough } from "node:stream" ;
67import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
8+ import { useAutoCleanupTempDirTracker } from "../../../../test/helpers/temp-dir.js" ;
79import {
810 getWindowsInstallRoots ,
911 resetWindowsInstallRootsForTests ,
@@ -37,6 +39,7 @@ vi.mock("../../../infra/windows-encoding.js", () => ({
3739} ) ) ;
3840
3941let createChildAdapter : typeof import ( "./child.js" ) . createChildAdapter ;
42+ const tempDirs = useAutoCleanupTempDirTracker ( afterEach ) ;
4043
4144function createStubChild ( pid = 1234 ) {
4245 const child = new EventEmitter ( ) as ChildProcess ;
@@ -161,6 +164,25 @@ describe("createChildAdapter", () => {
161164 vi . useRealTimers ( ) ;
162165 } ) ;
163166
167+ const createWindowsNpmShim = async ( params : { command : string ; packagePath : string [ ] } ) => {
168+ const binDir = tempDirs . make ( "openclaw-child-shim-" ) ;
169+ const entrypoint = path . join ( binDir , "node_modules" , ...params . packagePath ) ;
170+ await mkdir ( path . dirname ( entrypoint ) , { recursive : true } ) ;
171+ await writeFile ( entrypoint , "" , "utf8" ) ;
172+ const relativeEntrypoint = path . relative ( binDir , entrypoint ) . replaceAll ( path . sep , "\\" ) ;
173+ const shimHead =
174+ "@ECHO off\r\nGOTO start\r\n:find_dp0\r\nSET dp0=%~dp0\r\nEXIT /b\r\n:start\r\nSETLOCAL\r\nCALL :find_dp0\r\n" ;
175+ const shimCommand = entrypoint . endsWith ( ".exe" )
176+ ? `"%dp0%\\${ relativeEntrypoint } " %*\r\n`
177+ : `IF EXIST "%dp0%\\node.exe" (\r\n SET "_prog=%dp0%\\node.exe"\r\n) ELSE (\r\n SET "_prog=node"\r\n)\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\${ relativeEntrypoint } " %*\r\n` ;
178+ await writeFile (
179+ path . join ( binDir , `${ params . command } .cmd` ) ,
180+ `${ shimHead } ${ shimCommand } ` ,
181+ "utf8" ,
182+ ) ;
183+ return { binDir, entrypoint } ;
184+ } ;
185+
164186 it ( "uses process-tree kill for default SIGKILL" , async ( ) => {
165187 const { adapter, killMock } = await createAdapterHarness ( { pid : 4321 } ) ;
166188
@@ -408,6 +430,7 @@ describe("createChildAdapter", () => {
408430 await createAdapterHarness ( {
409431 pid : 3335 ,
410432 argv : [ "pnpm" , "--version" ] ,
433+ env : { PATH : "" , PATHEXT : ".EXE;.CMD;.BAT" } ,
411434 } ) ;
412435
413436 const spawnArgs = firstSpawnWithFallbackParams ( ) ;
@@ -424,6 +447,48 @@ describe("createChildAdapter", () => {
424447 expect ( spawnArgs . fallbacks ) . toStrictEqual ( [ ] ) ;
425448 } ) ;
426449
450+ it ( "unwraps Gemini's npm shim and preserves prompt argv on Windows" , async ( ) => {
451+ setPlatform ( "win32" ) ;
452+ const { binDir, entrypoint } = await createWindowsNpmShim ( {
453+ command : "gemini" ,
454+ packagePath : [ "@google" , "gemini-cli" , "bundle" , "gemini.js" ] ,
455+ } ) ;
456+ const nodePath = path . join ( binDir , "node.exe" ) ;
457+ await writeFile ( nodePath , "" , "utf8" ) ;
458+ const prompt = "explain A&B | C > D and 100% coverage" ;
459+
460+ await createAdapterHarness ( {
461+ pid : 3336 ,
462+ argv : [ "gemini" , "--prompt" , prompt ] ,
463+ env : { PATH : binDir , PATHEXT : ".EXE;.CMD;.BAT" } ,
464+ } ) ;
465+
466+ const spawnArgs = firstSpawnWithFallbackParams ( ) ;
467+ expect ( spawnArgs . argv ?. [ 0 ] ?. toLowerCase ( ) ) . toBe ( nodePath . toLowerCase ( ) ) ;
468+ expect ( spawnArgs . argv ?. slice ( 1 ) ) . toEqual ( [ entrypoint , "--prompt" , prompt ] ) ;
469+ expect ( spawnArgs . options ?. windowsVerbatimArguments ) . toBeUndefined ( ) ;
470+ expect ( spawnArgs . fallbacks ) . toStrictEqual ( [ ] ) ;
471+ } ) ;
472+
473+ it ( "unwraps Claude's npm shim to its native executable on Windows" , async ( ) => {
474+ setPlatform ( "win32" ) ;
475+ const { binDir, entrypoint } = await createWindowsNpmShim ( {
476+ command : "claude" ,
477+ packagePath : [ "@anthropic-ai" , "claude-code" , "bin" , "claude.exe" ] ,
478+ } ) ;
479+
480+ await createAdapterHarness ( {
481+ pid : 3337 ,
482+ argv : [ "claude" , "--version" ] ,
483+ env : { PATH : binDir , PATHEXT : ".EXE;.CMD;.BAT" } ,
484+ } ) ;
485+
486+ const spawnArgs = firstSpawnWithFallbackParams ( ) ;
487+ expect ( spawnArgs . argv ) . toEqual ( [ entrypoint , "--version" ] ) ;
488+ expect ( spawnArgs . options ?. windowsVerbatimArguments ) . toBeUndefined ( ) ;
489+ expect ( spawnArgs . fallbacks ) . toStrictEqual ( [ ] ) ;
490+ } ) ;
491+
427492 it ( "wraps Linux child spawns and strips shell-init env" , async ( ) => {
428493 const originalBashEnv = process . env . BASH_ENV ;
429494 const originalEnv = process . env . ENV ;
0 commit comments