@@ -12,17 +12,17 @@ import path from 'node:path';
1212import os from 'node:os' ;
1313
1414describe ( 'Telemetry E2E' , ( ) => {
15- it ( 'spawns server, logs events, and shuts down watchdog cleanly on termination' , async ( ) => {
15+ async function runTelemetryTest ( signal : NodeJS . Signals ) {
1616 // Setup
17- const logFile = path . join ( os . tmpdir ( ) , `test-mcp-telemetry-${ Date . now ( ) } .log` ) ;
17+ const logFile = path . join ( os . tmpdir ( ) , `test-mcp-telemetry-${ signal } - ${ Date . now ( ) } .log` ) ;
1818 const serverPath = path . resolve ( 'build/src/main.js' ) ;
1919
2020 if ( ! fs . existsSync ( serverPath ) ) {
2121 throw new Error ( `Server build not found at ${ serverPath } . Run 'npm run build' first.` ) ;
2222 }
2323
2424 // Spawn Server
25- console . log ( `Spawning server at ${ serverPath } with log file ${ logFile } ` ) ;
25+ console . log ( `[ ${ signal } ] Spawning server at ${ serverPath } with log file ${ logFile } ` ) ;
2626 const server = spawn ( process . execPath , [
2727 serverPath ,
2828 `--log-file=${ logFile } ` ,
@@ -33,7 +33,7 @@ describe('Telemetry E2E', () => {
3333 stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
3434 } ) ;
3535
36- server . stderr . on ( 'data' , ( d ) => console . log ( `Stderr: ${ d } ` ) ) ;
36+ server . stderr . on ( 'data' , ( d ) => console . log ( `[ ${ signal } ] Stderr: ${ d } ` ) ) ;
3737
3838 // Wait for Watchdog start
3939 let watchdogPid : number | undefined ;
@@ -63,15 +63,15 @@ describe('Telemetry E2E', () => {
6363
6464 await waitForWatchdogStart ;
6565 assert . ok ( watchdogPid , 'Watchdog PID not found' ) ;
66- console . log ( `Watchdog started with PID ${ watchdogPid } ` ) ;
66+ console . log ( `[ ${ signal } ] Watchdog started with PID ${ watchdogPid } ` ) ;
6767
6868 // Verify start event logged
6969 const contentBeforeKill = fs . readFileSync ( logFile , 'utf8' ) ;
7070 assert . match ( contentBeforeKill , / s e r v e r _ s t a r t / ) ;
7171
72- // Kill Server with SIGKILL
73- console . log ( `Killing server process ${ server . pid } with SIGKILL ` ) ;
74- server . kill ( 'SIGKILL' ) ;
72+ // Kill Server
73+ console . log ( `[ ${ signal } ] Killing server process ${ server . pid } with ${ signal } ` ) ;
74+ server . kill ( signal ) ;
7575
7676 // Wait for Watchdog exit
7777 const waitForWatchdogExit = new Promise < void > ( ( resolve , reject ) => {
@@ -93,15 +93,108 @@ describe('Telemetry E2E', () => {
9393 } ) ;
9494
9595 await waitForWatchdogExit ;
96- console . log ( ' Watchdog process is gone' ) ;
96+ console . log ( `[ ${ signal } ] Watchdog process is gone` ) ;
9797
9898 // Verify shutdown logged
9999 const contentAfter = fs . readFileSync ( logFile , 'utf8' ) ;
100100 assert . match ( contentAfter , / P a r e n t d e a t h d e t e c t e d / ) ;
101101 assert . match ( contentAfter , / s e r v e r _ s h u t d o w n / ) ;
102- console . log ( ' Shutdown events verified' ) ;
102+ console . log ( `[ ${ signal } ] Shutdown events verified` ) ;
103103
104104 // Cleanup
105105 if ( fs . existsSync ( logFile ) ) fs . unlinkSync ( logFile ) ;
106- } ) ;
106+ }
107+
108+ async function runFixtureTest ( action : 'exit-0' | 'exit-1' | 'crash' ) {
109+ // Setup
110+ const logFile = path . join ( os . tmpdir ( ) , `test-mcp-telemetry-fixture-${ action } -${ Date . now ( ) } .log` ) ;
111+ const fixturePath = path . resolve ( 'build/tests/e2e/fixtures/server-fixture.js' ) ;
112+
113+ if ( ! fs . existsSync ( fixturePath ) ) {
114+ throw new Error ( `Fixture build not found at ${ fixturePath } . Run 'npm run build' first.` ) ;
115+ }
116+
117+ // Spawn Fixture
118+ console . log ( `[${ action } ] Spawning fixture at ${ fixturePath } ` ) ;
119+ const server = spawn ( process . execPath , [
120+ fixturePath ,
121+ `--log-file=${ logFile } ` ,
122+ `--action=${ action } `
123+ ] , {
124+ stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
125+ } ) ;
126+
127+ server . stderr . on ( 'data' , ( d ) => console . log ( `[${ action } ] Stderr: ${ d } ` ) ) ;
128+
129+ // Wait for Watchdog start (same logic)
130+ let watchdogPid : number | undefined ;
131+ const waitForWatchdogStart = new Promise < void > ( ( resolve , reject ) => {
132+ const checkInterval = setInterval ( ( ) => {
133+ // Note: Fixture might exit cleanly for exit-0/1, so we don't reject on exitCode immediately
134+ // unless it exited BEFORE watchdog started?
135+
136+ if ( fs . existsSync ( logFile ) ) {
137+ const content = fs . readFileSync ( logFile , 'utf8' ) ;
138+ const match = content . match ( / W a t c h d o g s t a r t e d [ \s \S ] * ?" p i d " : \s * ( \d + ) / ) ;
139+ if ( match ) {
140+ watchdogPid = parseInt ( match [ 1 ] ) ;
141+ clearInterval ( checkInterval ) ;
142+ resolve ( ) ;
143+ }
144+ }
145+
146+ if ( server . exitCode !== null && ! watchdogPid ) {
147+ // If server exited and we haven't seen watchdog yet... might be too fast or failed?
148+ // But fixture waits 1s.
149+ // We'll keep checking logFile for a moment? No, if server gone, it won't write more.
150+ // reject(new Error(`Server exited early with code ${server.exitCode} before watchdog start`));
151+ }
152+ } , 100 ) ;
153+ setTimeout ( ( ) => {
154+ clearInterval ( checkInterval ) ;
155+ reject ( new Error ( 'Timeout waiting for Watchdog start' ) ) ;
156+ } , 5000 ) ;
157+ } ) ;
158+
159+ await waitForWatchdogStart ;
160+ assert . ok ( watchdogPid , 'Watchdog PID not found' ) ;
161+ console . log ( `[${ action } ] Watchdog started with PID ${ watchdogPid } ` ) ;
162+
163+ // Wait for Watchdog exit
164+ const waitForWatchdogExit = new Promise < void > ( ( resolve , reject ) => {
165+ const checkInterval = setInterval ( ( ) => {
166+ try {
167+ process . kill ( watchdogPid ! , 0 ) ;
168+ } catch ( e ) {
169+ clearInterval ( checkInterval ) ;
170+ resolve ( ) ;
171+ return ;
172+ }
173+ } , 100 ) ;
174+
175+ setTimeout ( ( ) => {
176+ clearInterval ( checkInterval ) ;
177+ try { process . kill ( watchdogPid ! , 'SIGKILL' ) ; } catch { }
178+ reject ( new Error ( 'Timeout waiting for Watchdog exit' ) ) ;
179+ } , 5000 ) ;
180+ } ) ;
181+
182+ await waitForWatchdogExit ;
183+ console . log ( `[${ action } ] Watchdog process is gone` ) ;
184+
185+ const contentAfter = fs . readFileSync ( logFile , 'utf8' ) ;
186+ assert . match ( contentAfter , / P a r e n t d e a t h d e t e c t e d / ) ;
187+ assert . match ( contentAfter , / s e r v e r _ s h u t d o w n / ) ;
188+ console . log ( `[${ action } ] Shutdown events verified` ) ;
189+
190+ if ( fs . existsSync ( logFile ) ) fs . unlinkSync ( logFile ) ;
191+ }
192+
193+ it ( 'handles clean exit (process.exit(0))' , ( ) => runFixtureTest ( 'exit-0' ) ) ;
194+ it ( 'handles error exit (process.exit(1))' , ( ) => runFixtureTest ( 'exit-1' ) ) ;
195+ it ( 'handles uncaught exception' , ( ) => runFixtureTest ( 'crash' ) ) ;
196+
197+ it ( 'handles SIGKILL' , ( ) => runTelemetryTest ( 'SIGKILL' ) ) ;
198+ it ( 'handles SIGTERM' , ( ) => runTelemetryTest ( 'SIGTERM' ) ) ;
199+ it ( 'handles SIGINT' , ( ) => runTelemetryTest ( 'SIGINT' ) ) ;
107200} ) ;
0 commit comments