Skip to content

Commit 195fcfd

Browse files
committed
[Telemetry] Comprehensive E2E test coverage for process termination (signals, crash, exit)
1 parent d54e253 commit 195fcfd

2 files changed

Lines changed: 159 additions & 11 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
import {hideBin} from 'yargs/helpers';
3+
import yargs from 'yargs';
4+
import process from 'node:process';
5+
import {ClearcutLogger} from '../../../src/telemetry/clearcut-logger.js';
6+
import {saveLogsToFile} from '../../../src/logger.js';
7+
8+
async function main() {
9+
const argv = await yargs(hideBin(process.argv))
10+
.option('action', {
11+
type: 'string',
12+
choices: ['exit-0', 'exit-1', 'crash', 'stay-alive'],
13+
demandOption: true,
14+
})
15+
.option('log-file', {
16+
type: 'string',
17+
demandOption: true,
18+
})
19+
.parse();
20+
21+
if (argv.logFile) {
22+
saveLogsToFile(argv.logFile);
23+
}
24+
25+
// Initialize Watchdog (mimicking main.ts)
26+
new ClearcutLogger({
27+
logFile: argv.logFile,
28+
appVersion: '1.0.0-test',
29+
});
30+
31+
console.log(`Server fixture started with PID ${process.pid}`);
32+
33+
// Give watchdog time to start
34+
setTimeout(() => {
35+
switch (argv.action) {
36+
case 'exit-0':
37+
console.log('Exiting with 0');
38+
process.exit(0);
39+
break;
40+
case 'exit-1':
41+
console.log('Exiting with 1');
42+
process.exit(1);
43+
break;
44+
case 'crash':
45+
console.log('Crashing');
46+
throw new Error('Planned crash');
47+
case 'stay-alive':
48+
console.log('Staying alive');
49+
setInterval(() => {}, 1000); // Keep alive
50+
break;
51+
}
52+
}, 1000);
53+
}
54+
55+
main();

tests/e2e/telemetry.test.ts

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ import path from 'node:path';
1212
import os from 'node:os';
1313

1414
describe('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, /server_start/);
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, /Parent death detected/);
101101
assert.match(contentAfter, /server_shutdown/);
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(/Watchdog started[\s\S]*?"pid":\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, /Parent death detected/);
187+
assert.match(contentAfter, /server_shutdown/);
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

Comments
 (0)