@@ -3,7 +3,17 @@ import { EventEmitter } from "node:events";
33import fs from "node:fs/promises" ;
44import os from "node:os" ;
55import path from "node:path" ;
6- import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
6+ import {
7+ afterAll ,
8+ afterEach ,
9+ beforeAll ,
10+ beforeEach ,
11+ describe ,
12+ expect ,
13+ it ,
14+ vi ,
15+ type MockInstance ,
16+ } from "vitest" ;
717import { MAX_SAFE_TIMEOUT_DELAY_MS } from "../../../gateway-client/src/timeouts.js" ;
818
919const spawnMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
@@ -24,13 +34,15 @@ import {
2434 type QmdBinaryAvailability ,
2535} from "./qmd-process.js" ;
2636
27- function createMockChild ( ) {
37+ function createMockChild ( params : { pid ?: number } = { } ) {
2838 const child = new EventEmitter ( ) as EventEmitter & {
39+ pid ?: number ;
2940 stdout : EventEmitter ;
3041 stderr : EventEmitter ;
3142 kill : ReturnType < typeof vi . fn > ;
3243 closeWith : ( code ?: number | null , signal ?: NodeJS . Signals | null ) => void ;
3344 } ;
45+ child . pid = params . pid ;
3446 child . stdout = new EventEmitter ( ) ;
3547 child . stderr = new EventEmitter ( ) ;
3648 child . kill = vi . fn ( ) ;
@@ -42,7 +54,7 @@ function createMockChild() {
4254
4355let fixtureRoot = "" ;
4456let tempDir = "" ;
45- let platformSpy : { mockRestore ( ) : void } | null = null ;
57+ let platformSpy : MockInstance < ( ) => NodeJS . Platform > | null = null ;
4658let fixtureId = 0 ;
4759const originalPath = process . env . PATH ;
4860const originalPathExt = process . env . PATHEXT ;
@@ -69,6 +81,7 @@ afterEach(() => {
6981 vi . useRealTimers ( ) ;
7082 process . env . PATH = originalPath ;
7183 process . env . PATHEXT = originalPathExt ;
84+ platformSpy ?. mockReturnValue ( "win32" ) ;
7285 spawnMock . mockReset ( ) ;
7386 tempDir = "" ;
7487} ) ;
@@ -218,6 +231,34 @@ describe("checkQmdBinaryAvailability", () => {
218231
219232 expect ( timeoutSpy ) . toHaveBeenCalledWith ( expect . any ( Function ) , MAX_SAFE_TIMEOUT_DELAY_MS ) ;
220233 } ) ;
234+
235+ it ( "kills timed-out availability probes by process group on POSIX" , async ( ) => {
236+ platformSpy ?. mockReturnValue ( "linux" ) ;
237+ const killProcess = vi . spyOn ( process , "kill" ) . mockImplementation ( ( ) => true ) ;
238+ const child = createMockChild ( { pid : 4321 } ) ;
239+ spawnMock . mockReturnValueOnce ( child ) ;
240+
241+ try {
242+ await expect (
243+ checkQmdBinaryAvailability ( {
244+ command : "qmd" ,
245+ env : process . env ,
246+ cwd : tempDir ,
247+ timeoutMs : 1 ,
248+ } ) ,
249+ ) . resolves . toEqual ( {
250+ available : false ,
251+ reason : "binary" ,
252+ error : "spawn qmd timed out after 1ms" ,
253+ } ) ;
254+
255+ expect ( spawnMock . mock . calls [ 0 ] ?. [ 2 ] ) . toMatchObject ( { detached : true } ) ;
256+ expect ( killProcess ) . toHaveBeenCalledWith ( - 4321 , "SIGKILL" ) ;
257+ expect ( child . kill ) . not . toHaveBeenCalledWith ( "SIGKILL" ) ;
258+ } finally {
259+ killProcess . mockRestore ( ) ;
260+ }
261+ } ) ;
221262} ) ;
222263
223264describe ( "runCliCommand" , ( ) => {
@@ -341,4 +382,33 @@ describe("runCliCommand", () => {
341382
342383 expect ( timeoutSpy ) . toHaveBeenCalledWith ( expect . any ( Function ) , MAX_SAFE_TIMEOUT_DELAY_MS ) ;
343384 } ) ;
385+
386+ it ( "kills timed-out cli command process groups on POSIX" , async ( ) => {
387+ platformSpy ?. mockReturnValue ( "linux" ) ;
388+ const killProcess = vi . spyOn ( process , "kill" ) . mockImplementation ( ( ) => true ) ;
389+ const child = createMockChild ( { pid : 8765 } ) ;
390+ spawnMock . mockReturnValueOnce ( child ) ;
391+
392+ try {
393+ const pending = runCliCommand ( {
394+ commandSummary : "qmd query test" ,
395+ spawnInvocation : { command : "qmd" , argv : [ "query" , "test" , "--json" ] } ,
396+ env : process . env ,
397+ cwd : tempDir ,
398+ maxOutputChars : 10_000 ,
399+ timeoutMs : 1 ,
400+ } ) ;
401+ const timeoutAssertion = expect ( pending ) . rejects . toThrow (
402+ "qmd query test timed out after 1ms" ,
403+ ) ;
404+
405+ await timeoutAssertion ;
406+
407+ expect ( spawnMock . mock . calls [ 0 ] ?. [ 2 ] ) . toMatchObject ( { detached : true } ) ;
408+ expect ( killProcess ) . toHaveBeenCalledWith ( - 8765 , "SIGKILL" ) ;
409+ expect ( child . kill ) . not . toHaveBeenCalledWith ( "SIGKILL" ) ;
410+ } finally {
411+ killProcess . mockRestore ( ) ;
412+ }
413+ } ) ;
344414} ) ;
0 commit comments