@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.
- package/build/__tests__/assessment-runner/index.test.js +11 -3
- package/build/__tests__/assessment-runner/native-module-detector.test.js +199 -0
- package/build/__tests__/assessment-runner/server-connection.test.js +184 -1
- package/build/__tests__/assessment-runner-facade.test.js +10 -2
- package/build/__tests__/jsonl-events.test.js +83 -1
- package/build/__tests__/static-mode-integration.test.js +387 -0
- package/build/__tests__/static-only-mode.test.js +439 -0
- package/build/__tests__/transport.test.js +141 -0
- package/build/assess-full.js +532 -106
- package/build/assess-security.js +54 -90
- package/build/lib/assessment-runner/assessment-executor.js +194 -0
- package/build/lib/assessment-runner/config-builder.js +46 -1
- package/build/lib/assessment-runner/index.js +2 -0
- package/build/lib/assessment-runner/native-module-detector.js +107 -0
- package/build/lib/assessment-runner/server-connection.js +56 -9
- package/build/lib/cli-parser.js +67 -0
- package/build/lib/cli-parserSchemas.js +12 -0
- package/build/lib/jsonl-events.js +29 -0
- package/build/lib/static-modules.js +103 -0
- package/build/transport.js +32 -7
- package/build/validate-testbed.js +0 -0
- package/package.json +1 -1
- package/build/lib/__tests__/zodErrorFormatter.test.js +0 -282
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
85
|
+
it("should export exactly 10 functions", () => {
|
|
86
86
|
const exportedFunctions = Object.entries(AssessmentRunner).filter(([, value]) => typeof value === "function");
|
|
87
|
-
expect(exportedFunctions.length).toBe(
|
|
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", () => {
|