@bryan-thompson/inspector-assessment-cli 1.32.2 → 1.32.3

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.
@@ -191,9 +191,10 @@ describe("loadServerConfig", () => {
191
191
  },
192
192
  },
193
193
  }));
194
- expect(() => loadServerConfig("nourl", "/config.json")).toThrow(/'url' is missing/);
194
+ // Zod union validation returns "Invalid input" when neither HTTP/SSE nor stdio schema matches
195
+ expect(() => loadServerConfig("nourl", "/config.json")).toThrow(/Invalid/);
195
196
  });
196
- it('should throw "url is missing" when transport=sse but no url', () => {
197
+ it("should throw validation error when transport=sse but no url", () => {
197
198
  mockExistsSync.mockReturnValue(true);
198
199
  mockReadFileSync.mockReturnValue(JSON.stringify({
199
200
  mcpServers: {
@@ -202,14 +203,16 @@ describe("loadServerConfig", () => {
202
203
  },
203
204
  },
204
205
  }));
205
- expect(() => loadServerConfig("nourl", "/config.json")).toThrow(/'url' is missing/);
206
+ // Zod union validation returns "Invalid input" when neither HTTP/SSE nor stdio schema matches
207
+ expect(() => loadServerConfig("nourl", "/config.json")).toThrow(/Invalid/);
206
208
  });
207
- it('should throw "url is missing" for root-level transport:http without url', () => {
209
+ it("should throw validation error for root-level transport:http without url", () => {
208
210
  mockExistsSync.mockReturnValue(true);
209
211
  mockReadFileSync.mockReturnValue(JSON.stringify({
210
212
  transport: "http",
211
213
  }));
212
- expect(() => loadServerConfig("server", "/config.json")).toThrow(/'url' is missing/);
214
+ // Zod union validation returns "Invalid input" when neither HTTP/SSE nor stdio schema matches
215
+ expect(() => loadServerConfig("server", "/config.json")).toThrow(/Invalid/);
213
216
  });
214
217
  it('should throw "Server config not found" when server not in any path', () => {
215
218
  mockExistsSync.mockReturnValue(false);
@@ -2,9 +2,14 @@
2
2
  * JSONL Events Module Unit Tests
3
3
  *
4
4
  * Tests for JSONL event emission functions used for real-time monitoring.
5
+ *
6
+ * NOTE: This tests the cli/src/lib/jsonl-events.ts implementation.
7
+ * A parallel implementation exists in scripts/lib/jsonl-events.ts and is tested
8
+ * in scripts/__tests__/jsonl-events-phase7.test.ts. Both implementations must
9
+ * emit identical JSONL structure for Phase 7 events (TEST-REQ-001).
5
10
  */
6
11
  import { jest, describe, it, expect, beforeEach, afterEach, } from "@jest/globals";
7
- import { emitJSONL, emitServerConnected, emitToolDiscovered, emitToolsDiscoveryComplete, emitAssessmentComplete, emitTestBatch, emitVulnerabilityFound, emitAnnotationMissing, emitAnnotationMisaligned, emitAnnotationReviewRecommended, emitAnnotationAligned, emitModulesConfigured, extractToolParams, } from "../lib/jsonl-events.js";
12
+ import { emitJSONL, emitServerConnected, emitToolDiscovered, emitToolsDiscoveryComplete, emitAssessmentComplete, emitTestBatch, emitVulnerabilityFound, emitAnnotationMissing, emitAnnotationMisaligned, emitAnnotationReviewRecommended, emitAnnotationAligned, emitModulesConfigured, emitToolTestComplete, emitValidationSummary, emitPhaseStarted, emitPhaseComplete, extractToolParams, SCHEMA_VERSION, } from "../lib/jsonl-events.js";
8
13
  describe("JSONL Event Emission", () => {
9
14
  let consoleErrorSpy;
10
15
  let emittedEvents;
@@ -290,6 +295,195 @@ describe("JSONL Event Emission", () => {
290
295
  expect(emittedEvents[0]).toHaveProperty("reason", "default");
291
296
  });
292
297
  });
298
+ // ============================================================================
299
+ // Phase 7 Events - Per-Tool Testing & Phase Lifecycle
300
+ // ============================================================================
301
+ describe("emitToolTestComplete", () => {
302
+ it("should emit tool_test_complete with all required fields", () => {
303
+ emitToolTestComplete("test_calculator", "security", 15, 20, "high", "PASS", 5000);
304
+ expect(emittedEvents[0]).toHaveProperty("event", "tool_test_complete");
305
+ expect(emittedEvents[0]).toHaveProperty("tool", "test_calculator");
306
+ expect(emittedEvents[0]).toHaveProperty("module", "security");
307
+ expect(emittedEvents[0]).toHaveProperty("scenariosPassed", 15);
308
+ expect(emittedEvents[0]).toHaveProperty("scenariosExecuted", 20);
309
+ expect(emittedEvents[0]).toHaveProperty("confidence", "high");
310
+ expect(emittedEvents[0]).toHaveProperty("status", "PASS");
311
+ expect(emittedEvents[0]).toHaveProperty("executionTime", 5000);
312
+ });
313
+ it("should include version and schemaVersion fields", () => {
314
+ emitToolTestComplete("tool", "module", 10, 10, "high", "PASS", 1000);
315
+ expect(emittedEvents[0]).toHaveProperty("version");
316
+ expect(emittedEvents[0]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
317
+ });
318
+ it("should handle FAIL status", () => {
319
+ emitToolTestComplete("vulnerable_tool", "security", 5, 20, "low", "FAIL", 3000);
320
+ expect(emittedEvents[0]).toHaveProperty("status", "FAIL");
321
+ expect(emittedEvents[0]).toHaveProperty("confidence", "low");
322
+ expect(emittedEvents[0]).toHaveProperty("scenariosPassed", 5);
323
+ expect(emittedEvents[0]).toHaveProperty("scenariosExecuted", 20);
324
+ });
325
+ it("should handle ERROR status", () => {
326
+ emitToolTestComplete("broken_tool", "functionality", 0, 10, "medium", "ERROR", 2000);
327
+ expect(emittedEvents[0]).toHaveProperty("status", "ERROR");
328
+ expect(emittedEvents[0]).toHaveProperty("scenariosPassed", 0);
329
+ });
330
+ it("should handle different confidence levels", () => {
331
+ emitToolTestComplete("t1", "m1", 10, 10, "high", "PASS", 1000);
332
+ emitToolTestComplete("t2", "m2", 5, 10, "medium", "PASS", 1000);
333
+ emitToolTestComplete("t3", "m3", 2, 10, "low", "FAIL", 1000);
334
+ expect(emittedEvents[0]).toHaveProperty("confidence", "high");
335
+ expect(emittedEvents[1]).toHaveProperty("confidence", "medium");
336
+ expect(emittedEvents[2]).toHaveProperty("confidence", "low");
337
+ });
338
+ it("should handle zero execution time", () => {
339
+ emitToolTestComplete("fast_tool", "module", 10, 10, "high", "PASS", 0);
340
+ expect(emittedEvents[0]).toHaveProperty("executionTime", 0);
341
+ });
342
+ it("should handle different modules", () => {
343
+ emitToolTestComplete("tool1", "security", 10, 10, "high", "PASS", 1000);
344
+ emitToolTestComplete("tool2", "functionality", 10, 10, "high", "PASS", 1000);
345
+ emitToolTestComplete("tool3", "error_handling", 10, 10, "high", "PASS", 1000);
346
+ expect(emittedEvents[0]).toHaveProperty("module", "security");
347
+ expect(emittedEvents[1]).toHaveProperty("module", "functionality");
348
+ expect(emittedEvents[2]).toHaveProperty("module", "error_handling");
349
+ });
350
+ });
351
+ describe("emitValidationSummary", () => {
352
+ it("should emit validation_summary with all required fields", () => {
353
+ emitValidationSummary("test_tool", 5, 3, 2, 1, 4);
354
+ expect(emittedEvents[0]).toHaveProperty("event", "validation_summary");
355
+ expect(emittedEvents[0]).toHaveProperty("tool", "test_tool");
356
+ expect(emittedEvents[0]).toHaveProperty("wrongType", 5);
357
+ expect(emittedEvents[0]).toHaveProperty("missingRequired", 3);
358
+ expect(emittedEvents[0]).toHaveProperty("extraParams", 2);
359
+ expect(emittedEvents[0]).toHaveProperty("nullValues", 1);
360
+ expect(emittedEvents[0]).toHaveProperty("invalidValues", 4);
361
+ });
362
+ it("should include version and schemaVersion fields", () => {
363
+ emitValidationSummary("tool", 0, 0, 0, 0, 0);
364
+ expect(emittedEvents[0]).toHaveProperty("version");
365
+ expect(emittedEvents[0]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
366
+ });
367
+ it("should handle zero validation errors", () => {
368
+ emitValidationSummary("perfect_tool", 0, 0, 0, 0, 0);
369
+ expect(emittedEvents[0]).toHaveProperty("wrongType", 0);
370
+ expect(emittedEvents[0]).toHaveProperty("missingRequired", 0);
371
+ expect(emittedEvents[0]).toHaveProperty("extraParams", 0);
372
+ expect(emittedEvents[0]).toHaveProperty("nullValues", 0);
373
+ expect(emittedEvents[0]).toHaveProperty("invalidValues", 0);
374
+ });
375
+ it("should handle high validation error counts", () => {
376
+ emitValidationSummary("poor_validation_tool", 100, 50, 25, 10, 75);
377
+ expect(emittedEvents[0]).toHaveProperty("wrongType", 100);
378
+ expect(emittedEvents[0]).toHaveProperty("missingRequired", 50);
379
+ expect(emittedEvents[0]).toHaveProperty("extraParams", 25);
380
+ expect(emittedEvents[0]).toHaveProperty("nullValues", 10);
381
+ expect(emittedEvents[0]).toHaveProperty("invalidValues", 75);
382
+ });
383
+ it("should handle only specific error types", () => {
384
+ emitValidationSummary("type_check_only", 10, 0, 0, 0, 0);
385
+ emitValidationSummary("required_check_only", 0, 5, 0, 0, 0);
386
+ emitValidationSummary("null_check_only", 0, 0, 0, 3, 0);
387
+ expect(emittedEvents[0]).toHaveProperty("wrongType", 10);
388
+ expect(emittedEvents[0]).toHaveProperty("missingRequired", 0);
389
+ expect(emittedEvents[1]).toHaveProperty("wrongType", 0);
390
+ expect(emittedEvents[1]).toHaveProperty("missingRequired", 5);
391
+ expect(emittedEvents[2]).toHaveProperty("nullValues", 3);
392
+ expect(emittedEvents[2]).toHaveProperty("wrongType", 0);
393
+ });
394
+ });
395
+ describe("emitPhaseStarted", () => {
396
+ it("should emit phase_started with phase name", () => {
397
+ emitPhaseStarted("discovery");
398
+ expect(emittedEvents[0]).toHaveProperty("event", "phase_started");
399
+ expect(emittedEvents[0]).toHaveProperty("phase", "discovery");
400
+ });
401
+ it("should include version and schemaVersion fields", () => {
402
+ emitPhaseStarted("assessment");
403
+ expect(emittedEvents[0]).toHaveProperty("version");
404
+ expect(emittedEvents[0]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
405
+ });
406
+ it("should handle different phase names", () => {
407
+ emitPhaseStarted("discovery");
408
+ emitPhaseStarted("assessment");
409
+ emitPhaseStarted("analysis");
410
+ emitPhaseStarted("reporting");
411
+ expect(emittedEvents[0]).toHaveProperty("phase", "discovery");
412
+ expect(emittedEvents[1]).toHaveProperty("phase", "assessment");
413
+ expect(emittedEvents[2]).toHaveProperty("phase", "analysis");
414
+ expect(emittedEvents[3]).toHaveProperty("phase", "reporting");
415
+ });
416
+ it("should handle custom phase names", () => {
417
+ emitPhaseStarted("custom_phase");
418
+ expect(emittedEvents[0]).toHaveProperty("phase", "custom_phase");
419
+ });
420
+ });
421
+ describe("emitPhaseComplete", () => {
422
+ it("should emit phase_complete with phase name and duration", () => {
423
+ emitPhaseComplete("discovery", 5000);
424
+ expect(emittedEvents[0]).toHaveProperty("event", "phase_complete");
425
+ expect(emittedEvents[0]).toHaveProperty("phase", "discovery");
426
+ expect(emittedEvents[0]).toHaveProperty("duration", 5000);
427
+ });
428
+ it("should include version and schemaVersion fields", () => {
429
+ emitPhaseComplete("assessment", 10000);
430
+ expect(emittedEvents[0]).toHaveProperty("version");
431
+ expect(emittedEvents[0]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
432
+ });
433
+ it("should handle zero duration", () => {
434
+ emitPhaseComplete("fast_phase", 0);
435
+ expect(emittedEvents[0]).toHaveProperty("duration", 0);
436
+ });
437
+ it("should handle long durations", () => {
438
+ emitPhaseComplete("long_phase", 120000);
439
+ expect(emittedEvents[0]).toHaveProperty("duration", 120000);
440
+ });
441
+ it("should handle different phase names", () => {
442
+ emitPhaseComplete("discovery", 1000);
443
+ emitPhaseComplete("assessment", 50000);
444
+ emitPhaseComplete("analysis", 3000);
445
+ expect(emittedEvents[0]).toHaveProperty("phase", "discovery");
446
+ expect(emittedEvents[0]).toHaveProperty("duration", 1000);
447
+ expect(emittedEvents[1]).toHaveProperty("phase", "assessment");
448
+ expect(emittedEvents[1]).toHaveProperty("duration", 50000);
449
+ expect(emittedEvents[2]).toHaveProperty("phase", "analysis");
450
+ expect(emittedEvents[2]).toHaveProperty("duration", 3000);
451
+ });
452
+ });
453
+ describe("Phase 7 Event Schema Consistency", () => {
454
+ it("all Phase 7 events should have consistent schema version", () => {
455
+ emitToolTestComplete("tool", "module", 10, 10, "high", "PASS", 1000);
456
+ emitValidationSummary("tool", 1, 2, 3, 4, 5);
457
+ emitPhaseStarted("phase");
458
+ emitPhaseComplete("phase", 1000);
459
+ expect(emittedEvents[0]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
460
+ expect(emittedEvents[1]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
461
+ expect(emittedEvents[2]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
462
+ expect(emittedEvents[3]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
463
+ });
464
+ it("all Phase 7 events should have version field", () => {
465
+ emitToolTestComplete("tool", "module", 10, 10, "high", "PASS", 1000);
466
+ emitValidationSummary("tool", 1, 2, 3, 4, 5);
467
+ emitPhaseStarted("phase");
468
+ emitPhaseComplete("phase", 1000);
469
+ emittedEvents.forEach((event) => {
470
+ expect(event).toHaveProperty("version");
471
+ expect(typeof event.version).toBe("string");
472
+ });
473
+ });
474
+ it("all Phase 7 events should emit valid JSON", () => {
475
+ emitToolTestComplete("tool", "module", 10, 10, "high", "PASS", 1000);
476
+ emitValidationSummary("tool", 1, 2, 3, 4, 5);
477
+ emitPhaseStarted("phase");
478
+ emitPhaseComplete("phase", 1000);
479
+ // If parsing failed, emittedEvents would be empty
480
+ expect(emittedEvents.length).toBe(4);
481
+ emittedEvents.forEach((event) => {
482
+ expect(event).toBeDefined();
483
+ expect(typeof event).toBe("object");
484
+ });
485
+ });
486
+ });
293
487
  });
294
488
  describe("extractToolParams", () => {
295
489
  it("should return empty array for null schema", () => {
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Server Config Schemas Type Guard Tests
3
+ *
4
+ * Tests for isHttpSseConfig and isStdioConfig type guards to ensure
5
+ * proper mutual exclusivity and correct type discrimination.
6
+ *
7
+ * Addresses QA requirement: Test that type guards are mutually exclusive
8
+ * and correctly discriminate between transport types.
9
+ */
10
+ import { describe, it, expect } from "@jest/globals";
11
+ import { z } from "zod";
12
+ // Define schemas inline to avoid import issues with client/lib in CLI tests
13
+ const HttpSseServerConfigSchema = z.object({
14
+ transport: z.enum(["http", "sse"]).optional(),
15
+ url: z
16
+ .string()
17
+ .min(1, "'url' is required for HTTP/SSE transport")
18
+ .url("url must be a valid URL"),
19
+ });
20
+ const StdioServerConfigSchema = z.object({
21
+ transport: z.literal("stdio").optional(),
22
+ command: z.string().min(1, "command is required for stdio transport"),
23
+ args: z.array(z.string()).optional().default([]),
24
+ env: z.record(z.string()).optional().default({}),
25
+ cwd: z.string().optional(),
26
+ });
27
+ const ServerEntrySchema = z.union([
28
+ HttpSseServerConfigSchema,
29
+ StdioServerConfigSchema,
30
+ ]);
31
+ // Type guard functions (copied from server-configSchemas.ts)
32
+ function isHttpSseConfig(entry) {
33
+ return ("url" in entry || entry.transport === "http" || entry.transport === "sse");
34
+ }
35
+ function isStdioConfig(entry) {
36
+ return "command" in entry && !("url" in entry);
37
+ }
38
+ describe("server-configSchemas type guards", () => {
39
+ describe("isHttpSseConfig", () => {
40
+ it("should return true for HTTP transport config", () => {
41
+ const config = {
42
+ transport: "http",
43
+ url: "http://localhost:8080",
44
+ };
45
+ expect(isHttpSseConfig(config)).toBe(true);
46
+ });
47
+ it("should return true for SSE transport config", () => {
48
+ const config = {
49
+ transport: "sse",
50
+ url: "http://localhost:3000/events",
51
+ };
52
+ expect(isHttpSseConfig(config)).toBe(true);
53
+ });
54
+ it("should return true for config with url but no explicit transport", () => {
55
+ const config = {
56
+ url: "http://api.example.com/mcp",
57
+ };
58
+ expect(isHttpSseConfig(config)).toBe(true);
59
+ });
60
+ it("should return false for stdio transport config", () => {
61
+ const config = {
62
+ transport: "stdio",
63
+ command: "node",
64
+ args: ["server.js"],
65
+ };
66
+ expect(isHttpSseConfig(config)).toBe(false);
67
+ });
68
+ it("should return false for config with command but no url", () => {
69
+ const config = {
70
+ command: "python",
71
+ args: ["server.py"],
72
+ };
73
+ expect(isHttpSseConfig(config)).toBe(false);
74
+ });
75
+ });
76
+ describe("isStdioConfig", () => {
77
+ it("should return true for stdio transport config", () => {
78
+ const config = {
79
+ transport: "stdio",
80
+ command: "node",
81
+ args: ["index.js"],
82
+ };
83
+ expect(isStdioConfig(config)).toBe(true);
84
+ });
85
+ it("should return true for config with command but no explicit transport", () => {
86
+ const config = {
87
+ command: "python",
88
+ args: ["-m", "server"],
89
+ };
90
+ expect(isStdioConfig(config)).toBe(true);
91
+ });
92
+ it("should return true for minimal stdio config (command only)", () => {
93
+ const config = {
94
+ command: "simple-server",
95
+ };
96
+ expect(isStdioConfig(config)).toBe(true);
97
+ });
98
+ it("should return false for HTTP transport config", () => {
99
+ const config = {
100
+ transport: "http",
101
+ url: "http://localhost:8080",
102
+ };
103
+ expect(isStdioConfig(config)).toBe(false);
104
+ });
105
+ it("should return false for SSE transport config", () => {
106
+ const config = {
107
+ transport: "sse",
108
+ url: "http://localhost:3000/events",
109
+ };
110
+ expect(isStdioConfig(config)).toBe(false);
111
+ });
112
+ it("should return false for config with url but no command", () => {
113
+ const config = {
114
+ url: "http://api.example.com",
115
+ };
116
+ expect(isStdioConfig(config)).toBe(false);
117
+ });
118
+ });
119
+ describe("mutual exclusivity", () => {
120
+ it("HTTP config should not be stdio config", () => {
121
+ const config = {
122
+ transport: "http",
123
+ url: "http://localhost:8080",
124
+ };
125
+ expect(isHttpSseConfig(config)).toBe(true);
126
+ expect(isStdioConfig(config)).toBe(false);
127
+ });
128
+ it("SSE config should not be stdio config", () => {
129
+ const config = {
130
+ transport: "sse",
131
+ url: "http://localhost:3000/events",
132
+ };
133
+ expect(isHttpSseConfig(config)).toBe(true);
134
+ expect(isStdioConfig(config)).toBe(false);
135
+ });
136
+ it("stdio config should not be HTTP/SSE config", () => {
137
+ const config = {
138
+ transport: "stdio",
139
+ command: "node",
140
+ args: ["server.js"],
141
+ };
142
+ expect(isStdioConfig(config)).toBe(true);
143
+ expect(isHttpSseConfig(config)).toBe(false);
144
+ });
145
+ it("config with url should not be stdio config", () => {
146
+ const config = {
147
+ url: "http://api.example.com",
148
+ };
149
+ expect(isHttpSseConfig(config)).toBe(true);
150
+ expect(isStdioConfig(config)).toBe(false);
151
+ });
152
+ it("config with command should not be HTTP/SSE config", () => {
153
+ const config = {
154
+ command: "python",
155
+ args: ["server.py"],
156
+ };
157
+ expect(isStdioConfig(config)).toBe(true);
158
+ expect(isHttpSseConfig(config)).toBe(false);
159
+ });
160
+ it("every valid ServerEntry must be exactly one transport type", () => {
161
+ // Property: For all valid ServerEntry configs, exactly one type guard returns true
162
+ const validConfigs = [
163
+ // HTTP configs
164
+ { transport: "http", url: "http://localhost:8080" },
165
+ { url: "http://api.example.com" },
166
+ // SSE configs
167
+ { transport: "sse", url: "http://localhost:3000/events" },
168
+ // stdio configs
169
+ { transport: "stdio", command: "node", args: ["server.js"] },
170
+ { command: "python", args: ["server.py"] },
171
+ { command: "simple-server" },
172
+ ];
173
+ for (const config of validConfigs) {
174
+ const isHttp = isHttpSseConfig(config);
175
+ const isStdio = isStdioConfig(config);
176
+ // Exactly one should be true (XOR)
177
+ const exclusivelyOne = isHttp !== isStdio;
178
+ expect(exclusivelyOne).toBe(true);
179
+ // Additional check: at least one must be true
180
+ const atLeastOne = isHttp || isStdio;
181
+ expect(atLeastOne).toBe(true);
182
+ }
183
+ });
184
+ });
185
+ describe("type guard integration with schemas", () => {
186
+ it("validated HTTP config should pass isHttpSseConfig", () => {
187
+ const input = {
188
+ transport: "http",
189
+ url: "http://localhost:8080",
190
+ };
191
+ const result = ServerEntrySchema.safeParse(input);
192
+ expect(result.success).toBe(true);
193
+ if (result.success) {
194
+ expect(isHttpSseConfig(result.data)).toBe(true);
195
+ }
196
+ });
197
+ it("validated SSE config should pass isHttpSseConfig", () => {
198
+ const input = {
199
+ transport: "sse",
200
+ url: "http://localhost:3000/events",
201
+ };
202
+ const result = ServerEntrySchema.safeParse(input);
203
+ expect(result.success).toBe(true);
204
+ if (result.success) {
205
+ expect(isHttpSseConfig(result.data)).toBe(true);
206
+ }
207
+ });
208
+ it("validated stdio config should pass isStdioConfig", () => {
209
+ const input = {
210
+ command: "node",
211
+ args: ["server.js"],
212
+ };
213
+ const result = ServerEntrySchema.safeParse(input);
214
+ expect(result.success).toBe(true);
215
+ if (result.success) {
216
+ expect(isStdioConfig(result.data)).toBe(true);
217
+ }
218
+ });
219
+ it("type guards should work with Zod-parsed data", () => {
220
+ const httpInput = { url: "http://api.example.com" };
221
+ const stdioInput = { command: "python", args: ["server.py"] };
222
+ const httpResult = HttpSseServerConfigSchema.safeParse(httpInput);
223
+ const stdioResult = StdioServerConfigSchema.safeParse(stdioInput);
224
+ expect(httpResult.success).toBe(true);
225
+ expect(stdioResult.success).toBe(true);
226
+ if (httpResult.success) {
227
+ expect(isHttpSseConfig(httpResult.data)).toBe(true);
228
+ expect(isStdioConfig(httpResult.data)).toBe(false);
229
+ }
230
+ if (stdioResult.success) {
231
+ expect(isStdioConfig(stdioResult.data)).toBe(true);
232
+ expect(isHttpSseConfig(stdioResult.data)).toBe(false);
233
+ }
234
+ });
235
+ });
236
+ describe("edge cases", () => {
237
+ it("should handle config with both url and command (ambiguous)", () => {
238
+ // This should not be possible with valid ServerEntry union,
239
+ // but test the type guard behavior if it happens
240
+ const ambiguousConfig = {
241
+ url: "http://localhost:8080",
242
+ command: "node",
243
+ };
244
+ // Type guards should prioritize based on their implementation
245
+ // isHttpSseConfig checks for 'url' or transport='http'/'sse'
246
+ // isStdioConfig checks for 'command' AND NOT 'url'
247
+ expect(isHttpSseConfig(ambiguousConfig)).toBe(true);
248
+ expect(isStdioConfig(ambiguousConfig)).toBe(false);
249
+ // This demonstrates the priority: url takes precedence over command
250
+ });
251
+ it("should handle config with transport field only (incomplete)", () => {
252
+ const incompleteHttpConfig = { transport: "http" };
253
+ const incompleteStdioConfig = {
254
+ transport: "stdio",
255
+ };
256
+ // These won't validate, but test type guard behavior
257
+ expect(isHttpSseConfig(incompleteHttpConfig)).toBe(true);
258
+ expect(isStdioConfig(incompleteStdioConfig)).toBe(false);
259
+ });
260
+ it("should handle empty config object", () => {
261
+ const emptyConfig = {};
262
+ // Neither type guard should match empty config
263
+ expect(isHttpSseConfig(emptyConfig)).toBe(false);
264
+ expect(isStdioConfig(emptyConfig)).toBe(false);
265
+ });
266
+ });
267
+ describe("TypeScript type narrowing", () => {
268
+ it("should narrow type to HttpSseServerConfig after guard check", () => {
269
+ const config = {
270
+ url: "http://localhost:8080",
271
+ };
272
+ if (isHttpSseConfig(config)) {
273
+ // TypeScript should infer config as HttpSseServerConfig here
274
+ const url = config.url;
275
+ expect(url).toBe("http://localhost:8080");
276
+ // @ts-expect-error - command should not exist on HttpSseServerConfig
277
+ const _command = config.command;
278
+ }
279
+ });
280
+ it("should narrow type to StdioServerConfig after guard check", () => {
281
+ const config = {
282
+ command: "node",
283
+ args: ["server.js"],
284
+ };
285
+ if (isStdioConfig(config)) {
286
+ // TypeScript should infer config as StdioServerConfig here
287
+ const command = config.command;
288
+ expect(command).toBe("node");
289
+ // @ts-expect-error - url should not exist on StdioServerConfig
290
+ const _url = config.url;
291
+ }
292
+ });
293
+ it("should handle exhaustive type checking pattern", () => {
294
+ const testConfigs = [
295
+ { url: "http://localhost:8080" },
296
+ { command: "node", args: ["server.js"] },
297
+ ];
298
+ for (const config of testConfigs) {
299
+ if (isHttpSseConfig(config)) {
300
+ // Handle HTTP/SSE case
301
+ expect(config.url).toBeTruthy();
302
+ }
303
+ else if (isStdioConfig(config)) {
304
+ // Handle stdio case
305
+ expect(config.command).toBeTruthy();
306
+ }
307
+ else {
308
+ // This branch should never be reached for valid ServerEntry
309
+ fail("Config should be either HTTP/SSE or stdio");
310
+ }
311
+ }
312
+ });
313
+ });
314
+ });