@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.
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Static Mode Integration Tests
3
+ *
4
+ * Tests for Issue #213: Static/manifest-only validation mode integration.
5
+ *
6
+ * Tests cover:
7
+ * - Source file loading error handling (malformed JSON, missing files)
8
+ * - CLI validation and error messages
9
+ * - Module filtering edge cases
10
+ */
11
+ import { describe, it, expect, jest, beforeEach, afterEach, } from "@jest/globals";
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import * as os from "os";
15
+ import { loadSourceFiles } from "../lib/assessment-runner/source-loader.js";
16
+ import { parseArgs } from "../lib/cli-parser.js";
17
+ import { buildConfig } from "../lib/assessment-runner/config-builder.js";
18
+ import { filterToStaticModules, filterToRuntimeModules, } from "../lib/static-modules.js";
19
+ describe("Static Mode Integration", () => {
20
+ let tempDir;
21
+ let consoleLogSpy;
22
+ let consoleWarnSpy;
23
+ let consoleErrorSpy;
24
+ let processExitSpy;
25
+ beforeEach(() => {
26
+ // Create temporary directory for test fixtures
27
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "inspector-test-"));
28
+ consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => { });
29
+ consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
30
+ consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
31
+ processExitSpy = jest
32
+ .spyOn(process, "exit")
33
+ .mockImplementation(() => undefined);
34
+ });
35
+ afterEach(() => {
36
+ // Clean up temporary directory
37
+ if (fs.existsSync(tempDir)) {
38
+ fs.rmSync(tempDir, { recursive: true, force: true });
39
+ }
40
+ jest.restoreAllMocks();
41
+ });
42
+ describe("Source File Loading Errors", () => {
43
+ it("should throw error for malformed package.json", () => {
44
+ // Create source directory with malformed package.json
45
+ fs.writeFileSync(path.join(tempDir, "package.json"), "{ invalid json }", "utf-8");
46
+ // Should throw parse error (not caught by loadSourceFiles)
47
+ expect(() => loadSourceFiles(tempDir, false)).toThrow();
48
+ });
49
+ it("should handle malformed manifest.json gracefully", () => {
50
+ // Create source directory with malformed manifest.json
51
+ fs.writeFileSync(path.join(tempDir, "manifest.json"), "{ invalid json }", "utf-8");
52
+ const sourceFiles = loadSourceFiles(tempDir, false);
53
+ // Should log warning about failed parse
54
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to parse manifest.json"));
55
+ // Should have manifestRaw but not manifestJson
56
+ expect(sourceFiles.manifestRaw).toBeDefined();
57
+ expect(sourceFiles.manifestJson).toBeUndefined();
58
+ });
59
+ it("should load valid source files successfully", () => {
60
+ // Create valid source files
61
+ fs.writeFileSync(path.join(tempDir, "package.json"), JSON.stringify({
62
+ name: "test-server",
63
+ version: "1.0.0",
64
+ dependencies: {},
65
+ }), "utf-8");
66
+ fs.writeFileSync(path.join(tempDir, "manifest.json"), JSON.stringify({
67
+ manifest_version: "0.3",
68
+ mcp_config: {
69
+ tools: [
70
+ {
71
+ name: "test_tool",
72
+ description: "Test tool",
73
+ inputSchema: { type: "object", properties: {} },
74
+ },
75
+ ],
76
+ },
77
+ }), "utf-8");
78
+ fs.writeFileSync(path.join(tempDir, "README.md"), "# Test Server\n\nThis is a test server.", "utf-8");
79
+ fs.writeFileSync(path.join(tempDir, "index.ts"), "// Test source file\nexport function test() {}", "utf-8");
80
+ const sourceFiles = loadSourceFiles(tempDir, false);
81
+ // Should load all files successfully
82
+ expect(sourceFiles.packageJson).toBeDefined();
83
+ expect(sourceFiles.manifestJson).toBeDefined();
84
+ expect(sourceFiles.readmeContent).toBeDefined();
85
+ // Should load .ts, .json files (index.ts, package.json, manifest.json)
86
+ expect(sourceFiles.sourceCodeFiles?.size).toBeGreaterThanOrEqual(1);
87
+ expect(sourceFiles.sourceCodeFiles?.has("index.ts")).toBe(true);
88
+ // Validate parsed content
89
+ expect(sourceFiles.packageJson.name).toBe("test-server");
90
+ expect(sourceFiles.manifestJson.mcp_config?.tools?.[0]?.name).toBe("test_tool");
91
+ });
92
+ it("should handle missing files gracefully", () => {
93
+ // Empty directory - no package.json, manifest.json, or README
94
+ const sourceFiles = loadSourceFiles(tempDir, false);
95
+ // Should complete without crashing
96
+ expect(sourceFiles).toBeDefined();
97
+ expect(sourceFiles.packageJson).toBeUndefined();
98
+ expect(sourceFiles.manifestJson).toBeUndefined();
99
+ expect(sourceFiles.readmeContent).toBeUndefined();
100
+ expect(sourceFiles.sourceCodeFiles?.size).toBe(0);
101
+ });
102
+ it("should skip files in .gitignore", () => {
103
+ // Create .gitignore
104
+ fs.writeFileSync(path.join(tempDir, ".gitignore"), "node_modules\n*.log\ntest.ts", "utf-8");
105
+ // Create files that should be ignored
106
+ fs.writeFileSync(path.join(tempDir, "test.ts"), "// Ignored", "utf-8");
107
+ fs.writeFileSync(path.join(tempDir, "debug.log"), "Debug log", "utf-8");
108
+ // Create files that should be loaded
109
+ fs.writeFileSync(path.join(tempDir, "index.ts"), "// Loaded", "utf-8");
110
+ const sourceFiles = loadSourceFiles(tempDir, false);
111
+ // Should only load index.ts
112
+ expect(sourceFiles.sourceCodeFiles?.size).toBe(1);
113
+ expect(sourceFiles.sourceCodeFiles?.has("index.ts")).toBe(true);
114
+ expect(sourceFiles.sourceCodeFiles?.has("test.ts")).toBe(false);
115
+ expect(sourceFiles.sourceCodeFiles?.has("debug.log")).toBe(false);
116
+ });
117
+ it("should handle large files by skipping them", () => {
118
+ // Create a file larger than MAX_SOURCE_FILE_SIZE (100,000 chars)
119
+ const largeContent = "a".repeat(150000);
120
+ fs.writeFileSync(path.join(tempDir, "large.ts"), largeContent, "utf-8");
121
+ // Create a normal file
122
+ fs.writeFileSync(path.join(tempDir, "normal.ts"), "// Normal file", "utf-8");
123
+ const sourceFiles = loadSourceFiles(tempDir, false);
124
+ // Should only load the normal file
125
+ expect(sourceFiles.sourceCodeFiles?.size).toBe(1);
126
+ expect(sourceFiles.sourceCodeFiles?.has("normal.ts")).toBe(true);
127
+ expect(sourceFiles.sourceCodeFiles?.has("large.ts")).toBe(false);
128
+ });
129
+ });
130
+ describe("Module Filtering Edge Cases", () => {
131
+ it("should result in empty array when filtering only runtime modules from static list", () => {
132
+ const runtimeModules = ["functionality", "security", "temporal"];
133
+ const result = filterToStaticModules(runtimeModules);
134
+ expect(result).toEqual([]);
135
+ });
136
+ it("should result in empty array when filtering only static modules from runtime list", () => {
137
+ const staticModules = [
138
+ "manifestValidation",
139
+ "prohibitedLibraries",
140
+ "portability",
141
+ ];
142
+ const result = filterToRuntimeModules(staticModules);
143
+ expect(result).toEqual([]);
144
+ });
145
+ it("should correctly split mixed module list", () => {
146
+ const mixedModules = [
147
+ "functionality",
148
+ "manifestValidation",
149
+ "security",
150
+ "prohibitedLibraries",
151
+ "temporal",
152
+ "portability",
153
+ ];
154
+ const staticResult = filterToStaticModules(mixedModules);
155
+ const runtimeResult = filterToRuntimeModules(mixedModules);
156
+ expect(staticResult).toEqual([
157
+ "manifestValidation",
158
+ "prohibitedLibraries",
159
+ "portability",
160
+ ]);
161
+ expect(runtimeResult).toEqual(["functionality", "security", "temporal"]);
162
+ // Should split perfectly with no overlap
163
+ expect(staticResult.length + runtimeResult.length).toBe(mixedModules.length);
164
+ });
165
+ it("should produce zero enabled modules with --only-modules containing only runtime modules", () => {
166
+ const config = buildConfig({
167
+ serverName: "test-server",
168
+ sourceCodePath: tempDir,
169
+ staticOnly: true,
170
+ onlyModules: ["functionality", "security", "temporal"],
171
+ });
172
+ const categories = config.assessmentCategories;
173
+ // Count enabled modules
174
+ const enabledCount = Object.values(categories).filter(Boolean).length;
175
+ expect(enabledCount).toBe(0);
176
+ // Verify runtime modules are disabled
177
+ expect(categories.functionality).toBe(false);
178
+ expect(categories.security).toBe(false);
179
+ expect(categories.temporal).toBe(false);
180
+ // Verify static modules are also disabled (not in onlyModules)
181
+ expect(categories.manifestValidation).toBe(false);
182
+ expect(categories.prohibitedLibraries).toBe(false);
183
+ });
184
+ it("should produce minimal enabled modules when most static modules are skipped", () => {
185
+ // Note: documentation and usability are deprecated aliases for developerExperience
186
+ // So skipping them doesn't disable those keys in the config object
187
+ // (the keys remain because STATIC_MODULES contains the deprecated names)
188
+ const config = buildConfig({
189
+ serverName: "test-server",
190
+ sourceCodePath: tempDir,
191
+ staticOnly: true,
192
+ skipModules: [
193
+ "manifestValidation",
194
+ "prohibitedLibraries",
195
+ "portability",
196
+ "externalAPIScanner",
197
+ "fileModularization",
198
+ "conformance",
199
+ "toolAnnotations",
200
+ "authentication",
201
+ "aupCompliance",
202
+ ],
203
+ });
204
+ const categories = config.assessmentCategories;
205
+ // Most static modules should be disabled
206
+ expect(categories.manifestValidation).toBe(false);
207
+ expect(categories.prohibitedLibraries).toBe(false);
208
+ expect(categories.portability).toBe(false);
209
+ expect(categories.toolAnnotations).toBe(false);
210
+ expect(categories.externalAPIScanner).toBe(false);
211
+ expect(categories.fileModularization).toBe(false);
212
+ expect(categories.conformance).toBe(false);
213
+ expect(categories.authentication).toBe(false);
214
+ expect(categories.aupCompliance).toBe(false);
215
+ // Only documentation and usability (deprecated aliases) may remain enabled
216
+ // This is acceptable behavior - they're in STATIC_MODULES for backward compatibility
217
+ const enabledCount = Object.values(categories).filter(Boolean).length;
218
+ // Should have at most 2 enabled (documentation, usability - both resolve to developerExperience)
219
+ expect(enabledCount).toBeLessThanOrEqual(2);
220
+ });
221
+ it("should handle --only-modules with mix of static and runtime modules in static mode", () => {
222
+ const config = buildConfig({
223
+ serverName: "test-server",
224
+ sourceCodePath: tempDir,
225
+ staticOnly: true,
226
+ onlyModules: [
227
+ "manifestValidation",
228
+ "security", // runtime - should be ignored
229
+ "prohibitedLibraries",
230
+ "functionality", // runtime - should be ignored
231
+ ],
232
+ });
233
+ const categories = config.assessmentCategories;
234
+ // Only static modules from onlyModules should be enabled
235
+ expect(categories.manifestValidation).toBe(true);
236
+ expect(categories.prohibitedLibraries).toBe(true);
237
+ // Runtime modules should be disabled even if in onlyModules
238
+ expect(categories.security).toBe(false);
239
+ expect(categories.functionality).toBe(false);
240
+ // Other static modules not in onlyModules should be disabled
241
+ expect(categories.portability).toBe(false);
242
+ expect(categories.toolAnnotations).toBe(false);
243
+ });
244
+ });
245
+ describe("CLI Validation Messages", () => {
246
+ it("should show helpful error for --static-only without --source", () => {
247
+ const result = parseArgs(["--server", "test-server", "--static-only"]);
248
+ expect(result.helpRequested).toBe(true);
249
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--static-only requires --source"));
250
+ });
251
+ it("should show helpful error for --static-only with --http", () => {
252
+ const result = parseArgs([
253
+ "--server",
254
+ "test-server",
255
+ "--source",
256
+ tempDir,
257
+ "--static-only",
258
+ "--http",
259
+ "http://localhost:8080",
260
+ ]);
261
+ expect(result.helpRequested).toBe(true);
262
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("cannot be used with --http"));
263
+ });
264
+ it("should show helpful error for --static-only with --fallback-static", () => {
265
+ const result = parseArgs([
266
+ "--server",
267
+ "test-server",
268
+ "--source",
269
+ tempDir,
270
+ "--static-only",
271
+ "--fallback-static",
272
+ ]);
273
+ expect(result.helpRequested).toBe(true);
274
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("mutually exclusive"));
275
+ });
276
+ it("should allow valid --static-only with --source", () => {
277
+ const result = parseArgs([
278
+ "--server",
279
+ "test-server",
280
+ "--source",
281
+ tempDir,
282
+ "--static-only",
283
+ ]);
284
+ expect(result.staticOnly).toBe(true);
285
+ expect(result.sourceCodePath).toBe(tempDir);
286
+ expect(result.helpRequested).toBeUndefined();
287
+ });
288
+ it("should allow valid --fallback-static with --source and --http", () => {
289
+ const result = parseArgs([
290
+ "--server",
291
+ "test-server",
292
+ "--source",
293
+ tempDir,
294
+ "--http",
295
+ "http://localhost:8080",
296
+ "--fallback-static",
297
+ ]);
298
+ expect(result.fallbackStatic).toBe(true);
299
+ expect(result.httpUrl).toBe("http://localhost:8080");
300
+ expect(result.sourceCodePath).toBe(tempDir);
301
+ expect(result.helpRequested).toBeUndefined();
302
+ });
303
+ });
304
+ describe("Config Builder Integration", () => {
305
+ it("should enable static modules and disable runtime modules in static-only mode", () => {
306
+ const config = buildConfig({
307
+ serverName: "test-server",
308
+ sourceCodePath: tempDir,
309
+ staticOnly: true,
310
+ });
311
+ const categories = config.assessmentCategories;
312
+ // Count enabled modules
313
+ const enabledModules = Object.entries(categories)
314
+ .filter(([_, enabled]) => enabled)
315
+ .map(([name]) => name);
316
+ const disabledModules = Object.entries(categories)
317
+ .filter(([_, enabled]) => !enabled)
318
+ .map(([name]) => name);
319
+ // All enabled modules should be static-capable
320
+ for (const moduleName of enabledModules) {
321
+ const isStatic = moduleName === "manifestValidation" ||
322
+ moduleName === "documentation" ||
323
+ moduleName === "usability" ||
324
+ moduleName === "prohibitedLibraries" ||
325
+ moduleName === "portability" ||
326
+ moduleName === "externalAPIScanner" ||
327
+ moduleName === "fileModularization" ||
328
+ moduleName === "conformance" ||
329
+ moduleName === "toolAnnotations" ||
330
+ moduleName === "authentication" ||
331
+ moduleName === "aupCompliance";
332
+ expect(isStatic).toBe(true);
333
+ }
334
+ // All disabled modules should include runtime-only modules
335
+ expect(disabledModules).toContain("functionality");
336
+ expect(disabledModules).toContain("security");
337
+ expect(disabledModules).toContain("temporal");
338
+ expect(disabledModules).toContain("protocolCompliance");
339
+ });
340
+ it("should log static mode info message", () => {
341
+ buildConfig({
342
+ serverName: "test-server",
343
+ sourceCodePath: tempDir,
344
+ staticOnly: true,
345
+ });
346
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Static-only mode"));
347
+ });
348
+ it("should respect --only-modules filter in static mode", () => {
349
+ const config = buildConfig({
350
+ serverName: "test-server",
351
+ sourceCodePath: tempDir,
352
+ staticOnly: true,
353
+ onlyModules: ["manifestValidation", "prohibitedLibraries"],
354
+ });
355
+ const categories = config.assessmentCategories;
356
+ // Count enabled modules
357
+ const enabledCount = Object.values(categories).filter(Boolean).length;
358
+ // Should only have 2 enabled modules
359
+ expect(enabledCount).toBe(2);
360
+ expect(categories.manifestValidation).toBe(true);
361
+ expect(categories.prohibitedLibraries).toBe(true);
362
+ // All other modules should be disabled
363
+ expect(categories.portability).toBe(false);
364
+ expect(categories.functionality).toBe(false);
365
+ expect(categories.security).toBe(false);
366
+ });
367
+ it("should respect --skip-modules filter in static mode", () => {
368
+ const config = buildConfig({
369
+ serverName: "test-server",
370
+ sourceCodePath: tempDir,
371
+ staticOnly: true,
372
+ skipModules: ["portability", "externalAPIScanner"],
373
+ });
374
+ const categories = config.assessmentCategories;
375
+ // Skipped modules should be disabled
376
+ expect(categories.portability).toBe(false);
377
+ expect(categories.externalAPIScanner).toBe(false);
378
+ // Other static modules should be enabled
379
+ expect(categories.manifestValidation).toBe(true);
380
+ expect(categories.prohibitedLibraries).toBe(true);
381
+ expect(categories.toolAnnotations).toBe(true);
382
+ // Runtime modules should be disabled
383
+ expect(categories.functionality).toBe(false);
384
+ expect(categories.security).toBe(false);
385
+ });
386
+ });
387
+ });