@bryan-thompson/inspector-assessment-cli 1.25.0 → 1.25.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.
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Assess-Full CLI Unit Tests
3
+ *
4
+ * Tests for CLI argument parsing concepts, config building, and validation logic.
5
+ * These tests focus on pure logic that can be tested without mocking complex
6
+ * external dependencies. For integration testing of the full CLI, use the
7
+ * actual CLI binary.
8
+ */
9
+ import { describe, it, expect } from "@jest/globals";
10
+ import * as path from "path";
11
+ /**
12
+ * Pure function tests - these test logic concepts used in the CLI
13
+ * without needing to import the actual module (which has side effects)
14
+ */
15
+ describe("CLI Argument Parsing Concepts", () => {
16
+ describe("Profile Flag Parsing", () => {
17
+ const VALID_PROFILES = ["quick", "security", "compliance", "full"];
18
+ function parseProfile(args) {
19
+ const profileIndex = args.indexOf("--profile");
20
+ if (profileIndex === -1) {
21
+ return {}; // No profile specified
22
+ }
23
+ const profileValue = args[profileIndex + 1];
24
+ if (!profileValue || profileValue.startsWith("--")) {
25
+ return { error: "Missing profile value" };
26
+ }
27
+ if (!VALID_PROFILES.includes(profileValue)) {
28
+ return { error: `Invalid profile: ${profileValue}` };
29
+ }
30
+ return { profile: profileValue };
31
+ }
32
+ it("should parse valid profile flags", () => {
33
+ expect(parseProfile(["--profile", "quick"])).toEqual({
34
+ profile: "quick",
35
+ });
36
+ expect(parseProfile(["--profile", "security"])).toEqual({
37
+ profile: "security",
38
+ });
39
+ expect(parseProfile(["--profile", "compliance"])).toEqual({
40
+ profile: "compliance",
41
+ });
42
+ expect(parseProfile(["--profile", "full"])).toEqual({ profile: "full" });
43
+ });
44
+ it("should handle missing profile value", () => {
45
+ expect(parseProfile(["--profile"])).toEqual({
46
+ error: "Missing profile value",
47
+ });
48
+ expect(parseProfile(["--profile", "--other"])).toEqual({
49
+ error: "Missing profile value",
50
+ });
51
+ });
52
+ it("should reject invalid profile names", () => {
53
+ expect(parseProfile(["--profile", "invalid"])).toEqual({
54
+ error: "Invalid profile: invalid",
55
+ });
56
+ expect(parseProfile(["--profile", "QUICK"])).toEqual({
57
+ error: "Invalid profile: QUICK",
58
+ });
59
+ });
60
+ it("should return empty when no profile specified", () => {
61
+ expect(parseProfile([])).toEqual({});
62
+ expect(parseProfile(["--server", "test"])).toEqual({});
63
+ });
64
+ });
65
+ describe("Module List Parsing", () => {
66
+ function parseModules(input) {
67
+ return input
68
+ .split(",")
69
+ .map((n) => n.trim())
70
+ .filter(Boolean);
71
+ }
72
+ it("should parse comma-separated modules", () => {
73
+ expect(parseModules("security,functionality")).toEqual([
74
+ "security",
75
+ "functionality",
76
+ ]);
77
+ });
78
+ it("should trim whitespace", () => {
79
+ expect(parseModules("security , functionality , temporal")).toEqual([
80
+ "security",
81
+ "functionality",
82
+ "temporal",
83
+ ]);
84
+ });
85
+ it("should filter empty values", () => {
86
+ expect(parseModules("security,,functionality")).toEqual([
87
+ "security",
88
+ "functionality",
89
+ ]);
90
+ });
91
+ it("should handle single module", () => {
92
+ expect(parseModules("security")).toEqual(["security"]);
93
+ });
94
+ it("should handle empty input", () => {
95
+ expect(parseModules("")).toEqual([]);
96
+ });
97
+ });
98
+ describe("Output Format Validation", () => {
99
+ const VALID_FORMATS = ["json", "markdown", "html"];
100
+ function validateFormat(format) {
101
+ return VALID_FORMATS.includes(format);
102
+ }
103
+ it("should accept valid formats", () => {
104
+ expect(validateFormat("json")).toBe(true);
105
+ expect(validateFormat("markdown")).toBe(true);
106
+ expect(validateFormat("html")).toBe(true);
107
+ });
108
+ it("should reject invalid formats", () => {
109
+ expect(validateFormat("xml")).toBe(false);
110
+ expect(validateFormat("JSON")).toBe(false);
111
+ expect(validateFormat("pdf")).toBe(false);
112
+ });
113
+ });
114
+ });
115
+ describe("Config File Handling", () => {
116
+ describe("Config Parsing", () => {
117
+ function validateConfig(config) {
118
+ const transport = config.transport || "stdio";
119
+ if (transport === "stdio") {
120
+ if (!config.command) {
121
+ return { valid: false, error: "STDIO transport requires command" };
122
+ }
123
+ return { valid: true };
124
+ }
125
+ if (transport === "http" || transport === "sse") {
126
+ if (!config.url) {
127
+ return {
128
+ valid: false,
129
+ error: `${transport.toUpperCase()} transport requires url`,
130
+ };
131
+ }
132
+ return { valid: true };
133
+ }
134
+ return { valid: false, error: `Unknown transport: ${transport}` };
135
+ }
136
+ it("should validate STDIO config", () => {
137
+ expect(validateConfig({
138
+ transport: "stdio",
139
+ command: "python",
140
+ args: ["-m", "server"],
141
+ })).toEqual({ valid: true });
142
+ });
143
+ it("should require command for STDIO", () => {
144
+ expect(validateConfig({ transport: "stdio" })).toEqual({
145
+ valid: false,
146
+ error: "STDIO transport requires command",
147
+ });
148
+ });
149
+ it("should validate HTTP config", () => {
150
+ expect(validateConfig({ transport: "http", url: "http://localhost:3000/mcp" })).toEqual({ valid: true });
151
+ });
152
+ it("should require url for HTTP", () => {
153
+ expect(validateConfig({ transport: "http" })).toEqual({
154
+ valid: false,
155
+ error: "HTTP transport requires url",
156
+ });
157
+ });
158
+ it("should validate SSE config", () => {
159
+ expect(validateConfig({ transport: "sse", url: "http://localhost:3000/sse" })).toEqual({ valid: true });
160
+ });
161
+ it("should require url for SSE", () => {
162
+ expect(validateConfig({ transport: "sse" })).toEqual({
163
+ valid: false,
164
+ error: "SSE transport requires url",
165
+ });
166
+ });
167
+ it("should default to STDIO when transport not specified", () => {
168
+ expect(validateConfig({ command: "node" })).toEqual({ valid: true });
169
+ });
170
+ });
171
+ describe("Config File Reading", () => {
172
+ it("should detect JSON file by extension", () => {
173
+ const filePath = "/tmp/config.json";
174
+ expect(path.extname(filePath)).toBe(".json");
175
+ });
176
+ it("should handle paths with multiple dots", () => {
177
+ const filePath = "/tmp/my.server.config.json";
178
+ expect(path.extname(filePath)).toBe(".json");
179
+ });
180
+ });
181
+ });
182
+ describe("Exit Code Logic", () => {
183
+ function determineExitCode(result) {
184
+ if (result.overallStatus === "FAIL")
185
+ return 1;
186
+ if (result.security?.vulnerabilities &&
187
+ result.security.vulnerabilities.length > 0) {
188
+ return 1;
189
+ }
190
+ return 0;
191
+ }
192
+ it("should return 0 for PASS with no vulnerabilities", () => {
193
+ expect(determineExitCode({ overallStatus: "PASS" })).toBe(0);
194
+ });
195
+ it("should return 1 for FAIL status", () => {
196
+ expect(determineExitCode({ overallStatus: "FAIL" })).toBe(1);
197
+ });
198
+ it("should return 1 when vulnerabilities exist", () => {
199
+ expect(determineExitCode({
200
+ overallStatus: "PASS",
201
+ security: { vulnerabilities: [{ type: "injection" }] },
202
+ })).toBe(1);
203
+ });
204
+ it("should return 0 for PASS with empty vulnerabilities", () => {
205
+ expect(determineExitCode({
206
+ overallStatus: "PASS",
207
+ security: { vulnerabilities: [] },
208
+ })).toBe(0);
209
+ });
210
+ });
211
+ describe("State Management Concepts", () => {
212
+ describe("Resume Flag Parsing", () => {
213
+ function getResumeMode(args) {
214
+ if (args.includes("--no-resume"))
215
+ return "no-resume";
216
+ if (args.includes("--resume"))
217
+ return "resume";
218
+ return "default";
219
+ }
220
+ it("should detect --resume flag", () => {
221
+ expect(getResumeMode(["--resume"])).toBe("resume");
222
+ expect(getResumeMode(["--server", "test", "--resume"])).toBe("resume");
223
+ });
224
+ it("should detect --no-resume flag", () => {
225
+ expect(getResumeMode(["--no-resume"])).toBe("no-resume");
226
+ });
227
+ it("should return default when neither specified", () => {
228
+ expect(getResumeMode([])).toBe("default");
229
+ expect(getResumeMode(["--server", "test"])).toBe("default");
230
+ });
231
+ it("should handle --no-resume even when --resume also present", () => {
232
+ // --no-resume takes precedence
233
+ expect(getResumeMode(["--resume", "--no-resume"])).toBe("no-resume");
234
+ });
235
+ });
236
+ describe("State File Path Generation", () => {
237
+ function getStateFilePath(serverName, outputDir) {
238
+ const dir = outputDir || "/tmp";
239
+ return path.join(dir, `.${serverName}-assessment-state.json`);
240
+ }
241
+ it("should generate state file path with server name", () => {
242
+ expect(getStateFilePath("test-server")).toBe("/tmp/.test-server-assessment-state.json");
243
+ });
244
+ it("should use custom output directory", () => {
245
+ expect(getStateFilePath("test-server", "/home/user/results")).toBe("/home/user/results/.test-server-assessment-state.json");
246
+ });
247
+ it("should handle server names with special characters", () => {
248
+ expect(getStateFilePath("my-mcp-server")).toBe("/tmp/.my-mcp-server-assessment-state.json");
249
+ });
250
+ });
251
+ });
252
+ describe("Output Path Generation", () => {
253
+ describe("Default Output Paths", () => {
254
+ function getDefaultOutputPath(serverName, format) {
255
+ const ext = format === "markdown" ? ".md" : `.${format}`;
256
+ return `/tmp/inspector-assessment-${serverName}${ext}`;
257
+ }
258
+ it("should generate JSON output path", () => {
259
+ expect(getDefaultOutputPath("my-server", "json")).toBe("/tmp/inspector-assessment-my-server.json");
260
+ });
261
+ it("should generate Markdown output path", () => {
262
+ expect(getDefaultOutputPath("my-server", "markdown")).toBe("/tmp/inspector-assessment-my-server.md");
263
+ });
264
+ it("should generate HTML output path", () => {
265
+ expect(getDefaultOutputPath("my-server", "html")).toBe("/tmp/inspector-assessment-my-server.html");
266
+ });
267
+ });
268
+ describe("Custom Output Paths", () => {
269
+ function resolveOutputPath(specified, serverName, format) {
270
+ if (specified) {
271
+ // If directory, append filename
272
+ if (!path.extname(specified)) {
273
+ const ext = format === "markdown" ? ".md" : `.${format}`;
274
+ return path.join(specified, `inspector-assessment-${serverName}${ext}`);
275
+ }
276
+ return specified;
277
+ }
278
+ const ext = format === "markdown" ? ".md" : `.${format}`;
279
+ return `/tmp/inspector-assessment-${serverName}${ext}`;
280
+ }
281
+ it("should use specified path directly if it has extension", () => {
282
+ expect(resolveOutputPath("/home/user/results.json", "server", "json")).toBe("/home/user/results.json");
283
+ });
284
+ it("should append filename if path is a directory", () => {
285
+ expect(resolveOutputPath("/home/user/results", "server", "json")).toBe("/home/user/results/inspector-assessment-server.json");
286
+ });
287
+ it("should use default when not specified", () => {
288
+ expect(resolveOutputPath(undefined, "server", "json")).toBe("/tmp/inspector-assessment-server.json");
289
+ });
290
+ });
291
+ });
292
+ describe("Boolean Flag Parsing", () => {
293
+ function parseBooleanFlags(args) {
294
+ const flags = {
295
+ verbose: false,
296
+ silent: false,
297
+ claudeEnabled: false,
298
+ includePolicy: false,
299
+ preflight: false,
300
+ };
301
+ if (args.includes("--verbose") || args.includes("-v"))
302
+ flags.verbose = true;
303
+ if (args.includes("--silent") || args.includes("-s"))
304
+ flags.silent = true;
305
+ if (args.includes("--claude-enabled"))
306
+ flags.claudeEnabled = true;
307
+ if (args.includes("--include-policy"))
308
+ flags.includePolicy = true;
309
+ if (args.includes("--preflight"))
310
+ flags.preflight = true;
311
+ return flags;
312
+ }
313
+ it("should parse verbose flag", () => {
314
+ expect(parseBooleanFlags(["--verbose"]).verbose).toBe(true);
315
+ expect(parseBooleanFlags(["-v"]).verbose).toBe(true);
316
+ });
317
+ it("should parse silent flag", () => {
318
+ expect(parseBooleanFlags(["--silent"]).silent).toBe(true);
319
+ expect(parseBooleanFlags(["-s"]).silent).toBe(true);
320
+ });
321
+ it("should parse claude-enabled flag", () => {
322
+ expect(parseBooleanFlags(["--claude-enabled"]).claudeEnabled).toBe(true);
323
+ });
324
+ it("should parse include-policy flag", () => {
325
+ expect(parseBooleanFlags(["--include-policy"]).includePolicy).toBe(true);
326
+ });
327
+ it("should parse preflight flag", () => {
328
+ expect(parseBooleanFlags(["--preflight"]).preflight).toBe(true);
329
+ });
330
+ it("should handle multiple flags", () => {
331
+ const flags = parseBooleanFlags([
332
+ "--verbose",
333
+ "--include-policy",
334
+ "--preflight",
335
+ ]);
336
+ expect(flags.verbose).toBe(true);
337
+ expect(flags.includePolicy).toBe(true);
338
+ expect(flags.preflight).toBe(true);
339
+ expect(flags.silent).toBe(false);
340
+ });
341
+ it("should return all false when no flags", () => {
342
+ const flags = parseBooleanFlags([]);
343
+ expect(Object.values(flags).every((v) => v === false)).toBe(true);
344
+ });
345
+ });
@@ -0,0 +1,358 @@
1
+ /**
2
+ * JSONL Events Module Unit Tests
3
+ *
4
+ * Tests for JSONL event emission functions used for real-time monitoring.
5
+ */
6
+ 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";
8
+ describe("JSONL Event Emission", () => {
9
+ let consoleErrorSpy;
10
+ let emittedEvents;
11
+ beforeEach(() => {
12
+ emittedEvents = [];
13
+ consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(((msg) => {
14
+ try {
15
+ emittedEvents.push(JSON.parse(msg));
16
+ }
17
+ catch {
18
+ // Not JSON, ignore
19
+ }
20
+ }));
21
+ });
22
+ afterEach(() => {
23
+ consoleErrorSpy.mockRestore();
24
+ });
25
+ describe("emitJSONL", () => {
26
+ it("should emit event to stderr as JSON", () => {
27
+ emitJSONL({ event: "test", data: "value" });
28
+ expect(consoleErrorSpy).toHaveBeenCalled();
29
+ expect(emittedEvents[0]).toHaveProperty("event", "test");
30
+ expect(emittedEvents[0]).toHaveProperty("data", "value");
31
+ });
32
+ it("should include version field", () => {
33
+ emitJSONL({ event: "test" });
34
+ expect(emittedEvents[0]).toHaveProperty("version");
35
+ });
36
+ it("should handle complex nested objects", () => {
37
+ emitJSONL({
38
+ event: "complex",
39
+ nested: { a: 1, b: [2, 3] },
40
+ });
41
+ expect(emittedEvents[0]).toHaveProperty("nested.a", 1);
42
+ });
43
+ });
44
+ describe("emitServerConnected", () => {
45
+ it("should emit server_connected event", () => {
46
+ emitServerConnected("test-server", "http");
47
+ expect(emittedEvents[0]).toHaveProperty("event", "server_connected");
48
+ expect(emittedEvents[0]).toHaveProperty("serverName", "test-server");
49
+ expect(emittedEvents[0]).toHaveProperty("transport", "http");
50
+ });
51
+ it("should handle different transport types", () => {
52
+ emitServerConnected("server1", "stdio");
53
+ emitServerConnected("server2", "sse");
54
+ emitServerConnected("server3", "http");
55
+ expect(emittedEvents[0]).toHaveProperty("transport", "stdio");
56
+ expect(emittedEvents[1]).toHaveProperty("transport", "sse");
57
+ expect(emittedEvents[2]).toHaveProperty("transport", "http");
58
+ });
59
+ });
60
+ describe("emitToolDiscovered", () => {
61
+ it("should emit tool_discovered event with basic info", () => {
62
+ const tool = {
63
+ name: "test_tool",
64
+ description: "A test tool",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {},
68
+ },
69
+ };
70
+ emitToolDiscovered(tool);
71
+ expect(emittedEvents[0]).toHaveProperty("event", "tool_discovered");
72
+ expect(emittedEvents[0]).toHaveProperty("name", "test_tool");
73
+ expect(emittedEvents[0]).toHaveProperty("description", "A test tool");
74
+ });
75
+ it("should handle tool without description", () => {
76
+ const tool = {
77
+ name: "no_desc_tool",
78
+ inputSchema: { type: "object" },
79
+ };
80
+ emitToolDiscovered(tool);
81
+ expect(emittedEvents[0]).toHaveProperty("name", "no_desc_tool");
82
+ expect(emittedEvents[0]).toHaveProperty("description", null);
83
+ });
84
+ it("should extract parameters from inputSchema", () => {
85
+ const tool = {
86
+ name: "param_tool",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ query: { type: "string", description: "Search query" },
91
+ limit: { type: "number" },
92
+ },
93
+ required: ["query"],
94
+ },
95
+ };
96
+ emitToolDiscovered(tool);
97
+ const params = emittedEvents[0].params;
98
+ expect(params.length).toBe(2);
99
+ expect(params.find((p) => p.name === "query")).toMatchObject({
100
+ name: "query",
101
+ type: "string",
102
+ required: true,
103
+ description: "Search query",
104
+ });
105
+ expect(params.find((p) => p.name === "limit")).toMatchObject({
106
+ name: "limit",
107
+ type: "number",
108
+ required: false,
109
+ });
110
+ });
111
+ it("should include annotations when present", () => {
112
+ const tool = {
113
+ name: "annotated_tool",
114
+ inputSchema: { type: "object" },
115
+ annotations: {
116
+ readOnlyHint: true,
117
+ destructiveHint: false,
118
+ idempotentHint: true,
119
+ openWorldHint: false,
120
+ },
121
+ };
122
+ emitToolDiscovered(tool);
123
+ const annotations = emittedEvents[0].annotations;
124
+ expect(annotations).not.toBeNull();
125
+ expect(annotations.readOnlyHint).toBe(true);
126
+ expect(annotations.destructiveHint).toBe(false);
127
+ });
128
+ it("should have null annotations when not present", () => {
129
+ const tool = {
130
+ name: "no_annotations",
131
+ inputSchema: { type: "object" },
132
+ };
133
+ emitToolDiscovered(tool);
134
+ expect(emittedEvents[0].annotations).toBeNull();
135
+ });
136
+ });
137
+ describe("emitToolsDiscoveryComplete", () => {
138
+ it("should emit tools_discovery_complete with count", () => {
139
+ emitToolsDiscoveryComplete(15);
140
+ expect(emittedEvents[0]).toHaveProperty("event", "tools_discovery_complete");
141
+ expect(emittedEvents[0]).toHaveProperty("count", 15);
142
+ });
143
+ it("should handle zero tools", () => {
144
+ emitToolsDiscoveryComplete(0);
145
+ expect(emittedEvents[0]).toHaveProperty("count", 0);
146
+ });
147
+ });
148
+ describe("emitAssessmentComplete", () => {
149
+ it("should emit assessment_complete with all fields", () => {
150
+ emitAssessmentComplete("PASS", 1500, 120000, "/tmp/results.json");
151
+ expect(emittedEvents[0]).toHaveProperty("event", "assessment_complete");
152
+ expect(emittedEvents[0]).toHaveProperty("overallStatus", "PASS");
153
+ expect(emittedEvents[0]).toHaveProperty("totalTests", 1500);
154
+ expect(emittedEvents[0]).toHaveProperty("executionTime", 120000);
155
+ expect(emittedEvents[0]).toHaveProperty("outputPath", "/tmp/results.json");
156
+ });
157
+ it("should handle FAIL status", () => {
158
+ emitAssessmentComplete("FAIL", 200, 60000, "/tmp/fail.json");
159
+ expect(emittedEvents[0]).toHaveProperty("overallStatus", "FAIL");
160
+ });
161
+ });
162
+ describe("emitTestBatch", () => {
163
+ it("should emit test_batch with progress info", () => {
164
+ emitTestBatch("security", 100, 500, 50, 30000);
165
+ expect(emittedEvents[0]).toHaveProperty("event", "test_batch");
166
+ expect(emittedEvents[0]).toHaveProperty("module", "security");
167
+ expect(emittedEvents[0]).toHaveProperty("completed", 100);
168
+ expect(emittedEvents[0]).toHaveProperty("total", 500);
169
+ expect(emittedEvents[0]).toHaveProperty("batchSize", 50);
170
+ expect(emittedEvents[0]).toHaveProperty("elapsed", 30000);
171
+ });
172
+ it("should handle final batch", () => {
173
+ emitTestBatch("functionality", 100, 100, 10, 5000);
174
+ expect(emittedEvents[0]).toHaveProperty("completed", 100);
175
+ expect(emittedEvents[0]).toHaveProperty("total", 100);
176
+ });
177
+ });
178
+ describe("emitVulnerabilityFound", () => {
179
+ it("should emit vulnerability_found with all fields", () => {
180
+ emitVulnerabilityFound("exec_tool", "Command Injection", "high", "Response executed shell command", "HIGH", true, "; rm -rf /");
181
+ expect(emittedEvents[0]).toHaveProperty("event", "vulnerability_found");
182
+ expect(emittedEvents[0]).toHaveProperty("tool", "exec_tool");
183
+ expect(emittedEvents[0]).toHaveProperty("pattern", "Command Injection");
184
+ expect(emittedEvents[0]).toHaveProperty("confidence", "high");
185
+ expect(emittedEvents[0]).toHaveProperty("evidence", "Response executed shell command");
186
+ expect(emittedEvents[0]).toHaveProperty("riskLevel", "HIGH");
187
+ expect(emittedEvents[0]).toHaveProperty("requiresReview", true);
188
+ expect(emittedEvents[0]).toHaveProperty("payload", "; rm -rf /");
189
+ });
190
+ it("should omit payload when not provided", () => {
191
+ emitVulnerabilityFound("tool", "SQLi", "medium", "evidence", "MEDIUM", false);
192
+ expect(emittedEvents[0]).not.toHaveProperty("payload");
193
+ });
194
+ it("should handle different confidence levels", () => {
195
+ emitVulnerabilityFound("t1", "p1", "high", "e1", "HIGH", true);
196
+ emitVulnerabilityFound("t2", "p2", "medium", "e2", "MEDIUM", false);
197
+ emitVulnerabilityFound("t3", "p3", "low", "e3", "LOW", false);
198
+ expect(emittedEvents[0]).toHaveProperty("confidence", "high");
199
+ expect(emittedEvents[1]).toHaveProperty("confidence", "medium");
200
+ expect(emittedEvents[2]).toHaveProperty("confidence", "low");
201
+ });
202
+ });
203
+ describe("emitAnnotationMissing", () => {
204
+ it("should emit annotation_missing with tool info", () => {
205
+ const params = [
206
+ { name: "file", type: "string", required: true },
207
+ ];
208
+ emitAnnotationMissing("delete_file", "Delete File", "Deletes a file", params, {
209
+ expectedReadOnly: false,
210
+ expectedDestructive: true,
211
+ reason: "delete operation implies destructive",
212
+ });
213
+ expect(emittedEvents[0]).toHaveProperty("event", "annotation_missing");
214
+ expect(emittedEvents[0]).toHaveProperty("tool", "delete_file");
215
+ expect(emittedEvents[0]).toHaveProperty("title", "Delete File");
216
+ expect(emittedEvents[0]).toHaveProperty("description", "Deletes a file");
217
+ expect(emittedEvents[0]).toHaveProperty("parameters", params);
218
+ expect(emittedEvents[0]).toHaveProperty("inferredBehavior");
219
+ });
220
+ it("should omit optional fields when undefined", () => {
221
+ emitAnnotationMissing("tool", undefined, undefined, [], {
222
+ expectedReadOnly: true,
223
+ expectedDestructive: false,
224
+ reason: "test",
225
+ });
226
+ expect(emittedEvents[0]).not.toHaveProperty("title");
227
+ expect(emittedEvents[0]).not.toHaveProperty("description");
228
+ });
229
+ });
230
+ describe("emitAnnotationMisaligned", () => {
231
+ it("should emit annotation_misaligned with all fields", () => {
232
+ const params = [];
233
+ emitAnnotationMisaligned("write_tool", "Write Data", "Writes to disk", params, "readOnlyHint", true, false, 0.95, "Tool performs write operations");
234
+ expect(emittedEvents[0]).toHaveProperty("event", "annotation_misaligned");
235
+ expect(emittedEvents[0]).toHaveProperty("tool", "write_tool");
236
+ expect(emittedEvents[0]).toHaveProperty("field", "readOnlyHint");
237
+ expect(emittedEvents[0]).toHaveProperty("actual", true);
238
+ expect(emittedEvents[0]).toHaveProperty("expected", false);
239
+ expect(emittedEvents[0]).toHaveProperty("confidence", 0.95);
240
+ expect(emittedEvents[0]).toHaveProperty("reason", "Tool performs write operations");
241
+ });
242
+ it("should handle undefined actual value", () => {
243
+ emitAnnotationMisaligned("tool", undefined, undefined, [], "destructiveHint", undefined, true, 0.8, "reason");
244
+ // JSON.stringify omits undefined values, so the property won't exist
245
+ expect(emittedEvents[0]).not.toHaveProperty("actual");
246
+ });
247
+ });
248
+ describe("emitAnnotationReviewRecommended", () => {
249
+ it("should emit annotation_review_recommended with all fields", () => {
250
+ emitAnnotationReviewRecommended("cache_tool", "Cache Manager", "Manages cache", [], "readOnlyHint", undefined, false, "medium", true, "Cache operations are ambiguous");
251
+ expect(emittedEvents[0]).toHaveProperty("event", "annotation_review_recommended");
252
+ expect(emittedEvents[0]).toHaveProperty("tool", "cache_tool");
253
+ expect(emittedEvents[0]).toHaveProperty("confidence", "medium");
254
+ expect(emittedEvents[0]).toHaveProperty("isAmbiguous", true);
255
+ });
256
+ });
257
+ describe("emitAnnotationAligned", () => {
258
+ it("should emit annotation_aligned with annotations", () => {
259
+ emitAnnotationAligned("read_tool", "high", {
260
+ readOnlyHint: true,
261
+ destructiveHint: false,
262
+ });
263
+ expect(emittedEvents[0]).toHaveProperty("event", "annotation_aligned");
264
+ expect(emittedEvents[0]).toHaveProperty("tool", "read_tool");
265
+ expect(emittedEvents[0]).toHaveProperty("confidence", "high");
266
+ expect(emittedEvents[0]).toHaveProperty("annotations");
267
+ });
268
+ });
269
+ describe("emitModulesConfigured", () => {
270
+ it("should emit modules_configured with enabled and skipped", () => {
271
+ emitModulesConfigured(["security", "functionality"], ["temporal"], "skip-modules");
272
+ expect(emittedEvents[0]).toHaveProperty("event", "modules_configured");
273
+ expect(emittedEvents[0]).toHaveProperty("enabled", [
274
+ "security",
275
+ "functionality",
276
+ ]);
277
+ expect(emittedEvents[0]).toHaveProperty("skipped", ["temporal"]);
278
+ expect(emittedEvents[0]).toHaveProperty("reason", "skip-modules");
279
+ });
280
+ it("should handle only-modules reason", () => {
281
+ emitModulesConfigured(["security"], [], "only-modules");
282
+ expect(emittedEvents[0]).toHaveProperty("reason", "only-modules");
283
+ });
284
+ it("should handle default reason", () => {
285
+ emitModulesConfigured(["security", "functionality"], [], "default");
286
+ expect(emittedEvents[0]).toHaveProperty("reason", "default");
287
+ });
288
+ });
289
+ });
290
+ describe("extractToolParams", () => {
291
+ it("should return empty array for null schema", () => {
292
+ expect(extractToolParams(null)).toEqual([]);
293
+ });
294
+ it("should return empty array for undefined schema", () => {
295
+ expect(extractToolParams(undefined)).toEqual([]);
296
+ });
297
+ it("should return empty array for non-object schema", () => {
298
+ expect(extractToolParams("string")).toEqual([]);
299
+ });
300
+ it("should return empty array for schema without properties", () => {
301
+ expect(extractToolParams({ type: "object" })).toEqual([]);
302
+ });
303
+ it("should extract parameters with all fields", () => {
304
+ const schema = {
305
+ type: "object",
306
+ properties: {
307
+ name: { type: "string", description: "User name" },
308
+ age: { type: "number" },
309
+ },
310
+ required: ["name"],
311
+ };
312
+ const params = extractToolParams(schema);
313
+ expect(params.length).toBe(2);
314
+ const nameParam = params.find((p) => p.name === "name");
315
+ expect(nameParam).toEqual({
316
+ name: "name",
317
+ type: "string",
318
+ required: true,
319
+ description: "User name",
320
+ });
321
+ const ageParam = params.find((p) => p.name === "age");
322
+ expect(ageParam).toEqual({
323
+ name: "age",
324
+ type: "number",
325
+ required: false,
326
+ });
327
+ });
328
+ it("should handle schema without required array", () => {
329
+ const schema = {
330
+ type: "object",
331
+ properties: {
332
+ optional: { type: "string" },
333
+ },
334
+ };
335
+ const params = extractToolParams(schema);
336
+ expect(params[0].required).toBe(false);
337
+ });
338
+ it("should default to 'any' type when not specified", () => {
339
+ const schema = {
340
+ type: "object",
341
+ properties: {
342
+ unknown: {},
343
+ },
344
+ };
345
+ const params = extractToolParams(schema);
346
+ expect(params[0].type).toBe("any");
347
+ });
348
+ it("should not include description when not present", () => {
349
+ const schema = {
350
+ type: "object",
351
+ properties: {
352
+ simple: { type: "boolean" },
353
+ },
354
+ };
355
+ const params = extractToolParams(schema);
356
+ expect(params[0]).not.toHaveProperty("description");
357
+ });
358
+ });
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Profile Module Unit Tests
3
+ *
4
+ * Tests for profile definitions, module resolution, and legacy config mapping.
5
+ */
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ import { jest, describe, it, expect } from "@jest/globals";
8
+ import { ASSESSMENT_PROFILES, PROFILE_METADATA, MODULE_ALIASES, DEPRECATED_MODULES, TIER_1_CORE_SECURITY, TIER_2_COMPLIANCE, TIER_3_CAPABILITY, TIER_4_EXTENDED, ALL_MODULES, resolveModuleNames, getProfileModules, isValidProfileName, getProfileHelpText, mapLegacyConfigToModules, modulesToLegacyConfig, } from "../profiles.js";
9
+ describe("Profile Definitions", () => {
10
+ describe("Profile Constants", () => {
11
+ it("should have four profiles defined", () => {
12
+ const profiles = Object.keys(ASSESSMENT_PROFILES);
13
+ expect(profiles).toEqual(["quick", "security", "compliance", "full"]);
14
+ });
15
+ it("should have metadata for all profiles", () => {
16
+ const profileNames = Object.keys(ASSESSMENT_PROFILES);
17
+ const metadataNames = Object.keys(PROFILE_METADATA);
18
+ expect(metadataNames).toEqual(profileNames);
19
+ });
20
+ it("should have correct module counts in metadata", () => {
21
+ for (const [name, meta] of Object.entries(PROFILE_METADATA)) {
22
+ const profile = ASSESSMENT_PROFILES[name];
23
+ expect(meta.moduleCount).toBe(profile.length);
24
+ }
25
+ });
26
+ });
27
+ describe("Tier Definitions", () => {
28
+ it("should have Tier 1 core security modules", () => {
29
+ expect(TIER_1_CORE_SECURITY).toContain("functionality");
30
+ expect(TIER_1_CORE_SECURITY).toContain("security");
31
+ expect(TIER_1_CORE_SECURITY).toContain("temporal");
32
+ expect(TIER_1_CORE_SECURITY).toContain("errorHandling");
33
+ expect(TIER_1_CORE_SECURITY).toContain("protocolCompliance");
34
+ expect(TIER_1_CORE_SECURITY).toContain("aupCompliance");
35
+ });
36
+ it("should have Tier 2 compliance modules", () => {
37
+ expect(TIER_2_COMPLIANCE).toContain("toolAnnotations");
38
+ expect(TIER_2_COMPLIANCE).toContain("prohibitedLibraries");
39
+ expect(TIER_2_COMPLIANCE).toContain("manifestValidation");
40
+ expect(TIER_2_COMPLIANCE).toContain("authentication");
41
+ });
42
+ it("should have Tier 3 capability modules", () => {
43
+ expect(TIER_3_CAPABILITY).toContain("resources");
44
+ expect(TIER_3_CAPABILITY).toContain("prompts");
45
+ expect(TIER_3_CAPABILITY).toContain("crossCapability");
46
+ });
47
+ it("should have Tier 4 extended modules", () => {
48
+ expect(TIER_4_EXTENDED).toContain("developerExperience");
49
+ expect(TIER_4_EXTENDED).toContain("portability");
50
+ expect(TIER_4_EXTENDED).toContain("externalAPIScanner");
51
+ });
52
+ it("should combine all tiers in ALL_MODULES", () => {
53
+ const expectedLength = TIER_1_CORE_SECURITY.length +
54
+ TIER_2_COMPLIANCE.length +
55
+ TIER_3_CAPABILITY.length +
56
+ TIER_4_EXTENDED.length;
57
+ expect(ALL_MODULES.length).toBe(expectedLength);
58
+ });
59
+ });
60
+ describe("Profile Compositions", () => {
61
+ it("quick profile should have functionality and security only", () => {
62
+ expect(ASSESSMENT_PROFILES.quick).toEqual(["functionality", "security"]);
63
+ });
64
+ it("security profile should include all Tier 1 modules", () => {
65
+ for (const module of TIER_1_CORE_SECURITY) {
66
+ expect(ASSESSMENT_PROFILES.security).toContain(module);
67
+ }
68
+ });
69
+ it("compliance profile should include Tier 1 and Tier 2", () => {
70
+ for (const module of TIER_1_CORE_SECURITY) {
71
+ expect(ASSESSMENT_PROFILES.compliance).toContain(module);
72
+ }
73
+ for (const module of TIER_2_COMPLIANCE) {
74
+ expect(ASSESSMENT_PROFILES.compliance).toContain(module);
75
+ }
76
+ });
77
+ it("full profile should include all tiers", () => {
78
+ for (const module of ALL_MODULES) {
79
+ expect(ASSESSMENT_PROFILES.full).toContain(module);
80
+ }
81
+ });
82
+ });
83
+ });
84
+ describe("Module Aliases", () => {
85
+ it("should map deprecated mcpSpecCompliance to protocolCompliance", () => {
86
+ expect(MODULE_ALIASES.mcpSpecCompliance).toBe("protocolCompliance");
87
+ });
88
+ it("should map deprecated protocolConformance to protocolCompliance", () => {
89
+ expect(MODULE_ALIASES.protocolConformance).toBe("protocolCompliance");
90
+ });
91
+ it("should map deprecated documentation to developerExperience", () => {
92
+ expect(MODULE_ALIASES.documentation).toBe("developerExperience");
93
+ });
94
+ it("should map deprecated usability to developerExperience", () => {
95
+ expect(MODULE_ALIASES.usability).toBe("developerExperience");
96
+ });
97
+ it("should have DEPRECATED_MODULES match MODULE_ALIASES keys", () => {
98
+ const aliasKeys = Object.keys(MODULE_ALIASES);
99
+ for (const key of aliasKeys) {
100
+ expect(DEPRECATED_MODULES.has(key)).toBe(true);
101
+ }
102
+ expect(DEPRECATED_MODULES.size).toBe(aliasKeys.length);
103
+ });
104
+ });
105
+ describe("resolveModuleNames", () => {
106
+ it("should return unchanged names for non-deprecated modules", () => {
107
+ const modules = ["functionality", "security", "temporal"];
108
+ const result = resolveModuleNames(modules, false);
109
+ expect(result).toEqual(modules);
110
+ });
111
+ it("should replace deprecated module names with aliases", () => {
112
+ const modules = ["mcpSpecCompliance", "documentation"];
113
+ const result = resolveModuleNames(modules, false);
114
+ expect(result).toContain("protocolCompliance");
115
+ expect(result).toContain("developerExperience");
116
+ expect(result).not.toContain("mcpSpecCompliance");
117
+ expect(result).not.toContain("documentation");
118
+ });
119
+ it("should deduplicate when deprecated and replacement both specified", () => {
120
+ const modules = ["mcpSpecCompliance", "protocolCompliance"];
121
+ const result = resolveModuleNames(modules, false);
122
+ expect(result.length).toBe(1);
123
+ expect(result).toContain("protocolCompliance");
124
+ });
125
+ it("should emit warnings for deprecated modules when warn=true", () => {
126
+ const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
127
+ const modules = ["documentation"];
128
+ resolveModuleNames(modules, true);
129
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("deprecated"));
130
+ consoleSpy.mockRestore();
131
+ });
132
+ it("should not emit warnings when warn=false", () => {
133
+ const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
134
+ const modules = ["documentation"];
135
+ resolveModuleNames(modules, false);
136
+ expect(consoleSpy).not.toHaveBeenCalled();
137
+ consoleSpy.mockRestore();
138
+ });
139
+ });
140
+ describe("getProfileModules", () => {
141
+ it("should return modules for quick profile", () => {
142
+ const modules = getProfileModules("quick");
143
+ expect(modules).toEqual(["functionality", "security"]);
144
+ });
145
+ it("should return modules for security profile", () => {
146
+ const modules = getProfileModules("security");
147
+ expect(modules.length).toBe(TIER_1_CORE_SECURITY.length);
148
+ for (const mod of TIER_1_CORE_SECURITY) {
149
+ expect(modules).toContain(mod);
150
+ }
151
+ });
152
+ it("should exclude temporal when skipTemporal=true", () => {
153
+ const modules = getProfileModules("security", { skipTemporal: true });
154
+ expect(modules).not.toContain("temporal");
155
+ });
156
+ it("should exclude externalAPIScanner when hasSourceCode=false", () => {
157
+ const modules = getProfileModules("full", { hasSourceCode: false });
158
+ expect(modules).not.toContain("externalAPIScanner");
159
+ });
160
+ it("should include externalAPIScanner when hasSourceCode=true", () => {
161
+ const modules = getProfileModules("full", { hasSourceCode: true });
162
+ expect(modules).toContain("externalAPIScanner");
163
+ });
164
+ it("should handle multiple options together", () => {
165
+ const modules = getProfileModules("full", {
166
+ skipTemporal: true,
167
+ hasSourceCode: false,
168
+ });
169
+ expect(modules).not.toContain("temporal");
170
+ expect(modules).not.toContain("externalAPIScanner");
171
+ });
172
+ });
173
+ describe("isValidProfileName", () => {
174
+ it("should return true for valid profile names", () => {
175
+ expect(isValidProfileName("quick")).toBe(true);
176
+ expect(isValidProfileName("security")).toBe(true);
177
+ expect(isValidProfileName("compliance")).toBe(true);
178
+ expect(isValidProfileName("full")).toBe(true);
179
+ });
180
+ it("should return false for invalid profile names", () => {
181
+ expect(isValidProfileName("invalid")).toBe(false);
182
+ expect(isValidProfileName("")).toBe(false);
183
+ expect(isValidProfileName("QUICK")).toBe(false);
184
+ expect(isValidProfileName("fast")).toBe(false);
185
+ });
186
+ });
187
+ describe("getProfileHelpText", () => {
188
+ it("should return non-empty string", () => {
189
+ const help = getProfileHelpText();
190
+ expect(help.length).toBeGreaterThan(0);
191
+ });
192
+ it("should contain all profile names", () => {
193
+ const help = getProfileHelpText();
194
+ expect(help).toContain("quick");
195
+ expect(help).toContain("security");
196
+ expect(help).toContain("compliance");
197
+ expect(help).toContain("full");
198
+ });
199
+ it("should contain module counts", () => {
200
+ const help = getProfileHelpText();
201
+ expect(help).toContain("Modules:");
202
+ });
203
+ it("should contain time estimates", () => {
204
+ const help = getProfileHelpText();
205
+ expect(help).toContain("Time:");
206
+ });
207
+ });
208
+ describe("mapLegacyConfigToModules", () => {
209
+ it("should return empty array for empty config", () => {
210
+ const result = mapLegacyConfigToModules({});
211
+ expect(result).toEqual([]);
212
+ });
213
+ it("should return enabled modules", () => {
214
+ const config = {
215
+ functionality: true,
216
+ security: true,
217
+ temporal: false,
218
+ };
219
+ const result = mapLegacyConfigToModules(config);
220
+ expect(result).toContain("functionality");
221
+ expect(result).toContain("security");
222
+ expect(result).not.toContain("temporal");
223
+ });
224
+ it("should apply aliases for deprecated names", () => {
225
+ const config = {
226
+ documentation: true,
227
+ mcpSpecCompliance: true,
228
+ };
229
+ const result = mapLegacyConfigToModules(config);
230
+ expect(result).toContain("developerExperience");
231
+ expect(result).toContain("protocolCompliance");
232
+ expect(result).not.toContain("documentation");
233
+ expect(result).not.toContain("mcpSpecCompliance");
234
+ });
235
+ it("should deduplicate when both deprecated and new names enabled", () => {
236
+ const config = {
237
+ documentation: true,
238
+ developerExperience: true,
239
+ };
240
+ const result = mapLegacyConfigToModules(config);
241
+ const devExpCount = result.filter((m) => m === "developerExperience").length;
242
+ expect(devExpCount).toBe(1);
243
+ });
244
+ });
245
+ describe("modulesToLegacyConfig", () => {
246
+ it("should return config with all modules disabled by default", () => {
247
+ const result = modulesToLegacyConfig([]);
248
+ expect(result.functionality).toBe(false);
249
+ expect(result.security).toBe(false);
250
+ });
251
+ it("should enable specified modules", () => {
252
+ const result = modulesToLegacyConfig(["functionality", "security"]);
253
+ expect(result.functionality).toBe(true);
254
+ expect(result.security).toBe(true);
255
+ expect(result.temporal).toBe(false);
256
+ });
257
+ it("should map protocolCompliance to both old modules", () => {
258
+ const result = modulesToLegacyConfig(["protocolCompliance"]);
259
+ expect(result.mcpSpecCompliance).toBe(true);
260
+ expect(result.protocolConformance).toBe(true);
261
+ });
262
+ it("should map developerExperience to both old modules", () => {
263
+ const result = modulesToLegacyConfig(["developerExperience"]);
264
+ expect(result.documentation).toBe(true);
265
+ expect(result.usability).toBe(true);
266
+ });
267
+ it("should handle full profile modules", () => {
268
+ const modules = getProfileModules("full", { hasSourceCode: true });
269
+ const result = modulesToLegacyConfig(modules);
270
+ expect(result.functionality).toBe(true);
271
+ expect(result.security).toBe(true);
272
+ expect(result.temporal).toBe(true);
273
+ expect(result.toolAnnotations).toBe(true);
274
+ });
275
+ });
276
+ describe("Profile Metadata", () => {
277
+ it("should have descriptions for all profiles", () => {
278
+ for (const meta of Object.values(PROFILE_METADATA)) {
279
+ expect(meta.description).toBeTruthy();
280
+ expect(typeof meta.description).toBe("string");
281
+ }
282
+ });
283
+ it("should have estimated times for all profiles", () => {
284
+ for (const meta of Object.values(PROFILE_METADATA)) {
285
+ expect(meta.estimatedTime).toBeTruthy();
286
+ expect(meta.estimatedTime).toMatch(/~\d+-?\d*\s*minutes?/);
287
+ }
288
+ });
289
+ it("should have tier information for all profiles", () => {
290
+ for (const meta of Object.values(PROFILE_METADATA)) {
291
+ expect(Array.isArray(meta.tiers)).toBe(true);
292
+ expect(meta.tiers.length).toBeGreaterThan(0);
293
+ }
294
+ });
295
+ it("should have correct tier counts", () => {
296
+ expect(PROFILE_METADATA.quick.tiers.length).toBe(1);
297
+ expect(PROFILE_METADATA.security.tiers.length).toBe(1);
298
+ expect(PROFILE_METADATA.compliance.tiers.length).toBe(2);
299
+ expect(PROFILE_METADATA.full.tiers.length).toBe(4);
300
+ });
301
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Transport Module Unit Tests
3
+ *
4
+ * Tests for transport creation and configuration validation.
5
+ * Tests focus on input validation and error handling rather than
6
+ * mocked transport implementations due to ESM limitations.
7
+ */
8
+ import { describe, it, expect } from "@jest/globals";
9
+ import { createTransport } from "../transport.js";
10
+ describe("Transport Creation", () => {
11
+ describe("Input Validation", () => {
12
+ it("should throw error when URL is missing for HTTP transport", () => {
13
+ const options = {
14
+ transportType: "http",
15
+ };
16
+ expect(() => createTransport(options)).toThrow(/URL must be provided for SSE or HTTP transport types/);
17
+ });
18
+ it("should throw error when URL is missing for SSE transport", () => {
19
+ const options = {
20
+ transportType: "sse",
21
+ };
22
+ expect(() => createTransport(options)).toThrow(/URL must be provided for SSE or HTTP transport types/);
23
+ });
24
+ it("should throw error for invalid URL format", () => {
25
+ const options = {
26
+ transportType: "http",
27
+ url: ":::invalid-url",
28
+ };
29
+ expect(() => createTransport(options)).toThrow(/Failed to create transport/);
30
+ });
31
+ it("should throw error for unsupported transport type", () => {
32
+ const options = {
33
+ transportType: "websocket",
34
+ url: "ws://localhost:3000",
35
+ };
36
+ expect(() => createTransport(options)).toThrow(/Unsupported transport type/);
37
+ });
38
+ });
39
+ describe("STDIO Transport", () => {
40
+ it("should create transport for valid stdio options", () => {
41
+ const options = {
42
+ transportType: "stdio",
43
+ command: "python",
44
+ args: ["server.py"],
45
+ };
46
+ // Should not throw
47
+ const transport = createTransport(options);
48
+ expect(transport).toBeDefined();
49
+ });
50
+ it("should handle missing args", () => {
51
+ const options = {
52
+ transportType: "stdio",
53
+ command: "node",
54
+ };
55
+ const transport = createTransport(options);
56
+ expect(transport).toBeDefined();
57
+ });
58
+ it("should handle empty command", () => {
59
+ const options = {
60
+ transportType: "stdio",
61
+ command: "",
62
+ };
63
+ // Should not throw - empty command is handled by findActualExecutable
64
+ const transport = createTransport(options);
65
+ expect(transport).toBeDefined();
66
+ });
67
+ });
68
+ describe("HTTP Transport", () => {
69
+ it("should create transport for valid HTTP options", () => {
70
+ const options = {
71
+ transportType: "http",
72
+ url: "http://localhost:3000/mcp",
73
+ };
74
+ const transport = createTransport(options);
75
+ expect(transport).toBeDefined();
76
+ });
77
+ it("should create transport with headers", () => {
78
+ const options = {
79
+ transportType: "http",
80
+ url: "http://localhost:3000/mcp",
81
+ headers: {
82
+ Authorization: "Bearer token",
83
+ },
84
+ };
85
+ const transport = createTransport(options);
86
+ expect(transport).toBeDefined();
87
+ });
88
+ it("should handle HTTPS URLs", () => {
89
+ const options = {
90
+ transportType: "http",
91
+ url: "https://api.example.com/mcp",
92
+ };
93
+ const transport = createTransport(options);
94
+ expect(transport).toBeDefined();
95
+ });
96
+ });
97
+ describe("SSE Transport", () => {
98
+ it("should create transport for valid SSE options", () => {
99
+ const options = {
100
+ transportType: "sse",
101
+ url: "http://localhost:3000/sse",
102
+ };
103
+ const transport = createTransport(options);
104
+ expect(transport).toBeDefined();
105
+ });
106
+ it("should create transport with headers", () => {
107
+ const options = {
108
+ transportType: "sse",
109
+ url: "http://localhost:3000/sse",
110
+ headers: {
111
+ "X-API-Key": "secret",
112
+ },
113
+ };
114
+ const transport = createTransport(options);
115
+ expect(transport).toBeDefined();
116
+ });
117
+ });
118
+ });
119
+ describe("TransportOptions Type", () => {
120
+ it("should accept all valid transport types", () => {
121
+ const stdioOptions = {
122
+ transportType: "stdio",
123
+ command: "node",
124
+ args: ["server.js"],
125
+ };
126
+ const httpOptions = {
127
+ transportType: "http",
128
+ url: "http://localhost:3000/mcp",
129
+ };
130
+ const sseOptions = {
131
+ transportType: "sse",
132
+ url: "http://localhost:3000/sse",
133
+ };
134
+ // Type checking - these should compile without errors
135
+ expect(stdioOptions.transportType).toBe("stdio");
136
+ expect(httpOptions.transportType).toBe("http");
137
+ expect(sseOptions.transportType).toBe("sse");
138
+ });
139
+ it("should allow optional headers for HTTP/SSE", () => {
140
+ const withHeaders = {
141
+ transportType: "http",
142
+ url: "http://localhost:3000/mcp",
143
+ headers: { "Content-Type": "application/json" },
144
+ };
145
+ const withoutHeaders = {
146
+ transportType: "http",
147
+ url: "http://localhost:3000/mcp",
148
+ };
149
+ expect(withHeaders.headers).toBeDefined();
150
+ expect(withoutHeaders.headers).toBeUndefined();
151
+ });
152
+ it("should allow optional command args", () => {
153
+ const withArgs = {
154
+ transportType: "stdio",
155
+ command: "python",
156
+ args: ["-m", "server"],
157
+ };
158
+ const withoutArgs = {
159
+ transportType: "stdio",
160
+ command: "python",
161
+ };
162
+ expect(withArgs.args).toEqual(["-m", "server"]);
163
+ expect(withoutArgs.args).toBeUndefined();
164
+ });
165
+ });
@@ -1010,7 +1010,8 @@ function parseArgs() {
1010
1010
  }
1011
1011
  }
1012
1012
  // Validate mutual exclusivity of --profile, --skip-modules, and --only-modules
1013
- if (options.profile && (options.skipModules?.length || options.onlyModules?.length)) {
1013
+ if (options.profile &&
1014
+ (options.skipModules?.length || options.onlyModules?.length)) {
1014
1015
  console.error("Error: --profile cannot be used with --skip-modules or --only-modules");
1015
1016
  setTimeout(() => process.exit(1), 10);
1016
1017
  options.helpRequested = true;
package/build/profiles.js CHANGED
@@ -86,30 +86,34 @@ export const ALL_MODULES = [
86
86
  /**
87
87
  * Assessment profile definitions
88
88
  * Each profile includes a specific set of modules optimized for the use case.
89
+ *
90
+ * Note: Time estimates are based on testing a server with ~30 tools.
91
+ * The SecurityAssessor runs 23 attack patterns per tool (~3400+ tests),
92
+ * which dominates runtime across all profiles that include it.
89
93
  */
90
94
  export const ASSESSMENT_PROFILES = {
91
95
  /**
92
96
  * Quick profile: Minimal testing for fast CI/CD checks
93
97
  * Use when: Pre-commit hooks, quick validation
94
- * Time: ~30 seconds
98
+ * Time: ~3-4 minutes (security module dominates)
95
99
  */
96
100
  quick: ["functionality", "security"],
97
101
  /**
98
102
  * Security profile: Core security modules (Tier 1)
99
103
  * Use when: Security-focused audits, vulnerability scanning
100
- * Time: ~2-3 minutes
104
+ * Time: ~8-10 minutes
101
105
  */
102
106
  security: [...TIER_1_CORE_SECURITY],
103
107
  /**
104
108
  * Compliance profile: Security + Directory compliance (Tier 1 + 2)
105
109
  * Use when: Pre-submission validation for MCP Directory
106
- * Time: ~5 minutes
110
+ * Time: ~8-10 minutes
107
111
  */
108
112
  compliance: [...TIER_1_CORE_SECURITY, ...TIER_2_COMPLIANCE],
109
113
  /**
110
114
  * Full profile: All modules (Tier 1 + 2 + 3 + 4)
111
115
  * Use when: Comprehensive audits, initial server review
112
- * Time: ~10-15 minutes
116
+ * Time: ~8-12 minutes
113
117
  */
114
118
  full: [
115
119
  ...TIER_1_CORE_SECURITY,
@@ -121,25 +125,25 @@ export const ASSESSMENT_PROFILES = {
121
125
  export const PROFILE_METADATA = {
122
126
  quick: {
123
127
  description: "Fast validation (functionality + security only)",
124
- estimatedTime: "~30 seconds",
128
+ estimatedTime: "~3-4 minutes",
125
129
  moduleCount: ASSESSMENT_PROFILES.quick.length,
126
130
  tiers: ["Tier 1 (partial)"],
127
131
  },
128
132
  security: {
129
133
  description: "Core security modules for vulnerability scanning",
130
- estimatedTime: "~2-3 minutes",
134
+ estimatedTime: "~8-10 minutes",
131
135
  moduleCount: ASSESSMENT_PROFILES.security.length,
132
136
  tiers: ["Tier 1 (Core Security)"],
133
137
  },
134
138
  compliance: {
135
139
  description: "Security + MCP Directory compliance validation",
136
- estimatedTime: "~5 minutes",
140
+ estimatedTime: "~8-10 minutes",
137
141
  moduleCount: ASSESSMENT_PROFILES.compliance.length,
138
142
  tiers: ["Tier 1 (Core Security)", "Tier 2 (Compliance)"],
139
143
  },
140
144
  full: {
141
145
  description: "Comprehensive audit with all assessment modules",
142
- estimatedTime: "~10-15 minutes",
146
+ estimatedTime: "~8-12 minutes",
143
147
  moduleCount: ASSESSMENT_PROFILES.full.length,
144
148
  tiers: [
145
149
  "Tier 1 (Core Security)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.25.0",
3
+ "version": "1.25.4",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",
@@ -30,13 +30,19 @@
30
30
  "scripts": {
31
31
  "build": "tsc",
32
32
  "postbuild": "node scripts/make-executable.js",
33
- "test": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js && node scripts/cli-header-tests.js && node scripts/cli-validation-tests.js",
33
+ "test": "npm run test:unit && npm run test:integration",
34
+ "test:unit": "node --experimental-vm-modules ../node_modules/.bin/jest --config jest.config.cjs",
35
+ "test:integration": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js && node scripts/cli-header-tests.js && node scripts/cli-validation-tests.js",
34
36
  "test:cli": "node scripts/cli-tests.js",
35
37
  "test:cli-tools": "node scripts/cli-tool-tests.js",
36
38
  "test:cli-headers": "node scripts/cli-header-tests.js",
37
39
  "test:cli-validation": "node scripts/cli-validation-tests.js"
38
40
  },
39
- "devDependencies": {},
41
+ "devDependencies": {
42
+ "@types/jest": "^29.5.14",
43
+ "jest": "^29.7.0",
44
+ "ts-jest": "^29.2.0"
45
+ },
40
46
  "dependencies": {
41
47
  "@bryan-thompson/inspector-assessment-client": "^1.5.0",
42
48
  "@modelcontextprotocol/sdk": "^1.24.3",