@@ -3,7 +3,7 @@ import { describe, it, before, after } from 'node:test'
33import process from 'node:process'
44import { fork , execSync } from 'node:child_process'
55import * as path from 'node:path'
6- import { kill , lookup , lookupSync , tree , treeSync , removeWmicPrefix , normalizeOutput } from '../../main/ts/ps.ts'
6+ import { kill , lookup , lookupSync , tree , treeSync , removeWmicPrefix , normalizeOutput , filterProcessList } from '../../main/ts/ps.ts'
77import { parse } from '@webpod/ingrid'
88
99const __dirname = new URL ( '.' , import . meta. url ) . pathname
@@ -40,6 +40,14 @@ describe('lookup()', () => {
4040 assert . equal ( list . length , 1 )
4141 assert . equal ( list [ 0 ] . pid , pid )
4242 } )
43+
44+ if ( process . platform !== 'win32' ) {
45+ it ( 'supports custom psargs' , async ( ) => {
46+ const list = await lookup ( { pid, psargs : '-eo pid,ppid,args' } )
47+ assert . equal ( list . length , 1 )
48+ assert . equal ( list [ 0 ] . pid , pid )
49+ } )
50+ }
4351} )
4452
4553describe ( 'lookupSync()' , ( ) => {
@@ -86,7 +94,6 @@ describe('tree()', () => {
8694 const childrenAll = await tree ( { pid, recursive : true } )
8795
8896 await Promise . all ( list . map ( p => kill ( p . pid ) ) )
89- await kill ( pid )
9097
9198 assert . equal ( children . length , 1 )
9299 assert . equal ( childrenAll . length , 2 )
@@ -113,7 +120,6 @@ describe('treeSync()', () => {
113120 const childrenAll = treeSync ( { pid, recursive : true } )
114121
115122 await Promise . all ( list . map ( p => kill ( p . pid ) ) )
116- await kill ( pid )
117123
118124 assert . equal ( children . length , 1 )
119125 assert . equal ( childrenAll . length , 2 )
@@ -170,6 +176,115 @@ describe('ps -eo vs ps -lx output comparison', { skip: process.platform === 'win
170176 } )
171177} )
172178
179+ describe ( 'kill() edge cases' , ( ) => {
180+ it ( 'rejects when killing a non-existent pid' , async ( ) => {
181+ await assert . rejects ( ( ) => kill ( 999_999 ) , { code : 'ESRCH' } )
182+ } )
183+
184+ it ( 'rejects with invalid signal' , async ( ) => {
185+ const pid = spawnChild ( )
186+ await assert . rejects ( ( ) => kill ( pid , 'INVALID' ) )
187+ killSafe ( pid )
188+ } )
189+
190+ it ( 'passes signal as string shorthand' , async ( ) => {
191+ const pid = spawnChild ( )
192+ await kill ( pid , 'SIGKILL' )
193+ assert . equal ( ( await lookup ( { pid } ) ) . length , 0 )
194+ } )
195+
196+ it ( 'invokes callback on error for non-existent pid' , async ( ) => {
197+ let cbErr : any
198+ await kill ( 999_999 , ( err ) => { cbErr = err } ) . catch ( ( ) => { } )
199+ assert . ok ( cbErr )
200+ } )
201+ } )
202+
203+ describe ( 'kill() timeout' , { skip : process . platform === 'win32' } , ( ) => {
204+ it ( 'rejects on timeout when process stays alive' , async ( ) => {
205+ // Signal 0 checks existence but doesn't actually kill — process stays alive, so poll times out
206+ const pid = spawnChild ( )
207+ await assert . rejects (
208+ ( ) => kill ( pid , { signal : 0 as any , timeout : 1 } ) ,
209+ ( err : Error ) => err . message . includes ( 'timeout' )
210+ )
211+ killSafe ( pid )
212+ } )
213+ } )
214+
215+ describe ( 'tree() edge cases' , ( ) => {
216+ it ( 'accepts string pid' , async ( ) => {
217+ const pid = spawnChild ( )
218+ const children = await tree ( String ( pid ) )
219+ assert . ok ( Array . isArray ( children ) )
220+ killSafe ( pid )
221+ } )
222+
223+ it ( 'treeSync accepts number pid' , ( ) => {
224+ const pid = spawnChild ( )
225+ const children = treeSync ( pid )
226+ assert . ok ( Array . isArray ( children ) )
227+ killSafe ( pid )
228+ } )
229+ } )
230+
231+ describe ( 'filterProcessList()' , ( ) => {
232+ const list = [
233+ { pid : '1' , ppid : '0' , command : '/usr/bin/node' , arguments : [ 'server.js' , '--port=3000' ] } ,
234+ { pid : '2' , ppid : '1' , command : '/usr/bin/python' , arguments : [ 'app.py' ] } ,
235+ { pid : '3' , ppid : '1' , command : '/usr/bin/node' , arguments : [ 'worker.js' ] } ,
236+ ]
237+
238+ it ( 'filters by pid array' , ( ) => {
239+ assert . equal ( filterProcessList ( list , { pid : [ '1' , '3' ] } ) . length , 2 )
240+ } )
241+
242+ it ( 'filters by command regex' , ( ) => {
243+ assert . equal ( filterProcessList ( list , { command : 'node' } ) . length , 2 )
244+ } )
245+
246+ it ( 'filters by arguments regex' , ( ) => {
247+ assert . equal ( filterProcessList ( list , { arguments : 'port' } ) . length , 1 )
248+ } )
249+
250+ it ( 'filters by ppid' , ( ) => {
251+ assert . equal ( filterProcessList ( list , { ppid : 1 } ) . length , 2 )
252+ } )
253+
254+ it ( 'returns all when no filters' , ( ) => {
255+ assert . equal ( filterProcessList ( list ) . length , 3 )
256+ } )
257+ } )
258+
259+ describe ( 'normalizeOutput()' , ( ) => {
260+ it ( 'skips entries without pid' , ( ) => {
261+ const data = [ { COMMAND : [ 'node' ] } ] as any
262+ assert . equal ( normalizeOutput ( data ) . length , 0 )
263+ } )
264+
265+ it ( 'skips entries without command' , ( ) => {
266+ const data = [ { PID : [ '1' ] } ] as any
267+ assert . equal ( normalizeOutput ( data ) . length , 0 )
268+ } )
269+
270+ it ( 'handles ARGS header (macOS)' , ( ) => {
271+ const data = [ { PID : [ '1' ] , PPID : [ '0' ] , ARGS : [ '/usr/bin/node server.js' ] } ] as any
272+ const result = normalizeOutput ( data )
273+ assert . equal ( result . length , 1 )
274+ assert . ok ( result [ 0 ] . command )
275+ } )
276+
277+ it ( 'handles quoted paths on Windows' , ( ) => {
278+ const data = [ {
279+ ProcessId : [ '1' ] ,
280+ ParentProcessId : [ '0' ] ,
281+ CommandLine : [ '"C:\\Program Files\\node.exe" server.js' ]
282+ } ] as any
283+ const result = normalizeOutput ( data )
284+ assert . equal ( result . length , 1 )
285+ } )
286+ } )
287+
173288describe ( 'removeWmicPrefix()' , ( ) => {
174289 it ( 'extracts wmic output' , ( ) => {
175290 const input = `CommandLine
@@ -184,4 +299,10 @@ PS C:\\Users\\user>`
184299 const sliced = removeWmicPrefix ( input ) . trim ( )
185300 assert . equal ( sliced , input . slice ( 0 , - 'PS C:\\Users\\user>' . length - 1 ) . trim ( ) )
186301 } )
302+
303+ it ( 'handles output without prompt suffix' , ( ) => {
304+ const input = `ParentProcessId ProcessId\n0 1`
305+ const result = removeWmicPrefix ( input )
306+ assert . ok ( result . includes ( 'ProcessId' ) )
307+ } )
187308} )
0 commit comments