@bryan-thompson/inspector-assessment-cli 1.32.2 → 1.32.4
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/build/__tests__/assessment-runner/assessment-executor.test.js +5 -0
- package/build/__tests__/assessment-runner/server-config.test.js +8 -5
- package/build/__tests__/jsonl-events.test.js +195 -1
- package/build/__tests__/lib/server-configSchemas.test.js +314 -0
- package/build/__tests__/lib/zodErrorFormatter.test.js +721 -0
- package/build/__tests__/security/security-pattern-count.test.js +245 -0
- package/build/assess-security.js +11 -77
- package/build/lib/assessment-runner/__tests__/server-config.test.js +116 -0
- package/build/lib/assessment-runner/assessment-executor.js +18 -1
- package/build/lib/assessment-runner/config-builder.js +10 -0
- package/build/lib/assessment-runner/server-config.js +43 -35
- package/build/lib/assessment-runner/server-configSchemas.js +4 -1
- package/build/lib/jsonl-events.js +59 -0
- package/build/lib/zodErrorFormatter.js +31 -0
- package/package.json +1 -1
|
@@ -77,6 +77,11 @@ jest.unstable_mockModule("../../lib/jsonl-events.js", () => ({
|
|
|
77
77
|
emitAnnotationReviewRecommended: jest.fn(),
|
|
78
78
|
emitAnnotationAligned: jest.fn(),
|
|
79
79
|
emitModulesConfigured: mockEmitModulesConfigured,
|
|
80
|
+
// Phase 7 events
|
|
81
|
+
emitPhaseStarted: jest.fn(),
|
|
82
|
+
emitPhaseComplete: jest.fn(),
|
|
83
|
+
emitToolTestComplete: jest.fn(),
|
|
84
|
+
emitValidationSummary: jest.fn(),
|
|
80
85
|
}));
|
|
81
86
|
jest.unstable_mockModule("fs", () => ({
|
|
82
87
|
existsSync: jest.fn().mockReturnValue(false),
|
|
@@ -191,9 +191,10 @@ describe("loadServerConfig", () => {
|
|
|
191
191
|
},
|
|
192
192
|
},
|
|
193
193
|
}));
|
|
194
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
});
|