@bryan-thompson/inspector-assessment-cli 1.43.1 → 1.43.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.
@@ -11,7 +11,7 @@ describe("assessment-runner index exports", () => {
11
11
  jest.clearAllMocks();
12
12
  });
13
13
  describe("function exports", () => {
14
- it("should export all 7 public functions", () => {
14
+ it("should export all 10 public functions", () => {
15
15
  expect(typeof assessmentRunner.loadServerConfig).toBe("function");
16
16
  expect(typeof assessmentRunner.loadSourceFiles).toBe("function");
17
17
  expect(typeof assessmentRunner.resolveSourcePath).toBe("function");
@@ -19,19 +19,27 @@ describe("assessment-runner index exports", () => {
19
19
  expect(typeof assessmentRunner.createCallToolWrapper).toBe("function");
20
20
  expect(typeof assessmentRunner.buildConfig).toBe("function");
21
21
  expect(typeof assessmentRunner.runFullAssessment).toBe("function");
22
+ // Issue #184: Single module execution
23
+ expect(typeof assessmentRunner.runSingleModule).toBe("function");
24
+ expect(typeof assessmentRunner.getValidModuleNames).toBe("function");
25
+ // Issue #212: Native module detection
26
+ expect(typeof assessmentRunner.detectNativeModules).toBe("function");
22
27
  });
23
- it("should export exactly 7 functions", () => {
28
+ it("should export exactly 10 functions", () => {
24
29
  const functionNames = Object.keys(assessmentRunner).filter((key) => typeof assessmentRunner[key] ===
25
30
  "function");
26
- expect(functionNames).toHaveLength(7);
31
+ expect(functionNames).toHaveLength(10);
27
32
  expect(functionNames.sort()).toEqual([
28
33
  "buildConfig",
29
34
  "connectToServer",
30
35
  "createCallToolWrapper",
36
+ "detectNativeModules", // Issue #212
37
+ "getValidModuleNames", // Issue #184
31
38
  "loadServerConfig",
32
39
  "loadSourceFiles",
33
40
  "resolveSourcePath",
34
41
  "runFullAssessment",
42
+ "runSingleModule", // Issue #184
35
43
  ]);
36
44
  });
37
45
  });
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Native Module Detector Tests
3
+ *
4
+ * Tests for the pre-flight native module detection that warns about
5
+ * potential issues before MCP server connection.
6
+ *
7
+ * @see https://github.com/triepod-ai/inspector-assessment/issues/212
8
+ */
9
+ import { describe, it, expect, jest, beforeEach, afterEach, } from "@jest/globals";
10
+ import { detectNativeModules, } from "../../lib/assessment-runner/native-module-detector.js";
11
+ // Mock the jsonl-events module
12
+ jest.mock("../../lib/jsonl-events.js", () => ({
13
+ emitNativeModuleWarning: jest.fn(),
14
+ }));
15
+ // Import the mocked function for assertions
16
+ import { emitNativeModuleWarning } from "../../lib/jsonl-events.js";
17
+ describe("native-module-detector", () => {
18
+ // Store original console methods
19
+ let consoleLogSpy;
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ // Mock console.log to suppress output during tests
23
+ consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => { });
24
+ });
25
+ afterEach(() => {
26
+ consoleLogSpy.mockRestore();
27
+ });
28
+ describe("detectNativeModules", () => {
29
+ it("should return empty result for undefined packageJson", () => {
30
+ const result = detectNativeModules(undefined, { jsonOnly: true });
31
+ expect(result.detected).toBe(false);
32
+ expect(result.count).toBe(0);
33
+ expect(result.modules).toHaveLength(0);
34
+ expect(Object.keys(result.suggestedEnvVars)).toHaveLength(0);
35
+ expect(emitNativeModuleWarning).not.toHaveBeenCalled();
36
+ });
37
+ it("should return empty result for package without native modules", () => {
38
+ const packageJson = {
39
+ dependencies: {
40
+ express: "^4.18.0",
41
+ lodash: "^4.17.21",
42
+ },
43
+ };
44
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
45
+ expect(result.detected).toBe(false);
46
+ expect(result.count).toBe(0);
47
+ expect(result.modules).toHaveLength(0);
48
+ expect(emitNativeModuleWarning).not.toHaveBeenCalled();
49
+ });
50
+ it("should detect canvas in dependencies", () => {
51
+ const packageJson = {
52
+ dependencies: { canvas: "^2.11.0" },
53
+ };
54
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
55
+ expect(result.detected).toBe(true);
56
+ expect(result.count).toBe(1);
57
+ expect(result.modules).toHaveLength(1);
58
+ expect(result.modules[0].name).toBe("canvas");
59
+ expect(result.modules[0].category).toBe("image");
60
+ expect(result.modules[0].severity).toBe("HIGH");
61
+ expect(result.modules[0].dependencyType).toBe("dependencies");
62
+ expect(result.modules[0].version).toBe("^2.11.0");
63
+ });
64
+ it("should emit JSONL event for each detected module", () => {
65
+ const packageJson = {
66
+ dependencies: { canvas: "^2.11.0" },
67
+ };
68
+ detectNativeModules(packageJson, { jsonOnly: true });
69
+ expect(emitNativeModuleWarning).toHaveBeenCalledTimes(1);
70
+ expect(emitNativeModuleWarning).toHaveBeenCalledWith("canvas", "image", "HIGH", expect.stringContaining("Cairo"), "dependencies", "^2.11.0", expect.objectContaining({ CANVAS_BACKEND: "mock" }));
71
+ });
72
+ it("should detect multiple native modules", () => {
73
+ const packageJson = {
74
+ dependencies: {
75
+ canvas: "^2.11.0",
76
+ sharp: "^0.32.0",
77
+ },
78
+ };
79
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
80
+ expect(result.detected).toBe(true);
81
+ expect(result.count).toBe(2);
82
+ expect(result.modules).toHaveLength(2);
83
+ const names = result.modules.map((m) => m.name);
84
+ expect(names).toContain("canvas");
85
+ expect(names).toContain("sharp");
86
+ expect(emitNativeModuleWarning).toHaveBeenCalledTimes(2);
87
+ });
88
+ it("should collect suggested env vars from all modules", () => {
89
+ const packageJson = {
90
+ dependencies: {
91
+ canvas: "^2.11.0",
92
+ sharp: "^0.32.0",
93
+ "maplibre-gl-native": "^1.0.0",
94
+ },
95
+ };
96
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
97
+ expect(result.suggestedEnvVars).toEqual({
98
+ CANVAS_BACKEND: "mock",
99
+ SHARP_IGNORE_GLOBAL_LIBVIPS: "1",
100
+ ENABLE_DYNAMIC_MAPS: "false",
101
+ });
102
+ });
103
+ it("should detect modules in devDependencies", () => {
104
+ const packageJson = {
105
+ devDependencies: { "better-sqlite3": "^9.0.0" },
106
+ };
107
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
108
+ expect(result.detected).toBe(true);
109
+ expect(result.modules[0].dependencyType).toBe("devDependencies");
110
+ });
111
+ it("should detect modules in optionalDependencies", () => {
112
+ const packageJson = {
113
+ optionalDependencies: { bcrypt: "^5.1.0" },
114
+ };
115
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
116
+ expect(result.detected).toBe(true);
117
+ expect(result.modules[0].dependencyType).toBe("optionalDependencies");
118
+ });
119
+ it("should suppress console output when jsonOnly is true", () => {
120
+ const packageJson = {
121
+ dependencies: { canvas: "^2.11.0" },
122
+ };
123
+ detectNativeModules(packageJson, { jsonOnly: true });
124
+ // Console should not have been called (except for the mock)
125
+ expect(consoleLogSpy).not.toHaveBeenCalled();
126
+ });
127
+ it("should print console warnings when jsonOnly is false", () => {
128
+ const packageJson = {
129
+ dependencies: { canvas: "^2.11.0" },
130
+ };
131
+ detectNativeModules(packageJson, { jsonOnly: false });
132
+ // Console should have been called with warning message
133
+ expect(consoleLogSpy).toHaveBeenCalled();
134
+ const allCalls = consoleLogSpy.mock.calls.flat().join(" ");
135
+ expect(allCalls).toContain("Native Module Warning");
136
+ });
137
+ it("should handle modules without suggested env vars", () => {
138
+ const packageJson = {
139
+ dependencies: { "better-sqlite3": "^9.0.0" },
140
+ };
141
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
142
+ expect(result.detected).toBe(true);
143
+ expect(result.modules[0].suggestedEnvVars).toBeUndefined();
144
+ // Env vars should be empty since better-sqlite3 has no suggestions
145
+ expect(Object.keys(result.suggestedEnvVars)).toHaveLength(0);
146
+ });
147
+ it("should handle empty dependencies objects", () => {
148
+ const packageJson = {
149
+ dependencies: {},
150
+ devDependencies: {},
151
+ };
152
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
153
+ expect(result.detected).toBe(false);
154
+ expect(result.count).toBe(0);
155
+ });
156
+ it("should include correct severity icons in console output", () => {
157
+ const packageJson = {
158
+ dependencies: {
159
+ canvas: "^2.11.0", // HIGH severity
160
+ bcrypt: "^5.1.0", // MEDIUM severity
161
+ },
162
+ };
163
+ detectNativeModules(packageJson, { jsonOnly: false });
164
+ // Check that console.log was called
165
+ expect(consoleLogSpy).toHaveBeenCalled();
166
+ });
167
+ });
168
+ describe("return type shape", () => {
169
+ it("should return correct result shape for detected modules", () => {
170
+ const packageJson = {
171
+ dependencies: { canvas: "^2.11.0" },
172
+ };
173
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
174
+ // Verify all expected properties exist
175
+ expect(result).toHaveProperty("detected");
176
+ expect(result).toHaveProperty("count");
177
+ expect(result).toHaveProperty("modules");
178
+ expect(result).toHaveProperty("suggestedEnvVars");
179
+ // Verify types
180
+ expect(typeof result.detected).toBe("boolean");
181
+ expect(typeof result.count).toBe("number");
182
+ expect(Array.isArray(result.modules)).toBe(true);
183
+ expect(typeof result.suggestedEnvVars).toBe("object");
184
+ });
185
+ it("should return correct module shape", () => {
186
+ const packageJson = {
187
+ dependencies: { canvas: "^2.11.0" },
188
+ };
189
+ const result = detectNativeModules(packageJson, { jsonOnly: true });
190
+ const module = result.modules[0];
191
+ expect(module).toHaveProperty("name");
192
+ expect(module).toHaveProperty("category");
193
+ expect(module).toHaveProperty("severity");
194
+ expect(module).toHaveProperty("warningMessage");
195
+ expect(module).toHaveProperty("dependencyType");
196
+ expect(module).toHaveProperty("version");
197
+ });
198
+ });
199
+ });
@@ -123,7 +123,7 @@ describe("connectToServer", () => {
123
123
  };
124
124
  await expect(connectToServer(config)).rejects.toThrow("Command required for stdio transport");
125
125
  });
126
- it("should merge process.env with config.env", async () => {
126
+ it("should pass minimal env vars plus config.env overrides", async () => {
127
127
  const originalEnv = process.env;
128
128
  process.env = { PATH: "/usr/bin", HOME: "/home/user" };
129
129
  const config = {
@@ -134,11 +134,40 @@ describe("connectToServer", () => {
134
134
  };
135
135
  await connectToServer(config);
136
136
  const callArg = StdioClientTransport.mock.calls[0][0];
137
+ // Minimal env vars (PATH, HOME) should be present
137
138
  expect(callArg.env).toEqual(expect.objectContaining({
138
139
  PATH: "/usr/bin",
139
140
  HOME: "/home/user",
140
141
  CUSTOM_VAR: "value",
141
142
  }));
143
+ // NODE_ENV should default to "production" when not set
144
+ expect(callArg.env.NODE_ENV).toBe("production");
145
+ process.env = originalEnv;
146
+ });
147
+ it("should NOT pass arbitrary process.env vars (Issue #211)", async () => {
148
+ const originalEnv = process.env;
149
+ process.env = {
150
+ PATH: "/usr/bin",
151
+ HOME: "/home/user",
152
+ SOME_RANDOM_VAR: "should-not-pass",
153
+ ENABLE_DYNAMIC_MAPS: "true", // This caused TomTom MCP issues
154
+ AWS_ACCESS_KEY_ID: "secret", // Sensitive vars should not leak
155
+ };
156
+ const config = {
157
+ transport: "stdio",
158
+ command: "node",
159
+ args: [],
160
+ env: {},
161
+ };
162
+ await connectToServer(config);
163
+ const callArg = StdioClientTransport.mock.calls[0][0];
164
+ // These arbitrary vars should NOT be in the env
165
+ expect(callArg.env.SOME_RANDOM_VAR).toBeUndefined();
166
+ expect(callArg.env.ENABLE_DYNAMIC_MAPS).toBeUndefined();
167
+ expect(callArg.env.AWS_ACCESS_KEY_ID).toBeUndefined();
168
+ // But essential vars should still be present
169
+ expect(callArg.env.PATH).toBe("/usr/bin");
170
+ expect(callArg.env.HOME).toBe("/home/user");
142
171
  process.env = originalEnv;
143
172
  });
144
173
  it("should setup stderr listener before connecting", async () => {
@@ -226,4 +255,158 @@ describe("connectToServer", () => {
226
255
  await expect(connectToServer(config)).rejects.toThrow("Failed to connect to MCP server: Connection timeout");
227
256
  });
228
257
  });
258
+ // ============================================================================
259
+ // GAP-002: SIGKILL Detection Tests (Issue #212)
260
+ // ============================================================================
261
+ describe("SIGKILL detection (Issue #212)", () => {
262
+ it('should detect SIGKILL from "exit code 137" error message', async () => {
263
+ let stderrCallback = () => { };
264
+ mockStdioTransport.stderr.on.mockImplementation((event, cb) => {
265
+ if (event === "data") {
266
+ stderrCallback = cb;
267
+ }
268
+ });
269
+ mockConnect.mockImplementation(async () => {
270
+ throw new Error("Process exited with exit code 137");
271
+ });
272
+ const config = {
273
+ transport: "stdio",
274
+ command: "node",
275
+ args: ["server.js"],
276
+ };
277
+ await expect(connectToServer(config)).rejects.toThrow(/Exit code 137 \(SIGKILL\) detected/);
278
+ });
279
+ it('should detect SIGKILL from "SIGKILL" in error message', async () => {
280
+ mockConnect.mockRejectedValue(new Error("Process killed with SIGKILL"));
281
+ const config = {
282
+ transport: "stdio",
283
+ command: "python",
284
+ args: ["server.py"],
285
+ };
286
+ await expect(connectToServer(config)).rejects.toThrow(/Exit code 137 \(SIGKILL\) detected/);
287
+ });
288
+ it('should detect SIGKILL from "killed" (lowercase) in error message', async () => {
289
+ mockConnect.mockRejectedValue(new Error("Server process was killed unexpectedly"));
290
+ const config = {
291
+ transport: "stdio",
292
+ command: "node",
293
+ args: [],
294
+ };
295
+ await expect(connectToServer(config)).rejects.toThrow(/Exit code 137 \(SIGKILL\) detected/);
296
+ });
297
+ it("should NOT trigger SIGKILL help for regular errors", async () => {
298
+ mockConnect.mockRejectedValue(new Error("Connection refused"));
299
+ const config = {
300
+ transport: "stdio",
301
+ command: "node",
302
+ args: [],
303
+ };
304
+ const error = await connectToServer(config).catch((e) => e);
305
+ expect(error.message).not.toMatch(/Exit code 137 \(SIGKILL\) detected/);
306
+ expect(error.message).toMatch(/Failed to connect to MCP server/);
307
+ expect(error.message).toMatch(/Common causes/);
308
+ });
309
+ it("should include xattr suggestion in SIGKILL error message", async () => {
310
+ mockConnect.mockRejectedValue(new Error("exit code 137"));
311
+ const config = {
312
+ transport: "stdio",
313
+ command: "node",
314
+ args: [],
315
+ };
316
+ await expect(connectToServer(config)).rejects.toThrow(/xattr -d com\.apple\.quarantine/);
317
+ });
318
+ it("should include Security & Privacy suggestion in SIGKILL error message", async () => {
319
+ mockConnect.mockRejectedValue(new Error("Process killed with SIGKILL"));
320
+ const config = {
321
+ transport: "stdio",
322
+ command: "node",
323
+ args: [],
324
+ };
325
+ await expect(connectToServer(config)).rejects.toThrow(/System Preferences > Security & Privacy/);
326
+ });
327
+ it("should mention Gatekeeper in SIGKILL error message", async () => {
328
+ mockConnect.mockRejectedValue(new Error("exit code 137"));
329
+ const config = {
330
+ transport: "stdio",
331
+ command: "node",
332
+ args: [],
333
+ };
334
+ await expect(connectToServer(config)).rejects.toThrow(/macOS Gatekeeper blocked unsigned native binaries/);
335
+ });
336
+ it("should mention native module examples in SIGKILL error message", async () => {
337
+ mockConnect.mockRejectedValue(new Error("killed"));
338
+ const config = {
339
+ transport: "stdio",
340
+ command: "node",
341
+ args: [],
342
+ };
343
+ await expect(connectToServer(config)).rejects.toThrow(/canvas, sharp, better-sqlite3/);
344
+ });
345
+ it("should mention pre-flight warnings in SIGKILL error message", async () => {
346
+ mockConnect.mockRejectedValue(new Error("SIGKILL"));
347
+ const config = {
348
+ transport: "stdio",
349
+ command: "node",
350
+ args: [],
351
+ };
352
+ await expect(connectToServer(config)).rejects.toThrow(/Check pre-flight warnings above for detected native modules/);
353
+ });
354
+ it("should suggest pure JavaScript alternatives in SIGKILL error message", async () => {
355
+ mockConnect.mockRejectedValue(new Error("exit code 137"));
356
+ const config = {
357
+ transport: "stdio",
358
+ command: "node",
359
+ args: [],
360
+ };
361
+ await expect(connectToServer(config)).rejects.toThrow(/jimp instead of sharp/);
362
+ });
363
+ it("should include both stderr and SIGKILL help when process is killed", async () => {
364
+ let stderrCallback = () => { };
365
+ mockStdioTransport.stderr.on.mockImplementation((event, cb) => {
366
+ if (event === "data") {
367
+ stderrCallback = cb;
368
+ }
369
+ });
370
+ mockConnect.mockImplementation(async () => {
371
+ stderrCallback(Buffer.from("Loading canvas module..."));
372
+ throw new Error("Process killed (exit code 137)");
373
+ });
374
+ const config = {
375
+ transport: "stdio",
376
+ command: "node",
377
+ args: ["server.js"],
378
+ };
379
+ const error = await connectToServer(config).catch((e) => e);
380
+ expect(error.message).toMatch(/Loading canvas module/);
381
+ expect(error.message).toMatch(/Exit code 137 \(SIGKILL\) detected/);
382
+ expect(error.message).toMatch(/macOS Gatekeeper/);
383
+ expect(error.message).toMatch(/xattr/);
384
+ });
385
+ it("should handle SIGKILL detection for HTTP transport", async () => {
386
+ // SIGKILL can occur during startup even for HTTP servers
387
+ mockConnect.mockRejectedValue(new Error("Server startup failed: exit code 137"));
388
+ const config = {
389
+ transport: "http",
390
+ url: "http://localhost:8080",
391
+ };
392
+ await expect(connectToServer(config)).rejects.toThrow(/Exit code 137 \(SIGKILL\) detected/);
393
+ });
394
+ it("should handle SIGKILL detection for SSE transport", async () => {
395
+ mockConnect.mockRejectedValue(new Error("Connection killed"));
396
+ const config = {
397
+ transport: "sse",
398
+ url: "http://localhost:3000/events",
399
+ };
400
+ await expect(connectToServer(config)).rejects.toThrow(/Exit code 137 \(SIGKILL\) detected/);
401
+ });
402
+ it("should mention OOM as possible cause in SIGKILL error", async () => {
403
+ mockConnect.mockRejectedValue(new Error("SIGKILL"));
404
+ const config = {
405
+ transport: "stdio",
406
+ command: "node",
407
+ args: [],
408
+ };
409
+ await expect(connectToServer(config)).rejects.toThrow(/Out of memory during native module initialization/);
410
+ });
411
+ });
229
412
  });
@@ -82,17 +82,20 @@ describe("Assessment Runner Facade", () => {
82
82
  });
83
83
  });
84
84
  describe("Export Completeness", () => {
85
- it("should export exactly 6 functions", () => {
85
+ it("should export exactly 10 functions", () => {
86
86
  const exportedFunctions = Object.entries(AssessmentRunner).filter(([, value]) => typeof value === "function");
87
- expect(exportedFunctions.length).toBe(7);
87
+ expect(exportedFunctions.length).toBe(10);
88
88
  expect(exportedFunctions.map(([name]) => name).sort()).toEqual([
89
89
  "buildConfig",
90
90
  "connectToServer",
91
91
  "createCallToolWrapper",
92
+ "detectNativeModules", // Issue #212
93
+ "getValidModuleNames", // Issue #184
92
94
  "loadServerConfig",
93
95
  "loadSourceFiles",
94
96
  "resolveSourcePath",
95
97
  "runFullAssessment",
98
+ "runSingleModule", // Issue #184
96
99
  ]);
97
100
  });
98
101
  it("should not have unexpected exports", () => {
@@ -105,6 +108,11 @@ describe("Assessment Runner Facade", () => {
105
108
  "createCallToolWrapper",
106
109
  "buildConfig",
107
110
  "runFullAssessment",
111
+ // Issue #184: Single module execution
112
+ "runSingleModule",
113
+ "getValidModuleNames",
114
+ // Issue #212: Native module detection
115
+ "detectNativeModules",
108
116
  ];
109
117
  // All exports should be in the expected list
110
118
  for (const exportName of allExports) {
@@ -9,7 +9,7 @@
9
9
  * emit identical JSONL structure for Phase 7 events (TEST-REQ-001).
10
10
  */
11
11
  import { jest, describe, it, expect, beforeEach, afterEach, } from "@jest/globals";
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";
12
+ import { emitJSONL, emitServerConnected, emitToolDiscovered, emitToolsDiscoveryComplete, emitAssessmentComplete, emitTestBatch, emitVulnerabilityFound, emitAnnotationMissing, emitAnnotationMisaligned, emitAnnotationReviewRecommended, emitAnnotationAligned, emitModulesConfigured, emitToolTestComplete, emitValidationSummary, emitPhaseStarted, emitPhaseComplete, emitNativeModuleWarning, extractToolParams, SCHEMA_VERSION, } from "../lib/jsonl-events.js";
13
13
  describe("JSONL Event Emission", () => {
14
14
  let consoleErrorSpy;
15
15
  let emittedEvents;
@@ -460,6 +460,88 @@ describe("JSONL Event Emission", () => {
460
460
  });
461
461
  });
462
462
  // ============================================================================
463
+ // GAP-001: emitNativeModuleWarning() Tests
464
+ // ============================================================================
465
+ describe("emitNativeModuleWarning", () => {
466
+ it("should emit native_module_warning with all required fields", () => {
467
+ emitNativeModuleWarning("canvas", "NATIVE_BINARY", "HIGH", "Native binary may trigger Gatekeeper on macOS", "dependencies", "^2.11.2", { CANVAS_BACKEND: "node" });
468
+ expect(emittedEvents[0]).toHaveProperty("event", "native_module_warning");
469
+ expect(emittedEvents[0]).toHaveProperty("moduleName", "canvas");
470
+ expect(emittedEvents[0]).toHaveProperty("category", "NATIVE_BINARY");
471
+ expect(emittedEvents[0]).toHaveProperty("severity", "HIGH");
472
+ expect(emittedEvents[0]).toHaveProperty("warningMessage", "Native binary may trigger Gatekeeper on macOS");
473
+ expect(emittedEvents[0]).toHaveProperty("dependencyType", "dependencies");
474
+ expect(emittedEvents[0]).toHaveProperty("moduleVersion", "^2.11.2");
475
+ expect(emittedEvents[0]).toHaveProperty("suggestedEnvVars");
476
+ expect(emittedEvents[0].suggestedEnvVars).toEqual({
477
+ CANVAS_BACKEND: "node",
478
+ });
479
+ });
480
+ it("should emit native_module_warning without suggestedEnvVars (optional parameter)", () => {
481
+ emitNativeModuleWarning("sharp", "NATIVE_BINARY", "MEDIUM", "Image processing module with native dependencies", "devDependencies", "^0.33.0");
482
+ expect(emittedEvents[0]).toHaveProperty("event", "native_module_warning");
483
+ expect(emittedEvents[0]).toHaveProperty("moduleName", "sharp");
484
+ expect(emittedEvents[0]).toHaveProperty("category", "NATIVE_BINARY");
485
+ expect(emittedEvents[0]).toHaveProperty("severity", "MEDIUM");
486
+ expect(emittedEvents[0]).toHaveProperty("warningMessage", "Image processing module with native dependencies");
487
+ expect(emittedEvents[0]).toHaveProperty("dependencyType", "devDependencies");
488
+ expect(emittedEvents[0]).toHaveProperty("moduleVersion", "^0.33.0");
489
+ expect(emittedEvents[0]).not.toHaveProperty("suggestedEnvVars");
490
+ });
491
+ it("should not include suggestedEnvVars when empty object provided", () => {
492
+ emitNativeModuleWarning("better-sqlite3", "NATIVE_BINARY", "HIGH", "SQLite with native bindings", "dependencies", "^9.0.0", {});
493
+ expect(emittedEvents[0]).toHaveProperty("event", "native_module_warning");
494
+ expect(emittedEvents[0]).toHaveProperty("moduleName", "better-sqlite3");
495
+ expect(emittedEvents[0]).not.toHaveProperty("suggestedEnvVars");
496
+ });
497
+ it("should handle HIGH severity", () => {
498
+ emitNativeModuleWarning("canvas", "NATIVE_BINARY", "HIGH", "Critical native module", "dependencies", "^2.0.0");
499
+ expect(emittedEvents[0]).toHaveProperty("severity", "HIGH");
500
+ });
501
+ it("should handle MEDIUM severity", () => {
502
+ emitNativeModuleWarning("node-gyp", "BUILD_TOOL", "MEDIUM", "Build toolchain required", "devDependencies", "^10.0.0");
503
+ expect(emittedEvents[0]).toHaveProperty("severity", "MEDIUM");
504
+ });
505
+ it("should include version and schemaVersion fields", () => {
506
+ emitNativeModuleWarning("test-module", "TEST_CATEGORY", "HIGH", "Test warning", "dependencies", "^1.0.0");
507
+ expect(emittedEvents[0]).toHaveProperty("version");
508
+ expect(emittedEvents[0]).toHaveProperty("schemaVersion", SCHEMA_VERSION);
509
+ });
510
+ it("should handle multiple suggestedEnvVars", () => {
511
+ emitNativeModuleWarning("puppeteer", "NATIVE_BINARY", "MEDIUM", "Chromium download may be blocked", "dependencies", "^21.0.0", {
512
+ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true",
513
+ PUPPETEER_EXECUTABLE_PATH: "/usr/bin/chromium",
514
+ });
515
+ expect(emittedEvents[0].suggestedEnvVars).toEqual({
516
+ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true",
517
+ PUPPETEER_EXECUTABLE_PATH: "/usr/bin/chromium",
518
+ });
519
+ });
520
+ it("should handle different dependencyType values", () => {
521
+ emitNativeModuleWarning("module1", "NATIVE_BINARY", "HIGH", "Warning 1", "dependencies", "^1.0.0");
522
+ emitNativeModuleWarning("module2", "NATIVE_BINARY", "HIGH", "Warning 2", "devDependencies", "^2.0.0");
523
+ emitNativeModuleWarning("module3", "NATIVE_BINARY", "MEDIUM", "Warning 3", "optionalDependencies", "^3.0.0");
524
+ expect(emittedEvents[0]).toHaveProperty("dependencyType", "dependencies");
525
+ expect(emittedEvents[1]).toHaveProperty("dependencyType", "devDependencies");
526
+ expect(emittedEvents[2]).toHaveProperty("dependencyType", "optionalDependencies");
527
+ });
528
+ it("should handle different category values", () => {
529
+ emitNativeModuleWarning("canvas", "NATIVE_BINARY", "HIGH", "Binary warning", "dependencies", "^1.0.0");
530
+ emitNativeModuleWarning("node-gyp", "BUILD_TOOL", "MEDIUM", "Build warning", "devDependencies", "^2.0.0");
531
+ emitNativeModuleWarning("custom", "CUSTOM_CATEGORY", "HIGH", "Custom warning", "dependencies", "^3.0.0");
532
+ expect(emittedEvents[0]).toHaveProperty("category", "NATIVE_BINARY");
533
+ expect(emittedEvents[1]).toHaveProperty("category", "BUILD_TOOL");
534
+ expect(emittedEvents[2]).toHaveProperty("category", "CUSTOM_CATEGORY");
535
+ });
536
+ it("should emit valid JSON", () => {
537
+ emitNativeModuleWarning("test-module", "TEST", "HIGH", "Test message", "dependencies", "^1.0.0", { VAR1: "value1" });
538
+ // If parsing failed, emittedEvents would be empty
539
+ expect(emittedEvents.length).toBe(1);
540
+ expect(emittedEvents[0]).toBeDefined();
541
+ expect(typeof emittedEvents[0]).toBe("object");
542
+ });
543
+ });
544
+ // ============================================================================
463
545
  // Phase 7 Events - Per-Tool Testing & Phase Lifecycle
464
546
  // ============================================================================
465
547
  describe("emitToolTestComplete", () => {