@bryan-thompson/inspector-assessment-cli 1.34.2 → 1.35.1

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,163 @@
1
+ /**
2
+ * Stage 3 Fix Validation Tests
3
+ *
4
+ * Tests for Issue #137 Stage 3 fixes (code review and corrections).
5
+ * Validates that FIX-001 (stageBVerbose schema) is correctly implemented.
6
+ *
7
+ * @see https://github.com/triepod-ai/inspector-assessment/issues/137
8
+ */
9
+ import { describe, it, expect } from "@jest/globals";
10
+ import { AssessmentOptionsSchema, safeParseAssessmentOptions, validateAssessmentOptions, } from "../lib/cli-parserSchemas.js";
11
+ describe("Stage 3 Fix Validation Tests", () => {
12
+ describe("[TEST-001] cli-parserSchemas.ts - stageBVerbose field (FIX-001)", () => {
13
+ describe("stageBVerbose field validation", () => {
14
+ it("should accept stageBVerbose with true value (happy path)", () => {
15
+ const options = {
16
+ serverName: "test-server",
17
+ stageBVerbose: true,
18
+ };
19
+ const result = safeParseAssessmentOptions(options);
20
+ expect(result.success).toBe(true);
21
+ if (result.success) {
22
+ expect(result.data.stageBVerbose).toBe(true);
23
+ }
24
+ });
25
+ it("should accept stageBVerbose with false value (edge case)", () => {
26
+ const options = {
27
+ serverName: "test-server",
28
+ stageBVerbose: false,
29
+ };
30
+ const result = safeParseAssessmentOptions(options);
31
+ expect(result.success).toBe(true);
32
+ if (result.success) {
33
+ expect(result.data.stageBVerbose).toBe(false);
34
+ }
35
+ });
36
+ it("should accept undefined stageBVerbose - optional field (edge case)", () => {
37
+ const options = {
38
+ serverName: "test-server",
39
+ // stageBVerbose omitted
40
+ };
41
+ const result = safeParseAssessmentOptions(options);
42
+ expect(result.success).toBe(true);
43
+ if (result.success) {
44
+ expect(result.data.stageBVerbose).toBeUndefined();
45
+ }
46
+ });
47
+ it("should validate with validateAssessmentOptions() accepting stageBVerbose (error case prevention)", () => {
48
+ const options = {
49
+ serverName: "test-server",
50
+ stageBVerbose: true,
51
+ };
52
+ const errors = validateAssessmentOptions(options);
53
+ expect(errors).toHaveLength(0);
54
+ });
55
+ it("should reject non-boolean stageBVerbose values (error case)", () => {
56
+ const testCases = [
57
+ { stageBVerbose: "true" }, // string instead of boolean
58
+ { stageBVerbose: 1 }, // number instead of boolean
59
+ { stageBVerbose: null },
60
+ { stageBVerbose: {} },
61
+ { stageBVerbose: [] },
62
+ ];
63
+ testCases.forEach((testCase) => {
64
+ const options = {
65
+ serverName: "test-server",
66
+ ...testCase,
67
+ };
68
+ const result = safeParseAssessmentOptions(options);
69
+ expect(result.success).toBe(false);
70
+ });
71
+ });
72
+ });
73
+ describe("stageBVerbose integration with other fields", () => {
74
+ it("should accept stageBVerbose with outputFormat=tiered", () => {
75
+ const options = {
76
+ serverName: "test-server",
77
+ outputFormat: "tiered",
78
+ stageBVerbose: true,
79
+ };
80
+ const result = safeParseAssessmentOptions(options);
81
+ expect(result.success).toBe(true);
82
+ if (result.success) {
83
+ expect(result.data.outputFormat).toBe("tiered");
84
+ expect(result.data.stageBVerbose).toBe(true);
85
+ }
86
+ });
87
+ it("should accept stageBVerbose with other CLI options", () => {
88
+ const options = {
89
+ serverName: "test-server",
90
+ verbose: true,
91
+ jsonOnly: false,
92
+ fullAssessment: true,
93
+ stageBVerbose: true,
94
+ };
95
+ const result = safeParseAssessmentOptions(options);
96
+ expect(result.success).toBe(true);
97
+ if (result.success) {
98
+ expect(result.data.verbose).toBe(true);
99
+ expect(result.data.jsonOnly).toBe(false);
100
+ expect(result.data.fullAssessment).toBe(true);
101
+ expect(result.data.stageBVerbose).toBe(true);
102
+ }
103
+ });
104
+ it("should work with validateAssessmentOptions for complex options", () => {
105
+ const options = {
106
+ serverName: "test-server",
107
+ outputFormat: "tiered",
108
+ autoTier: true,
109
+ stageBVerbose: true,
110
+ verbose: true,
111
+ };
112
+ const errors = validateAssessmentOptions(options);
113
+ expect(errors).toHaveLength(0);
114
+ });
115
+ });
116
+ describe("regression prevention for ISSUE-001", () => {
117
+ it("should maintain stageBVerbose in schema after validation", () => {
118
+ // Verify the field exists in the schema shape
119
+ const options = {
120
+ serverName: "test-server",
121
+ stageBVerbose: true,
122
+ };
123
+ const result = AssessmentOptionsSchema.safeParse(options);
124
+ expect(result.success).toBe(true);
125
+ if (result.success) {
126
+ expect(result.data).toHaveProperty("stageBVerbose");
127
+ expect(result.data.stageBVerbose).toBe(true);
128
+ }
129
+ });
130
+ it("should not strip stageBVerbose field during validation", () => {
131
+ const options = {
132
+ serverName: "test-server",
133
+ stageBVerbose: true,
134
+ outputFormat: "tiered",
135
+ };
136
+ const parsed = AssessmentOptionsSchema.parse(options);
137
+ // Field should be present after parsing
138
+ expect(parsed.stageBVerbose).toBe(true);
139
+ expect(parsed.outputFormat).toBe("tiered");
140
+ });
141
+ it("should backward compatibility - existing fields unaffected", () => {
142
+ const options = {
143
+ serverName: "test-server",
144
+ verbose: true,
145
+ jsonOnly: false,
146
+ format: "json",
147
+ fullAssessment: true,
148
+ // New field
149
+ stageBVerbose: true,
150
+ };
151
+ const result = safeParseAssessmentOptions(options);
152
+ expect(result.success).toBe(true);
153
+ if (result.success) {
154
+ expect(result.data.verbose).toBe(true);
155
+ expect(result.data.jsonOnly).toBe(false);
156
+ expect(result.data.format).toBe("json");
157
+ expect(result.data.fullAssessment).toBe(true);
158
+ expect(result.data.stageBVerbose).toBe(true);
159
+ }
160
+ });
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Stage 3 Fixes Regression Tests
3
+ *
4
+ * Tests for Issue #134 code review fixes to ensure they don't regress.
5
+ * These tests validate schema validation and JSONL event emission.
6
+ *
7
+ * @see https://github.com/triepod-ai/inspector-assessment/issues/134
8
+ */
9
+ import { describe, it, expect, jest, beforeEach, afterEach, } from "@jest/globals";
10
+ import { OutputFormatSchema, safeParseAssessmentOptions, } from "../lib/cli-parserSchemas.js";
11
+ import { emitTieredOutput, SCHEMA_VERSION, } from "../lib/jsonl-events.js";
12
+ import { INSPECTOR_VERSION } from "../../../client/lib/lib/moduleScoring.js";
13
+ describe("Stage 3 Fixes Regression Tests", () => {
14
+ describe("[TEST-REQ-001] cli-parserSchemas.ts - AssessmentOptionsSchema", () => {
15
+ describe("outputFormat field validation", () => {
16
+ it("should accept outputFormat with 'full' value", () => {
17
+ const options = {
18
+ serverName: "test-server",
19
+ outputFormat: "full",
20
+ };
21
+ const result = safeParseAssessmentOptions(options);
22
+ expect(result.success).toBe(true);
23
+ if (result.success) {
24
+ expect(result.data.outputFormat).toBe("full");
25
+ }
26
+ });
27
+ it("should accept outputFormat with 'tiered' value", () => {
28
+ const options = {
29
+ serverName: "test-server",
30
+ outputFormat: "tiered",
31
+ };
32
+ const result = safeParseAssessmentOptions(options);
33
+ expect(result.success).toBe(true);
34
+ if (result.success) {
35
+ expect(result.data.outputFormat).toBe("tiered");
36
+ }
37
+ });
38
+ it("should accept outputFormat with 'summary-only' value", () => {
39
+ const options = {
40
+ serverName: "test-server",
41
+ outputFormat: "summary-only",
42
+ };
43
+ const result = safeParseAssessmentOptions(options);
44
+ expect(result.success).toBe(true);
45
+ if (result.success) {
46
+ expect(result.data.outputFormat).toBe("summary-only");
47
+ }
48
+ });
49
+ it("should accept undefined outputFormat (optional field)", () => {
50
+ const options = {
51
+ serverName: "test-server",
52
+ // outputFormat omitted
53
+ };
54
+ const result = safeParseAssessmentOptions(options);
55
+ expect(result.success).toBe(true);
56
+ if (result.success) {
57
+ expect(result.data.outputFormat).toBeUndefined();
58
+ }
59
+ });
60
+ it("should reject invalid outputFormat value", () => {
61
+ const options = {
62
+ serverName: "test-server",
63
+ outputFormat: "invalid",
64
+ };
65
+ const result = safeParseAssessmentOptions(options);
66
+ expect(result.success).toBe(false);
67
+ if (!result.success) {
68
+ // Zod error messages include the full path and expected values
69
+ const errors = result.error.errors;
70
+ const hasOutputFormatError = errors.some((e) => e.path.includes("outputFormat") ||
71
+ e.message.includes("full") ||
72
+ e.message.includes("tiered") ||
73
+ e.message.includes("summary-only"));
74
+ expect(hasOutputFormatError).toBe(true);
75
+ }
76
+ });
77
+ it("should reject non-string outputFormat values", () => {
78
+ const testCases = [
79
+ { outputFormat: 123 },
80
+ { outputFormat: true },
81
+ { outputFormat: null },
82
+ { outputFormat: {} },
83
+ { outputFormat: [] },
84
+ ];
85
+ testCases.forEach((testCase) => {
86
+ const options = {
87
+ serverName: "test-server",
88
+ ...testCase,
89
+ };
90
+ const result = safeParseAssessmentOptions(options);
91
+ expect(result.success).toBe(false);
92
+ });
93
+ });
94
+ });
95
+ describe("autoTier field validation", () => {
96
+ it("should accept autoTier with true value", () => {
97
+ const options = {
98
+ serverName: "test-server",
99
+ autoTier: true,
100
+ };
101
+ const result = safeParseAssessmentOptions(options);
102
+ expect(result.success).toBe(true);
103
+ if (result.success) {
104
+ expect(result.data.autoTier).toBe(true);
105
+ }
106
+ });
107
+ it("should accept autoTier with false value", () => {
108
+ const options = {
109
+ serverName: "test-server",
110
+ autoTier: false,
111
+ };
112
+ const result = safeParseAssessmentOptions(options);
113
+ expect(result.success).toBe(true);
114
+ if (result.success) {
115
+ expect(result.data.autoTier).toBe(false);
116
+ }
117
+ });
118
+ it("should accept undefined autoTier (optional field)", () => {
119
+ const options = {
120
+ serverName: "test-server",
121
+ // autoTier omitted
122
+ };
123
+ const result = safeParseAssessmentOptions(options);
124
+ expect(result.success).toBe(true);
125
+ if (result.success) {
126
+ expect(result.data.autoTier).toBeUndefined();
127
+ }
128
+ });
129
+ it("should reject non-boolean autoTier values", () => {
130
+ const testCases = [
131
+ { autoTier: "true" }, // string instead of boolean
132
+ { autoTier: 1 }, // number instead of boolean
133
+ { autoTier: null },
134
+ { autoTier: {} },
135
+ { autoTier: [] },
136
+ ];
137
+ testCases.forEach((testCase) => {
138
+ const options = {
139
+ serverName: "test-server",
140
+ ...testCase,
141
+ };
142
+ const result = safeParseAssessmentOptions(options);
143
+ expect(result.success).toBe(false);
144
+ });
145
+ });
146
+ });
147
+ describe("combined outputFormat and autoTier validation", () => {
148
+ it("should accept both outputFormat and autoTier together", () => {
149
+ const options = {
150
+ serverName: "test-server",
151
+ outputFormat: "tiered",
152
+ autoTier: true,
153
+ };
154
+ const result = safeParseAssessmentOptions(options);
155
+ expect(result.success).toBe(true);
156
+ if (result.success) {
157
+ expect(result.data.outputFormat).toBe("tiered");
158
+ expect(result.data.autoTier).toBe(true);
159
+ }
160
+ });
161
+ it("should accept outputFormat without autoTier", () => {
162
+ const options = {
163
+ serverName: "test-server",
164
+ outputFormat: "summary-only",
165
+ };
166
+ const result = safeParseAssessmentOptions(options);
167
+ expect(result.success).toBe(true);
168
+ if (result.success) {
169
+ expect(result.data.outputFormat).toBe("summary-only");
170
+ expect(result.data.autoTier).toBeUndefined();
171
+ }
172
+ });
173
+ it("should accept autoTier without outputFormat", () => {
174
+ const options = {
175
+ serverName: "test-server",
176
+ autoTier: true,
177
+ };
178
+ const result = safeParseAssessmentOptions(options);
179
+ expect(result.success).toBe(true);
180
+ if (result.success) {
181
+ expect(result.data.autoTier).toBe(true);
182
+ expect(result.data.outputFormat).toBeUndefined();
183
+ }
184
+ });
185
+ });
186
+ describe("regression prevention", () => {
187
+ it("should fail if outputFormat schema loses required enum values", () => {
188
+ // Verify all three enum values are present
189
+ const validValues = OutputFormatSchema.options;
190
+ expect(validValues).toContain("full");
191
+ expect(validValues).toContain("tiered");
192
+ expect(validValues).toContain("summary-only");
193
+ expect(validValues.length).toBe(3);
194
+ });
195
+ it("should maintain backward compatibility with existing fields", () => {
196
+ // Test that adding new fields didn't break existing validation
197
+ const options = {
198
+ serverName: "test-server",
199
+ verbose: true,
200
+ jsonOnly: true,
201
+ format: "json",
202
+ // Include new fields
203
+ outputFormat: "tiered",
204
+ autoTier: true,
205
+ };
206
+ const result = safeParseAssessmentOptions(options);
207
+ expect(result.success).toBe(true);
208
+ if (result.success) {
209
+ expect(result.data.verbose).toBe(true);
210
+ expect(result.data.jsonOnly).toBe(true);
211
+ expect(result.data.format).toBe("json");
212
+ expect(result.data.outputFormat).toBe("tiered");
213
+ expect(result.data.autoTier).toBe(true);
214
+ }
215
+ });
216
+ });
217
+ });
218
+ describe("[TEST-REQ-002] jsonl-events.ts - TieredOutputEvent and emitTieredOutput", () => {
219
+ // Capture console.error output for testing
220
+ let consoleSpy;
221
+ let errorOutput = [];
222
+ beforeEach(() => {
223
+ errorOutput = [];
224
+ // Spy on console.error to capture JSONL output
225
+ consoleSpy = jest
226
+ .spyOn(console, "error")
227
+ .mockImplementation((msg) => {
228
+ errorOutput.push(msg);
229
+ });
230
+ });
231
+ afterEach(() => {
232
+ // Restore original console.error
233
+ if (consoleSpy) {
234
+ consoleSpy.mockRestore();
235
+ }
236
+ });
237
+ describe("emitTieredOutput function", () => {
238
+ it("should emit valid JSONL with correct event type", () => {
239
+ const tiers = {
240
+ executiveSummary: {
241
+ path: "/tmp/output/executive-summary.json",
242
+ estimatedTokens: 500,
243
+ },
244
+ toolSummaries: {
245
+ path: "/tmp/output/tool-summaries.json",
246
+ estimatedTokens: 1500,
247
+ toolCount: 10,
248
+ },
249
+ };
250
+ emitTieredOutput("/tmp/output", "summary-only", tiers);
251
+ expect(errorOutput.length).toBe(1);
252
+ const parsed = JSON.parse(errorOutput[0]);
253
+ expect(parsed.event).toBe("tiered_output_generated");
254
+ expect(parsed.outputDir).toBe("/tmp/output");
255
+ expect(parsed.outputFormat).toBe("summary-only");
256
+ expect(parsed.tiers).toEqual(tiers);
257
+ });
258
+ it("should include version and schemaVersion fields", () => {
259
+ const tiers = {
260
+ executiveSummary: {
261
+ path: "/tmp/output/executive-summary.json",
262
+ estimatedTokens: 500,
263
+ },
264
+ toolSummaries: {
265
+ path: "/tmp/output/tool-summaries.json",
266
+ estimatedTokens: 1500,
267
+ toolCount: 10,
268
+ },
269
+ };
270
+ emitTieredOutput("/tmp/output", "tiered", tiers);
271
+ expect(errorOutput.length).toBe(1);
272
+ const parsed = JSON.parse(errorOutput[0]);
273
+ expect(parsed.version).toBe(INSPECTOR_VERSION);
274
+ expect(parsed.schemaVersion).toBe(SCHEMA_VERSION);
275
+ });
276
+ it("should include all required tier properties", () => {
277
+ const tiers = {
278
+ executiveSummary: {
279
+ path: "/tmp/output/executive-summary.json",
280
+ estimatedTokens: 500,
281
+ },
282
+ toolSummaries: {
283
+ path: "/tmp/output/tool-summaries.json",
284
+ estimatedTokens: 1500,
285
+ toolCount: 10,
286
+ },
287
+ toolDetails: {
288
+ directory: "/tmp/output/tools",
289
+ fileCount: 10,
290
+ totalEstimatedTokens: 5000,
291
+ },
292
+ };
293
+ emitTieredOutput("/tmp/output", "tiered", tiers);
294
+ expect(errorOutput.length).toBe(1);
295
+ const parsed = JSON.parse(errorOutput[0]);
296
+ expect(parsed.tiers.executiveSummary).toEqual({
297
+ path: "/tmp/output/executive-summary.json",
298
+ estimatedTokens: 500,
299
+ });
300
+ expect(parsed.tiers.toolSummaries).toEqual({
301
+ path: "/tmp/output/tool-summaries.json",
302
+ estimatedTokens: 1500,
303
+ toolCount: 10,
304
+ });
305
+ expect(parsed.tiers.toolDetails).toEqual({
306
+ directory: "/tmp/output/tools",
307
+ fileCount: 10,
308
+ totalEstimatedTokens: 5000,
309
+ });
310
+ });
311
+ it("should handle missing optional toolDetails tier", () => {
312
+ const tiers = {
313
+ executiveSummary: {
314
+ path: "/tmp/output/executive-summary.json",
315
+ estimatedTokens: 500,
316
+ },
317
+ toolSummaries: {
318
+ path: "/tmp/output/tool-summaries.json",
319
+ estimatedTokens: 1500,
320
+ toolCount: 10,
321
+ },
322
+ // toolDetails omitted (optional)
323
+ };
324
+ emitTieredOutput("/tmp/output", "summary-only", tiers);
325
+ expect(errorOutput.length).toBe(1);
326
+ const parsed = JSON.parse(errorOutput[0]);
327
+ expect(parsed.tiers.executiveSummary).toBeDefined();
328
+ expect(parsed.tiers.toolSummaries).toBeDefined();
329
+ expect(parsed.tiers.toolDetails).toBeUndefined();
330
+ });
331
+ it("should emit valid JSON that can be parsed", () => {
332
+ const tiers = {
333
+ executiveSummary: {
334
+ path: "/tmp/output/executive-summary.json",
335
+ estimatedTokens: 500,
336
+ },
337
+ toolSummaries: {
338
+ path: "/tmp/output/tool-summaries.json",
339
+ estimatedTokens: 1500,
340
+ toolCount: 10,
341
+ },
342
+ };
343
+ emitTieredOutput("/tmp/output", "tiered", tiers);
344
+ expect(errorOutput.length).toBe(1);
345
+ // Should not throw when parsing
346
+ expect(() => JSON.parse(errorOutput[0])).not.toThrow();
347
+ });
348
+ });
349
+ describe("TieredOutputEvent type structure", () => {
350
+ it("should enforce correct event type", () => {
351
+ const event = {
352
+ event: "tiered_output_generated",
353
+ outputDir: "/tmp/output",
354
+ outputFormat: "tiered",
355
+ tiers: {
356
+ executiveSummary: {
357
+ path: "/tmp/output/executive-summary.json",
358
+ estimatedTokens: 500,
359
+ },
360
+ toolSummaries: {
361
+ path: "/tmp/output/tool-summaries.json",
362
+ estimatedTokens: 1500,
363
+ toolCount: 10,
364
+ },
365
+ },
366
+ };
367
+ expect(event.event).toBe("tiered_output_generated");
368
+ });
369
+ it("should support both 'tiered' and 'summary-only' output formats", () => {
370
+ const tieredEvent = {
371
+ event: "tiered_output_generated",
372
+ outputDir: "/tmp/output",
373
+ outputFormat: "tiered",
374
+ tiers: {
375
+ executiveSummary: { path: "/tmp/exec.json", estimatedTokens: 500 },
376
+ toolSummaries: {
377
+ path: "/tmp/tools.json",
378
+ estimatedTokens: 1500,
379
+ toolCount: 10,
380
+ },
381
+ },
382
+ };
383
+ const summaryOnlyEvent = {
384
+ event: "tiered_output_generated",
385
+ outputDir: "/tmp/output",
386
+ outputFormat: "summary-only",
387
+ tiers: {
388
+ executiveSummary: { path: "/tmp/exec.json", estimatedTokens: 500 },
389
+ toolSummaries: {
390
+ path: "/tmp/tools.json",
391
+ estimatedTokens: 1500,
392
+ toolCount: 10,
393
+ },
394
+ },
395
+ };
396
+ expect(tieredEvent.outputFormat).toBe("tiered");
397
+ expect(summaryOnlyEvent.outputFormat).toBe("summary-only");
398
+ });
399
+ it("should have correct structure for all tier properties", () => {
400
+ const event = {
401
+ event: "tiered_output_generated",
402
+ outputDir: "/tmp/output",
403
+ outputFormat: "tiered",
404
+ tiers: {
405
+ executiveSummary: {
406
+ path: "/tmp/output/executive-summary.json",
407
+ estimatedTokens: 500,
408
+ },
409
+ toolSummaries: {
410
+ path: "/tmp/output/tool-summaries.json",
411
+ estimatedTokens: 1500,
412
+ toolCount: 10,
413
+ },
414
+ toolDetails: {
415
+ directory: "/tmp/output/tools",
416
+ fileCount: 10,
417
+ totalEstimatedTokens: 5000,
418
+ },
419
+ },
420
+ };
421
+ // Verify executive summary structure
422
+ expect(event.tiers.executiveSummary).toHaveProperty("path");
423
+ expect(event.tiers.executiveSummary).toHaveProperty("estimatedTokens");
424
+ expect(typeof event.tiers.executiveSummary.path).toBe("string");
425
+ expect(typeof event.tiers.executiveSummary.estimatedTokens).toBe("number");
426
+ // Verify tool summaries structure
427
+ expect(event.tiers.toolSummaries).toHaveProperty("path");
428
+ expect(event.tiers.toolSummaries).toHaveProperty("estimatedTokens");
429
+ expect(event.tiers.toolSummaries).toHaveProperty("toolCount");
430
+ expect(typeof event.tiers.toolSummaries.path).toBe("string");
431
+ expect(typeof event.tiers.toolSummaries.estimatedTokens).toBe("number");
432
+ expect(typeof event.tiers.toolSummaries.toolCount).toBe("number");
433
+ // Verify tool details structure (optional)
434
+ expect(event.tiers.toolDetails).toHaveProperty("directory");
435
+ expect(event.tiers.toolDetails).toHaveProperty("fileCount");
436
+ expect(event.tiers.toolDetails).toHaveProperty("totalEstimatedTokens");
437
+ expect(typeof event.tiers.toolDetails?.directory).toBe("string");
438
+ expect(typeof event.tiers.toolDetails?.fileCount).toBe("number");
439
+ expect(typeof event.tiers.toolDetails?.totalEstimatedTokens).toBe("number");
440
+ });
441
+ });
442
+ describe("JSONL format compliance", () => {
443
+ it("should emit single-line JSON (no newlines within object)", () => {
444
+ const tiers = {
445
+ executiveSummary: {
446
+ path: "/tmp/output/executive-summary.json",
447
+ estimatedTokens: 500,
448
+ },
449
+ toolSummaries: {
450
+ path: "/tmp/output/tool-summaries.json",
451
+ estimatedTokens: 1500,
452
+ toolCount: 10,
453
+ },
454
+ };
455
+ emitTieredOutput("/tmp/output", "tiered", tiers);
456
+ expect(errorOutput.length).toBe(1);
457
+ const output = errorOutput[0];
458
+ // Should be single line (may have trailing newline from console.error)
459
+ const lines = output.trim().split("\n");
460
+ expect(lines.length).toBe(1);
461
+ });
462
+ it("should emit parseable JSON Lines format", () => {
463
+ const tiers1 = {
464
+ executiveSummary: { path: "/tmp/1/exec.json", estimatedTokens: 500 },
465
+ toolSummaries: {
466
+ path: "/tmp/1/tools.json",
467
+ estimatedTokens: 1500,
468
+ toolCount: 10,
469
+ },
470
+ };
471
+ const tiers2 = {
472
+ executiveSummary: { path: "/tmp/2/exec.json", estimatedTokens: 600 },
473
+ toolSummaries: {
474
+ path: "/tmp/2/tools.json",
475
+ estimatedTokens: 1600,
476
+ toolCount: 12,
477
+ },
478
+ };
479
+ emitTieredOutput("/tmp/output1", "tiered", tiers1);
480
+ emitTieredOutput("/tmp/output2", "summary-only", tiers2);
481
+ expect(errorOutput.length).toBe(2);
482
+ // Each line should be valid JSON
483
+ const parsed1 = JSON.parse(errorOutput[0]);
484
+ const parsed2 = JSON.parse(errorOutput[1]);
485
+ expect(parsed1.outputDir).toBe("/tmp/output1");
486
+ expect(parsed2.outputDir).toBe("/tmp/output2");
487
+ });
488
+ });
489
+ });
490
+ describe("Code duplication documentation", () => {
491
+ it("should have matching TieredOutputEvent interface across files", () => {
492
+ // This test documents the intentional duplication between
493
+ // cli/src/lib/jsonl-events.ts and scripts/lib/jsonl-events.ts
494
+ // as documented in FIX-002 and FIX-003
495
+ // The interface structure is tested above. This test serves as
496
+ // documentation that the duplication is intentional and tracked.
497
+ const expectedEventType = "tiered_output_generated";
498
+ const expectedOutputFormats = ["tiered", "summary-only"];
499
+ expect(expectedEventType).toBe("tiered_output_generated");
500
+ expect(expectedOutputFormats).toEqual(["tiered", "summary-only"]);
501
+ });
502
+ it("should maintain consistency reminder for developers", () => {
503
+ // This test serves as a reminder that TieredOutputEvent and
504
+ // emitTieredOutput are duplicated across two files and must
505
+ // be kept in sync when changes are made.
506
+ const files = [
507
+ "cli/src/lib/jsonl-events.ts",
508
+ "scripts/lib/jsonl-events.ts",
509
+ ];
510
+ // Documentation reminder
511
+ expect(files.length).toBe(2);
512
+ expect(files[0]).toContain("cli/src/lib/jsonl-events.ts");
513
+ expect(files[1]).toContain("scripts/lib/jsonl-events.ts");
514
+ });
515
+ });
516
+ });
@@ -13,8 +13,9 @@ import { ScopedListenerConfig } from "./lib/event-config.js";
13
13
  // Import from extracted modules
14
14
  import { parseArgs } from "./lib/cli-parser.js";
15
15
  import { runFullAssessment } from "./lib/assessment-runner.js";
16
- import { saveResults, displaySummary } from "./lib/result-output.js";
16
+ import { saveResults, saveTieredResults, saveSummaryOnly, displaySummary, } from "./lib/result-output.js";
17
17
  import { handleComparison, displayComparisonSummary, } from "./lib/comparison-handler.js";
18
+ import { shouldAutoTier, formatTokenEstimate, } from "../../client/lib/lib/assessment/summarizer/index.js";
18
19
  // ============================================================================
19
20
  // Main Entry Point
20
21
  // ============================================================================
@@ -60,13 +61,54 @@ async function main() {
60
61
  if (!options.jsonOnly) {
61
62
  displaySummary(results);
62
63
  }
63
- // Save results to file
64
- const outputPath = saveResults(options.serverName, results, options);
65
- if (options.jsonOnly) {
66
- console.log(outputPath);
64
+ // Determine output format (Issue #136: Tiered output strategy)
65
+ let effectiveFormat = options.outputFormat || "full";
66
+ // Auto-tier if requested and results exceed threshold
67
+ if (effectiveFormat === "full" &&
68
+ options.autoTier &&
69
+ shouldAutoTier(results)) {
70
+ effectiveFormat = "tiered";
71
+ if (!options.jsonOnly) {
72
+ const estimate = formatTokenEstimate(Math.ceil(JSON.stringify(results).length / 4));
73
+ console.log(`\n📊 Auto-tiering enabled: ${estimate.tokens} tokens (${estimate.recommendation})`);
74
+ }
75
+ }
76
+ // Save results in appropriate format
77
+ let outputPath;
78
+ if (effectiveFormat === "tiered") {
79
+ const tieredOutput = saveTieredResults(options.serverName, results, options);
80
+ outputPath = tieredOutput.outputDir;
81
+ if (options.jsonOnly) {
82
+ console.log(outputPath);
83
+ }
84
+ else {
85
+ console.log(`\n📁 Tiered output saved to: ${outputPath}/`);
86
+ console.log(` 📋 Executive Summary: executive-summary.json`);
87
+ console.log(` 📋 Tool Summaries: tool-summaries.json`);
88
+ console.log(` 📋 Tool Details: tools/ (${tieredOutput.toolDetailRefs.length} files)`);
89
+ console.log(` 📊 Total tokens: ~${tieredOutput.executiveSummary.estimatedTokens + tieredOutput.toolSummaries.estimatedTokens} (summaries only)\n`);
90
+ }
91
+ }
92
+ else if (effectiveFormat === "summary-only") {
93
+ outputPath = saveSummaryOnly(options.serverName, results, options);
94
+ if (options.jsonOnly) {
95
+ console.log(outputPath);
96
+ }
97
+ else {
98
+ console.log(`\n📁 Summary output saved to: ${outputPath}/`);
99
+ console.log(` 📋 Executive Summary: executive-summary.json`);
100
+ console.log(` 📋 Tool Summaries: tool-summaries.json\n`);
101
+ }
67
102
  }
68
103
  else {
69
- console.log(`📄 Results saved to: ${outputPath}\n`);
104
+ // Default: full output
105
+ outputPath = saveResults(options.serverName, results, options);
106
+ if (options.jsonOnly) {
107
+ console.log(outputPath);
108
+ }
109
+ else {
110
+ console.log(`📄 Results saved to: ${outputPath}\n`);
111
+ }
70
112
  }
71
113
  // Exit with appropriate code
72
114
  const exitCode = results.overallStatus === "FAIL" ? 1 : 0;
@@ -11,7 +11,7 @@
11
11
  import { ASSESSMENT_CATEGORY_METADATA, } from "../../../client/lib/lib/assessmentTypes.js";
12
12
  import { ASSESSMENT_PROFILES, getProfileHelpText, TIER_1_CORE_SECURITY, TIER_2_COMPLIANCE, TIER_3_CAPABILITY, TIER_4_EXTENDED, } from "../profiles.js";
13
13
  import packageJson from "../../package.json" with { type: "json" };
14
- import { safeParseModuleNames, LogLevelSchema, ReportFormatSchema, AssessmentProfileNameSchema, } from "./cli-parserSchemas.js";
14
+ import { safeParseModuleNames, LogLevelSchema, ReportFormatSchema, OutputFormatSchema, AssessmentProfileNameSchema, } from "./cli-parserSchemas.js";
15
15
  // ============================================================================
16
16
  // Constants
17
17
  // ============================================================================
@@ -197,6 +197,35 @@ export function parseArgs(argv) {
197
197
  // Enable official MCP conformance tests (requires HTTP/SSE transport with serverUrl)
198
198
  options.conformanceEnabled = true;
199
199
  break;
200
+ case "--output-format": {
201
+ // Issue #136: Tiered output strategy for large assessments
202
+ const outputFormatValue = args[++i];
203
+ if (!outputFormatValue) {
204
+ console.error("Error: --output-format requires a format");
205
+ console.error("Valid formats: full, tiered, summary-only");
206
+ setTimeout(() => process.exit(1), 10);
207
+ options.helpRequested = true;
208
+ return options;
209
+ }
210
+ const parseResult = OutputFormatSchema.safeParse(outputFormatValue);
211
+ if (!parseResult.success) {
212
+ console.error(`Error: Invalid output format: ${outputFormatValue}`);
213
+ console.error("Valid formats: full, tiered, summary-only");
214
+ setTimeout(() => process.exit(1), 10);
215
+ options.helpRequested = true;
216
+ return options;
217
+ }
218
+ options.outputFormat = parseResult.data;
219
+ break;
220
+ }
221
+ case "--auto-tier":
222
+ // Issue #136: Auto-enable tiered output when results exceed token threshold
223
+ options.autoTier = true;
224
+ break;
225
+ case "--stage-b-verbose":
226
+ // Issue #137: Stage B enrichment for Claude semantic analysis
227
+ options.stageBVerbose = true;
228
+ break;
200
229
  case "--profile": {
201
230
  const profileValue = args[++i];
202
231
  if (!profileValue) {
@@ -362,6 +391,14 @@ Options:
362
391
  --temporal-invocations <n> Number of invocations per tool for rug pull detection (default: 25)
363
392
  --skip-temporal Skip temporal/rug pull testing (faster assessment)
364
393
  --conformance Enable official MCP conformance tests (experimental, requires HTTP/SSE transport)
394
+ --output-format <fmt> Output format: full (default), tiered, summary-only
395
+ full: Complete JSON output (existing behavior)
396
+ tiered: Directory with executive-summary.json, tool-summaries.json, tools/
397
+ summary-only: Executive summary + tool summaries (no per-tool details)
398
+ --auto-tier Auto-enable tiered output when results exceed 100K tokens
399
+ --stage-b-verbose Enable Stage B enrichment for Claude semantic analysis
400
+ Adds evidence samples, payload correlations, and confidence
401
+ breakdowns to tiered output (Tier 2 + Tier 3)
365
402
  --skip-modules <list> Skip specific modules (comma-separated)
366
403
  --only-modules <list> Run only specific modules (comma-separated)
367
404
  --json Output only JSON path (no console summary)
@@ -8,9 +8,9 @@
8
8
  */
9
9
  import { z } from "zod";
10
10
  // Import shared schemas from single source of truth
11
- import { LogLevelSchema, ReportFormatSchema, TransportTypeSchema, ZOD_SCHEMA_VERSION, } from "../../../client/lib/lib/assessment/sharedSchemas.js";
11
+ import { LogLevelSchema, ReportFormatSchema, OutputFormatSchema, TransportTypeSchema, ZOD_SCHEMA_VERSION, } from "../../../client/lib/lib/assessment/sharedSchemas.js";
12
12
  // Re-export shared schemas for backwards compatibility
13
- export { LogLevelSchema, ReportFormatSchema, TransportTypeSchema };
13
+ export { LogLevelSchema, ReportFormatSchema, OutputFormatSchema, TransportTypeSchema, };
14
14
  // Export schema version for consumers
15
15
  export { ZOD_SCHEMA_VERSION };
16
16
  /**
@@ -121,6 +121,9 @@ export const AssessmentOptionsSchema = z
121
121
  profile: AssessmentProfileNameSchema.optional(),
122
122
  logLevel: LogLevelSchema.optional(),
123
123
  listModules: z.boolean().optional(),
124
+ outputFormat: OutputFormatSchema.optional(),
125
+ autoTier: z.boolean().optional(),
126
+ stageBVerbose: z.boolean().optional(),
124
127
  })
125
128
  .refine((data) => !(data.profile && (data.skipModules?.length || data.onlyModules?.length)), {
126
129
  message: "--profile cannot be used with --skip-modules or --only-modules",
@@ -255,3 +255,18 @@ export function emitPhaseComplete(phase, duration) {
255
255
  duration,
256
256
  });
257
257
  }
258
+ /**
259
+ * Emit tiered_output_generated event when tiered output files are created.
260
+ * Includes paths and token estimates for each tier.
261
+ *
262
+ * NOTE: This function is duplicated in scripts/lib/jsonl-events.ts.
263
+ * Keep both implementations in sync. See TieredOutputEvent comment above.
264
+ */
265
+ export function emitTieredOutput(outputDir, outputFormat, tiers) {
266
+ emitJSONL({
267
+ event: "tiered_output_generated",
268
+ outputDir,
269
+ outputFormat,
270
+ tiers,
271
+ });
272
+ }
@@ -7,9 +7,12 @@
7
7
  * @module cli/lib/result-output
8
8
  */
9
9
  import * as fs from "fs";
10
+ import * as path from "path";
10
11
  import { ASSESSMENT_CATEGORY_METADATA, } from "../../../client/lib/lib/assessmentTypes.js";
11
12
  import { createFormatter } from "../../../client/lib/lib/reportFormatters/index.js";
12
13
  import { generatePolicyComplianceReport } from "../../../client/lib/services/assessment/PolicyComplianceGenerator.js";
14
+ import { AssessmentSummarizer, estimateTokens, } from "../../../client/lib/lib/assessment/summarizer/index.js";
15
+ import { emitTieredOutput } from "./jsonl-events.js";
13
16
  // ============================================================================
14
17
  // Result Output
15
18
  // ============================================================================
@@ -53,6 +56,156 @@ export function saveResults(serverName, results, options) {
53
56
  }
54
57
  return finalPath;
55
58
  }
59
+ /**
60
+ * Save results in tiered format for LLM consumption.
61
+ * Creates a directory with executive summary, tool summaries, and per-tool details.
62
+ *
63
+ * Issue #136: Tiered output strategy for large assessments
64
+ *
65
+ * @param serverName - Server name for output directory
66
+ * @param results - Full assessment results
67
+ * @param options - Assessment options
68
+ * @returns Path to output directory
69
+ */
70
+ export function saveTieredResults(serverName, results, options) {
71
+ // Issue #137: Pass stageBVerbose option to summarizer for Stage B enrichment
72
+ const summarizer = new AssessmentSummarizer({
73
+ stageBVerbose: options.stageBVerbose,
74
+ });
75
+ // Determine output directory
76
+ const defaultDir = `/tmp/inspector-full-assessment-${serverName}`;
77
+ const outputDir = options.outputPath || defaultDir;
78
+ // Create directory structure
79
+ fs.mkdirSync(outputDir, { recursive: true });
80
+ fs.mkdirSync(path.join(outputDir, "tools"), { recursive: true });
81
+ // Tier 1: Executive Summary
82
+ const executiveSummary = summarizer.generateExecutiveSummary(results);
83
+ const executivePath = path.join(outputDir, "executive-summary.json");
84
+ fs.writeFileSync(executivePath, JSON.stringify(executiveSummary, null, 2));
85
+ // Tier 2: Tool Summaries
86
+ const toolSummaries = summarizer.generateToolSummaries(results);
87
+ const toolSummariesPath = path.join(outputDir, "tool-summaries.json");
88
+ fs.writeFileSync(toolSummariesPath, JSON.stringify(toolSummaries, null, 2));
89
+ // Tier 3: Per-Tool Details
90
+ const toolDetailRefs = [];
91
+ const toolNames = summarizer.getAllToolNames(results);
92
+ for (const toolName of toolNames) {
93
+ const detail = summarizer.extractToolDetail(toolName, results);
94
+ const safeFileName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_");
95
+ const relativePath = `tools/${safeFileName}.json`;
96
+ const absolutePath = path.join(outputDir, relativePath);
97
+ fs.writeFileSync(absolutePath, JSON.stringify(detail, null, 2));
98
+ const stats = fs.statSync(absolutePath);
99
+ toolDetailRefs.push({
100
+ toolName,
101
+ relativePath,
102
+ absolutePath,
103
+ fileSizeBytes: stats.size,
104
+ estimatedTokens: estimateTokens(detail),
105
+ });
106
+ }
107
+ // Create index file with all paths
108
+ const tieredOutput = {
109
+ executiveSummary,
110
+ toolSummaries,
111
+ toolDetailRefs,
112
+ outputDir,
113
+ paths: {
114
+ executiveSummary: executivePath,
115
+ toolSummaries: toolSummariesPath,
116
+ toolDetailsDir: path.join(outputDir, "tools"),
117
+ },
118
+ };
119
+ // Save index file
120
+ const indexPath = path.join(outputDir, "index.json");
121
+ fs.writeFileSync(indexPath, JSON.stringify({
122
+ timestamp: new Date().toISOString(),
123
+ serverName,
124
+ outputFormat: "tiered",
125
+ paths: tieredOutput.paths,
126
+ toolCount: toolDetailRefs.length,
127
+ totalEstimatedTokens: {
128
+ executiveSummary: executiveSummary.estimatedTokens,
129
+ toolSummaries: toolSummaries.estimatedTokens,
130
+ toolDetails: toolDetailRefs.reduce((sum, r) => sum + r.estimatedTokens, 0),
131
+ },
132
+ }, null, 2));
133
+ // Emit JSONL event for tiered output (Issue #136)
134
+ emitTieredOutput(outputDir, "tiered", {
135
+ executiveSummary: {
136
+ path: executivePath,
137
+ estimatedTokens: executiveSummary.estimatedTokens,
138
+ },
139
+ toolSummaries: {
140
+ path: toolSummariesPath,
141
+ estimatedTokens: toolSummaries.estimatedTokens,
142
+ toolCount: toolSummaries.tools.length,
143
+ },
144
+ toolDetails: {
145
+ directory: path.join(outputDir, "tools"),
146
+ fileCount: toolDetailRefs.length,
147
+ totalEstimatedTokens: toolDetailRefs.reduce((sum, r) => sum + r.estimatedTokens, 0),
148
+ },
149
+ });
150
+ return tieredOutput;
151
+ }
152
+ /**
153
+ * Save results in summary-only format (Tier 1 + Tier 2, no per-tool details).
154
+ *
155
+ * Issue #136: Tiered output strategy for large assessments
156
+ *
157
+ * @param serverName - Server name for output
158
+ * @param results - Full assessment results
159
+ * @param options - Assessment options
160
+ * @returns Path to output directory
161
+ */
162
+ export function saveSummaryOnly(serverName, results, options) {
163
+ // Issue #137: Pass stageBVerbose option to summarizer for Stage B enrichment
164
+ const summarizer = new AssessmentSummarizer({
165
+ stageBVerbose: options.stageBVerbose,
166
+ });
167
+ // Determine output directory
168
+ const defaultDir = `/tmp/inspector-full-assessment-${serverName}`;
169
+ const outputDir = options.outputPath || defaultDir;
170
+ // Create directory
171
+ fs.mkdirSync(outputDir, { recursive: true });
172
+ // Tier 1: Executive Summary
173
+ const executiveSummary = summarizer.generateExecutiveSummary(results);
174
+ const executivePath = path.join(outputDir, "executive-summary.json");
175
+ fs.writeFileSync(executivePath, JSON.stringify(executiveSummary, null, 2));
176
+ // Tier 2: Tool Summaries
177
+ const toolSummaries = summarizer.generateToolSummaries(results);
178
+ const toolSummariesPath = path.join(outputDir, "tool-summaries.json");
179
+ fs.writeFileSync(toolSummariesPath, JSON.stringify(toolSummaries, null, 2));
180
+ // Create index file
181
+ const indexPath = path.join(outputDir, "index.json");
182
+ fs.writeFileSync(indexPath, JSON.stringify({
183
+ timestamp: new Date().toISOString(),
184
+ serverName,
185
+ outputFormat: "summary-only",
186
+ paths: {
187
+ executiveSummary: executivePath,
188
+ toolSummaries: toolSummariesPath,
189
+ },
190
+ totalEstimatedTokens: {
191
+ executiveSummary: executiveSummary.estimatedTokens,
192
+ toolSummaries: toolSummaries.estimatedTokens,
193
+ },
194
+ }, null, 2));
195
+ // Emit JSONL event for summary-only output (Issue #136)
196
+ emitTieredOutput(outputDir, "summary-only", {
197
+ executiveSummary: {
198
+ path: executivePath,
199
+ estimatedTokens: executiveSummary.estimatedTokens,
200
+ },
201
+ toolSummaries: {
202
+ path: toolSummariesPath,
203
+ estimatedTokens: toolSummaries.estimatedTokens,
204
+ toolCount: toolSummaries.tools.length,
205
+ },
206
+ });
207
+ return outputDir;
208
+ }
56
209
  // ============================================================================
57
210
  // Summary Display
58
211
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.34.2",
3
+ "version": "1.35.1",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",