mcpmon 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcpmon.test.ts ADDED
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Unit and integration tests for mcpmon (Bun/TypeScript version).
3
+ *
4
+ * Run with: bun test
5
+ */
6
+
7
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
8
+ import { spawn, type Subprocess } from "bun";
9
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "fs";
10
+ import { join } from "path";
11
+ import { tmpdir } from "os";
12
+
13
+ // =============================================================================
14
+ // Test Utilities
15
+ // =============================================================================
16
+
17
+ function sleep(ms: number): Promise<void> {
18
+ return new Promise((resolve) => setTimeout(resolve, ms));
19
+ }
20
+
21
+ function createTempDir(): string {
22
+ return mkdtempSync(join(tmpdir(), "mcpmon-test-"));
23
+ }
24
+
25
+ function createDummyServer(dir: string): string {
26
+ const serverPath = join(dir, "server.py");
27
+ writeFileSync(
28
+ serverPath,
29
+ `
30
+ import sys
31
+ import time
32
+ import signal
33
+
34
+ print(f"[test-server] Started (pid={__import__('os').getpid()})", file=sys.stderr)
35
+ sys.stderr.flush()
36
+
37
+ def handle_term(sig, frame):
38
+ print("[test-server] Received SIGTERM", file=sys.stderr)
39
+ sys.exit(0)
40
+
41
+ signal.signal(signal.SIGTERM, handle_term)
42
+
43
+ while True:
44
+ time.sleep(0.1)
45
+ `
46
+ );
47
+ return serverPath;
48
+ }
49
+
50
+ // =============================================================================
51
+ // Unit Tests - Logging
52
+ // =============================================================================
53
+
54
+ describe("Logging", () => {
55
+ test("help shows all options", async () => {
56
+ const proc = spawn({
57
+ cmd: ["bun", "mcpmon.ts", "--help"],
58
+ stdout: "pipe",
59
+ stderr: "pipe",
60
+ });
61
+
62
+ const output = await new Response(proc.stdout).text();
63
+ await proc.exited;
64
+
65
+ expect(output).toContain("--watch");
66
+ expect(output).toContain("--ext");
67
+ expect(output).toContain("--quiet");
68
+ expect(output).toContain("--verbose");
69
+ expect(output).toContain("--debug");
70
+ expect(output).toContain("--timestamps");
71
+ expect(output).toContain("--log-file");
72
+ });
73
+
74
+ test("help shows logging levels", async () => {
75
+ const proc = spawn({
76
+ cmd: ["bun", "mcpmon.ts", "--help"],
77
+ stdout: "pipe",
78
+ stderr: "pipe",
79
+ });
80
+
81
+ const output = await new Response(proc.stdout).text();
82
+ await proc.exited;
83
+
84
+ expect(output).toContain("Logging levels:");
85
+ expect(output).toContain("--quiet");
86
+ expect(output).toContain("--verbose");
87
+ expect(output).toContain("--debug");
88
+ });
89
+
90
+ test("help shows examples", async () => {
91
+ const proc = spawn({
92
+ cmd: ["bun", "mcpmon.ts", "--help"],
93
+ stdout: "pipe",
94
+ stderr: "pipe",
95
+ });
96
+
97
+ const output = await new Response(proc.stdout).text();
98
+ await proc.exited;
99
+
100
+ expect(output).toContain("Examples:");
101
+ });
102
+ });
103
+
104
+ // =============================================================================
105
+ // Unit Tests - CLI Arguments
106
+ // =============================================================================
107
+
108
+ describe("CLI Arguments", () => {
109
+ test("errors without command", async () => {
110
+ const proc = spawn({
111
+ cmd: ["bun", "mcpmon.ts", "--watch", ".", "--"],
112
+ stdout: "pipe",
113
+ stderr: "pipe",
114
+ });
115
+
116
+ const stderr = await new Response(proc.stderr).text();
117
+ const code = await proc.exited;
118
+
119
+ // Should exit with error when no command after --
120
+ expect(code).not.toBe(0);
121
+ expect(stderr.toLowerCase()).toContain("error");
122
+ });
123
+
124
+ test("accepts -- separator", async () => {
125
+ const proc = spawn({
126
+ cmd: ["bun", "mcpmon.ts", "--help", "--"],
127
+ stdout: "pipe",
128
+ stderr: "pipe",
129
+ });
130
+
131
+ const output = await new Response(proc.stdout).text();
132
+ const code = await proc.exited;
133
+
134
+ expect(code).toBe(0);
135
+ expect(output).toContain("Usage:");
136
+ });
137
+ });
138
+
139
+ // =============================================================================
140
+ // Integration Tests
141
+ // =============================================================================
142
+
143
+ describe("Integration", () => {
144
+ let tempDir: string;
145
+ let proc: Subprocess | null = null;
146
+
147
+ beforeEach(() => {
148
+ tempDir = createTempDir();
149
+ });
150
+
151
+ afterEach(async () => {
152
+ if (proc && proc.exitCode === null) {
153
+ proc.kill("SIGTERM");
154
+ await proc.exited;
155
+ }
156
+ try {
157
+ rmSync(tempDir, { recursive: true, force: true });
158
+ } catch {
159
+ // Ignore cleanup errors
160
+ }
161
+ });
162
+
163
+ test("starts server and shows startup message", async () => {
164
+ const serverPath = createDummyServer(tempDir);
165
+ const logFile = join(tempDir, "mcpmon.log");
166
+
167
+ proc = spawn({
168
+ cmd: [
169
+ "bun",
170
+ "mcpmon.ts",
171
+ "--watch",
172
+ tempDir,
173
+ "--log-file",
174
+ logFile,
175
+ "--",
176
+ "python",
177
+ serverPath,
178
+ ],
179
+ stdout: "pipe",
180
+ stderr: "pipe",
181
+ });
182
+
183
+ // Wait for startup
184
+ await sleep(2000);
185
+
186
+ // Should still be running
187
+ expect(proc.exitCode).toBeNull();
188
+
189
+ // Check log file
190
+ const logContent = await Bun.file(logFile).text();
191
+ expect(logContent).toContain("Watching");
192
+ expect(logContent).toContain("Started:");
193
+ });
194
+
195
+ test("restarts on file change", async () => {
196
+ const serverPath = createDummyServer(tempDir);
197
+ const watchedFile = join(tempDir, "watched.py");
198
+ writeFileSync(watchedFile, "# initial");
199
+
200
+ const logFile = join(tempDir, "mcpmon.log");
201
+
202
+ proc = spawn({
203
+ cmd: [
204
+ "bun",
205
+ "mcpmon.ts",
206
+ "--watch",
207
+ tempDir,
208
+ "--ext",
209
+ "py",
210
+ "--log-file",
211
+ logFile,
212
+ "--",
213
+ "python",
214
+ serverPath,
215
+ ],
216
+ stdout: "pipe",
217
+ stderr: "pipe",
218
+ });
219
+
220
+ // Wait for startup
221
+ await sleep(2000);
222
+
223
+ // Trigger reload
224
+ writeFileSync(watchedFile, "# modified");
225
+
226
+ // Wait for restart
227
+ await sleep(2000);
228
+
229
+ const logContent = await Bun.file(logFile).text();
230
+ expect(logContent).toContain("Restart #1");
231
+ });
232
+
233
+ test("ignores non-matching extensions", async () => {
234
+ const serverPath = createDummyServer(tempDir);
235
+ const otherFile = join(tempDir, "readme.txt");
236
+ writeFileSync(otherFile, "initial");
237
+
238
+ const logFile = join(tempDir, "mcpmon.log");
239
+
240
+ proc = spawn({
241
+ cmd: [
242
+ "bun",
243
+ "mcpmon.ts",
244
+ "--watch",
245
+ tempDir,
246
+ "--ext",
247
+ "py",
248
+ "--debug",
249
+ "--log-file",
250
+ logFile,
251
+ "--",
252
+ "python",
253
+ serverPath,
254
+ ],
255
+ stdout: "pipe",
256
+ stderr: "pipe",
257
+ });
258
+
259
+ // Wait for startup
260
+ await sleep(2000);
261
+
262
+ // Modify non-matching file
263
+ writeFileSync(otherFile, "modified");
264
+
265
+ // Wait
266
+ await sleep(2000);
267
+
268
+ const logContent = await Bun.file(logFile).text();
269
+ expect(logContent).not.toContain("Restart #1");
270
+ });
271
+
272
+ test("handles graceful shutdown", async () => {
273
+ const serverPath = createDummyServer(tempDir);
274
+ const logFile = join(tempDir, "mcpmon.log");
275
+
276
+ proc = spawn({
277
+ cmd: [
278
+ "bun",
279
+ "mcpmon.ts",
280
+ "--watch",
281
+ tempDir,
282
+ "--log-file",
283
+ logFile,
284
+ "--",
285
+ "python",
286
+ serverPath,
287
+ ],
288
+ stdout: "pipe",
289
+ stderr: "pipe",
290
+ });
291
+
292
+ // Wait for startup
293
+ await sleep(2000);
294
+
295
+ // Send SIGTERM
296
+ proc.kill("SIGTERM");
297
+
298
+ // Wait for shutdown
299
+ const code = await proc.exited;
300
+
301
+ const logContent = await Bun.file(logFile).text();
302
+ expect(logContent).toContain("Shutdown complete");
303
+ });
304
+
305
+ test("tracks restart count", async () => {
306
+ const serverPath = createDummyServer(tempDir);
307
+ const watchedFile = join(tempDir, "watched.py");
308
+ writeFileSync(watchedFile, "# v1");
309
+
310
+ const logFile = join(tempDir, "mcpmon.log");
311
+
312
+ proc = spawn({
313
+ cmd: [
314
+ "bun",
315
+ "mcpmon.ts",
316
+ "--watch",
317
+ tempDir,
318
+ "--ext",
319
+ "py",
320
+ "--log-file",
321
+ logFile,
322
+ "--",
323
+ "python",
324
+ serverPath,
325
+ ],
326
+ stdout: "pipe",
327
+ stderr: "pipe",
328
+ });
329
+
330
+ // Wait for startup
331
+ await sleep(2500);
332
+
333
+ // Trigger first restart
334
+ writeFileSync(watchedFile, "# v2");
335
+ await sleep(2500);
336
+
337
+ // Verify first restart happened before triggering second
338
+ let logContent = await Bun.file(logFile).text();
339
+ expect(logContent).toContain("Restart #1");
340
+
341
+ // Trigger second restart
342
+ writeFileSync(watchedFile, "# v3");
343
+ await sleep(2500);
344
+
345
+ logContent = await Bun.file(logFile).text();
346
+ expect(logContent).toContain("Restart #2");
347
+ }, 15000); // Increase timeout to 15s
348
+
349
+ test("verbose mode shows file details", async () => {
350
+ const serverPath = createDummyServer(tempDir);
351
+ const watchedFile = join(tempDir, "mymodule.py");
352
+ writeFileSync(watchedFile, "# initial");
353
+
354
+ const logFile = join(tempDir, "mcpmon.log");
355
+
356
+ proc = spawn({
357
+ cmd: [
358
+ "bun",
359
+ "mcpmon.ts",
360
+ "--watch",
361
+ tempDir,
362
+ "--ext",
363
+ "py",
364
+ "--verbose",
365
+ "--log-file",
366
+ logFile,
367
+ "--",
368
+ "python",
369
+ serverPath,
370
+ ],
371
+ stdout: "pipe",
372
+ stderr: "pipe",
373
+ });
374
+
375
+ // Wait for startup
376
+ await sleep(2000);
377
+
378
+ // Trigger reload
379
+ writeFileSync(watchedFile, "# modified");
380
+ await sleep(2000);
381
+
382
+ const logContent = await Bun.file(logFile).text();
383
+ expect(logContent).toContain("File");
384
+ expect(logContent).toContain("mymodule.py");
385
+ });
386
+ });
387
+
388
+ // =============================================================================
389
+ // Timestamp Tests
390
+ // =============================================================================
391
+
392
+ describe("Timestamps", () => {
393
+ let tempDir: string;
394
+ let proc: Subprocess | null = null;
395
+
396
+ beforeEach(() => {
397
+ tempDir = createTempDir();
398
+ });
399
+
400
+ afterEach(async () => {
401
+ if (proc && proc.exitCode === null) {
402
+ proc.kill("SIGTERM");
403
+ await proc.exited;
404
+ }
405
+ try {
406
+ rmSync(tempDir, { recursive: true, force: true });
407
+ } catch {
408
+ // Ignore cleanup errors
409
+ }
410
+ });
411
+
412
+ test("log file always has full timestamps", async () => {
413
+ const serverPath = createDummyServer(tempDir);
414
+ const logFile = join(tempDir, "mcpmon.log");
415
+
416
+ proc = spawn({
417
+ cmd: [
418
+ "bun",
419
+ "mcpmon.ts",
420
+ "--watch",
421
+ tempDir,
422
+ "--log-file",
423
+ logFile,
424
+ "--",
425
+ "python",
426
+ serverPath,
427
+ ],
428
+ stdout: "pipe",
429
+ stderr: "pipe",
430
+ });
431
+
432
+ await sleep(2000);
433
+
434
+ proc.kill("SIGTERM");
435
+ await proc.exited;
436
+
437
+ const logContent = await Bun.file(logFile).text();
438
+ // Full timestamp format: YYYY-MM-DD HH:MM:SS
439
+ expect(logContent).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
440
+ });
441
+ });