jest-test-lineage-reporter 2.0.2 → 2.1.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,469 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server for Jest Test Lineage Reporter
5
+ * Exposes test analytics functionality via Model Context Protocol
6
+ */
7
+
8
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
9
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
10
+ const {
11
+ CallToolRequestSchema,
12
+ ListToolsRequestSchema,
13
+ } = require('@modelcontextprotocol/sdk/types.js');
14
+
15
+ const { runJest } = require('../cli/utils/jest-runner');
16
+ const { loadLineageData, processLineageDataForMutation } = require('../cli/utils/data-loader');
17
+ const { loadFullConfig } = require('../cli/utils/config-loader');
18
+ const MutationTester = require('../MutationTester');
19
+ const TestCoverageReporter = require('../TestCoverageReporter');
20
+
21
+ // Create MCP server
22
+ const server = new Server(
23
+ {
24
+ name: 'jest-test-lineage-reporter',
25
+ version: '1.0.0',
26
+ },
27
+ {
28
+ capabilities: {
29
+ tools: {},
30
+ },
31
+ }
32
+ );
33
+
34
+ // Define available tools
35
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
36
+ return {
37
+ tools: [
38
+ {
39
+ name: 'run_tests',
40
+ description: 'Run Jest tests with lineage tracking and generate coverage data',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ args: {
45
+ type: 'array',
46
+ items: { type: 'string' },
47
+ description: 'Jest command-line arguments (e.g., ["--watch", "--testPathPattern=calculator"])',
48
+ default: [],
49
+ },
50
+ enableLineage: {
51
+ type: 'boolean',
52
+ description: 'Enable lineage tracking',
53
+ default: true,
54
+ },
55
+ enablePerformance: {
56
+ type: 'boolean',
57
+ description: 'Enable performance tracking',
58
+ default: true,
59
+ },
60
+ enableQuality: {
61
+ type: 'boolean',
62
+ description: 'Enable quality analysis',
63
+ default: true,
64
+ },
65
+ },
66
+ },
67
+ },
68
+ {
69
+ name: 'run_mutation_testing',
70
+ description: 'Run mutation testing on existing lineage data to assess test effectiveness',
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ dataPath: {
75
+ type: 'string',
76
+ description: 'Path to lineage data file',
77
+ default: '.jest-lineage-data.json',
78
+ },
79
+ threshold: {
80
+ type: 'number',
81
+ description: 'Minimum mutation score threshold (0-100)',
82
+ default: 80,
83
+ },
84
+ timeout: {
85
+ type: 'number',
86
+ description: 'Timeout per mutation in milliseconds',
87
+ default: 5000,
88
+ },
89
+ debug: {
90
+ type: 'boolean',
91
+ description: 'Create debug mutation files instead of running tests',
92
+ default: false,
93
+ },
94
+ },
95
+ },
96
+ },
97
+ {
98
+ name: 'generate_report',
99
+ description: 'Generate HTML report from existing lineage data',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: {
103
+ dataPath: {
104
+ type: 'string',
105
+ description: 'Path to lineage data file',
106
+ default: '.jest-lineage-data.json',
107
+ },
108
+ outputPath: {
109
+ type: 'string',
110
+ description: 'Output HTML file path',
111
+ default: 'test-lineage-report.html',
112
+ },
113
+ },
114
+ },
115
+ },
116
+ {
117
+ name: 'query_coverage',
118
+ description: 'Query which tests cover specific files or lines',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ file: {
123
+ type: 'string',
124
+ description: 'File path to query (e.g., "src/calculator.ts")',
125
+ },
126
+ line: {
127
+ type: 'number',
128
+ description: 'Optional line number to query',
129
+ },
130
+ dataPath: {
131
+ type: 'string',
132
+ description: 'Path to lineage data file',
133
+ default: '.jest-lineage-data.json',
134
+ },
135
+ },
136
+ required: ['file'],
137
+ },
138
+ },
139
+ {
140
+ name: 'analyze_full',
141
+ description: 'Run full workflow: tests, mutation testing, and generate report',
142
+ inputSchema: {
143
+ type: 'object',
144
+ properties: {
145
+ skipTests: {
146
+ type: 'boolean',
147
+ description: 'Skip running tests (use existing data)',
148
+ default: false,
149
+ },
150
+ skipMutation: {
151
+ type: 'boolean',
152
+ description: 'Skip mutation testing',
153
+ default: false,
154
+ },
155
+ threshold: {
156
+ type: 'number',
157
+ description: 'Mutation score threshold',
158
+ default: 80,
159
+ },
160
+ outputPath: {
161
+ type: 'string',
162
+ description: 'Output HTML file path',
163
+ default: 'test-lineage-report.html',
164
+ },
165
+ },
166
+ },
167
+ },
168
+ ],
169
+ };
170
+ });
171
+
172
+ // Handle tool execution
173
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
174
+ const { name, arguments: args } = request.params;
175
+
176
+ try {
177
+ switch (name) {
178
+ case 'run_tests': {
179
+ const result = await runJest({
180
+ args: args.args || [],
181
+ enableLineage: args.enableLineage !== false,
182
+ enablePerformance: args.enablePerformance !== false,
183
+ enableQuality: args.enableQuality !== false,
184
+ quiet: false,
185
+ });
186
+
187
+ return {
188
+ content: [
189
+ {
190
+ type: 'text',
191
+ text: JSON.stringify({
192
+ success: result.success,
193
+ exitCode: result.exitCode,
194
+ message: result.success
195
+ ? 'Tests completed successfully. Lineage data saved to .jest-lineage-data.json'
196
+ : `Tests failed with exit code ${result.exitCode}`,
197
+ }, null, 2),
198
+ },
199
+ ],
200
+ };
201
+ }
202
+
203
+ case 'run_mutation_testing': {
204
+ const config = loadFullConfig({
205
+ threshold: args.threshold,
206
+ timeout: args.timeout,
207
+ debug: args.debug,
208
+ });
209
+
210
+ const rawData = loadLineageData(args.dataPath || '.jest-lineage-data.json');
211
+ const lineageData = processLineageDataForMutation(rawData);
212
+
213
+ const mutationTester = new MutationTester(config);
214
+ mutationTester.setLineageData(lineageData);
215
+
216
+ const results = await mutationTester.runMutationTesting();
217
+
218
+ await mutationTester.cleanup();
219
+
220
+ return {
221
+ content: [
222
+ {
223
+ type: 'text',
224
+ text: JSON.stringify({
225
+ success: true,
226
+ mutationScore: results.mutationScore,
227
+ totalMutations: results.totalMutations,
228
+ killedMutations: results.killedMutations,
229
+ survivedMutations: results.survivedMutations,
230
+ timeoutMutations: results.timeoutMutations || 0,
231
+ errorMutations: results.errorMutations || 0,
232
+ meetsThreshold: results.mutationScore >= (args.threshold || 80),
233
+ message: `Mutation testing complete. Score: ${results.mutationScore.toFixed(1)}%`,
234
+ }, null, 2),
235
+ },
236
+ ],
237
+ };
238
+ }
239
+
240
+ case 'generate_report': {
241
+ const rawData = loadLineageData(args.dataPath || '.jest-lineage-data.json');
242
+ const lineageData = processLineageDataForMutation(rawData);
243
+
244
+ const reporter = new TestCoverageReporter(
245
+ { rootDir: process.cwd() },
246
+ { outputFile: args.outputPath || 'test-lineage-report.html' }
247
+ );
248
+
249
+ reporter.processLineageResults(lineageData, 'unknown');
250
+ await reporter.generateHtmlReport();
251
+
252
+ return {
253
+ content: [
254
+ {
255
+ type: 'text',
256
+ text: JSON.stringify({
257
+ success: true,
258
+ outputPath: args.outputPath || 'test-lineage-report.html',
259
+ message: 'HTML report generated successfully',
260
+ }, null, 2),
261
+ },
262
+ ],
263
+ };
264
+ }
265
+
266
+ case 'query_coverage': {
267
+ const rawData = loadLineageData(args.dataPath || '.jest-lineage-data.json');
268
+ const lineageData = processLineageDataForMutation(rawData);
269
+
270
+ const path = require('path');
271
+ const normalizedFile = path.normalize(args.file);
272
+
273
+ const matchingFiles = Object.keys(lineageData).filter(f =>
274
+ f.includes(normalizedFile) || normalizedFile.includes(path.basename(f))
275
+ );
276
+
277
+ if (matchingFiles.length === 0) {
278
+ return {
279
+ content: [
280
+ {
281
+ type: 'text',
282
+ text: JSON.stringify({
283
+ success: false,
284
+ error: `No coverage data found for file: ${args.file}`,
285
+ }, null, 2),
286
+ },
287
+ ],
288
+ };
289
+ }
290
+
291
+ const targetFile = matchingFiles[0];
292
+ const fileCoverage = lineageData[targetFile];
293
+
294
+ if (args.line) {
295
+ const lineNumber = args.line.toString();
296
+ if (!fileCoverage[lineNumber]) {
297
+ return {
298
+ content: [
299
+ {
300
+ type: 'text',
301
+ text: JSON.stringify({
302
+ success: false,
303
+ error: `No coverage data for line ${args.line} in ${args.file}`,
304
+ }, null, 2),
305
+ },
306
+ ],
307
+ };
308
+ }
309
+
310
+ return {
311
+ content: [
312
+ {
313
+ type: 'text',
314
+ text: JSON.stringify({
315
+ success: true,
316
+ file: targetFile,
317
+ line: lineNumber,
318
+ tests: fileCoverage[lineNumber],
319
+ }, null, 2),
320
+ },
321
+ ],
322
+ };
323
+ } else {
324
+ const lines = Object.keys(fileCoverage);
325
+ const totalTests = new Set(
326
+ lines.flatMap(lineNum => fileCoverage[lineNum].map(t => t.testName))
327
+ ).size;
328
+
329
+ return {
330
+ content: [
331
+ {
332
+ type: 'text',
333
+ text: JSON.stringify({
334
+ success: true,
335
+ file: targetFile,
336
+ linesCovered: lines.length,
337
+ totalTests,
338
+ coverageByLine: fileCoverage,
339
+ }, null, 2),
340
+ },
341
+ ],
342
+ };
343
+ }
344
+ }
345
+
346
+ case 'analyze_full': {
347
+ const results = {
348
+ steps: [],
349
+ };
350
+
351
+ // Step 1: Run tests
352
+ if (!args.skipTests) {
353
+ const testResult = await runJest({
354
+ args: [],
355
+ enableLineage: true,
356
+ enablePerformance: true,
357
+ enableQuality: true,
358
+ quiet: false,
359
+ });
360
+
361
+ results.steps.push({
362
+ step: 'tests',
363
+ success: testResult.success,
364
+ exitCode: testResult.exitCode,
365
+ });
366
+
367
+ if (!testResult.success) {
368
+ results.success = false;
369
+ results.message = 'Tests failed';
370
+ return {
371
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
372
+ };
373
+ }
374
+ }
375
+
376
+ // Step 2: Mutation testing
377
+ if (!args.skipMutation) {
378
+ try {
379
+ const config = loadFullConfig({ threshold: args.threshold });
380
+ const rawData = loadLineageData('.jest-lineage-data.json');
381
+ const lineageData = processLineageDataForMutation(rawData);
382
+
383
+ const mutationTester = new MutationTester(config);
384
+ mutationTester.setLineageData(lineageData);
385
+
386
+ const mutationResults = await mutationTester.runMutationTesting();
387
+ await mutationTester.cleanup();
388
+
389
+ results.steps.push({
390
+ step: 'mutation',
391
+ success: mutationResults.mutationScore >= (args.threshold || 80),
392
+ mutationScore: mutationResults.mutationScore,
393
+ });
394
+ } catch (err) {
395
+ results.steps.push({
396
+ step: 'mutation',
397
+ success: false,
398
+ error: err.message,
399
+ });
400
+ }
401
+ }
402
+
403
+ // Step 3: Generate report
404
+ try {
405
+ const rawData = loadLineageData('.jest-lineage-data.json');
406
+ const lineageData = processLineageDataForMutation(rawData);
407
+
408
+ const reporter = new TestCoverageReporter(
409
+ { rootDir: process.cwd() },
410
+ { outputFile: args.outputPath || 'test-lineage-report.html' }
411
+ );
412
+
413
+ reporter.processLineageResults(lineageData, 'unknown');
414
+ await reporter.generateHtmlReport();
415
+
416
+ results.steps.push({
417
+ step: 'report',
418
+ success: true,
419
+ outputPath: args.outputPath || 'test-lineage-report.html',
420
+ });
421
+ } catch (err) {
422
+ results.steps.push({
423
+ step: 'report',
424
+ success: false,
425
+ error: err.message,
426
+ });
427
+ }
428
+
429
+ results.success = results.steps.every(s => s.success);
430
+ results.message = results.success
431
+ ? 'Full analysis completed successfully'
432
+ : 'Some steps failed';
433
+
434
+ return {
435
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
436
+ };
437
+ }
438
+
439
+ default:
440
+ throw new Error(`Unknown tool: ${name}`);
441
+ }
442
+ } catch (error) {
443
+ return {
444
+ content: [
445
+ {
446
+ type: 'text',
447
+ text: JSON.stringify({
448
+ success: false,
449
+ error: error.message,
450
+ stack: error.stack,
451
+ }, null, 2),
452
+ },
453
+ ],
454
+ isError: true,
455
+ };
456
+ }
457
+ });
458
+
459
+ // Start server
460
+ async function main() {
461
+ const transport = new StdioServerTransport();
462
+ await server.connect(transport);
463
+ console.error('Jest Test Lineage Reporter MCP server running on stdio');
464
+ }
465
+
466
+ main().catch((error) => {
467
+ console.error('Server error:', error);
468
+ process.exit(1);
469
+ });
@@ -0,0 +1,82 @@
1
+ // Example file to demonstrate CPU cycle and performance tracking
2
+
3
+ export function lightweightFunction(x: number): number {
4
+ return x + 1; // Very fast operation - minimal CPU cycles
5
+ }
6
+
7
+ export function mediumFunction(x: number): number {
8
+ let result = x;
9
+ let i = 0;
10
+ while (i < 100) { // Medium CPU usage
11
+ result = Math.sqrt(result + i);
12
+ i++;
13
+ }
14
+ return result;
15
+ }
16
+
17
+ export function heavyFunction(x: number): number {
18
+ let result = x;
19
+ let i = 0;
20
+ while (i < 1000) { // Heavy CPU usage - many cycles (reduced for testing)
21
+ result = Math.sin(Math.cos(Math.sqrt(result + i)));
22
+ i++;
23
+ }
24
+ return result;
25
+ }
26
+
27
+ export function memoryIntensiveFunction(size: number): number[] {
28
+ const array = new Array(size); // Memory allocation
29
+ let i = 0;
30
+ while (i < size) {
31
+ array[i] = Math.random() * i; // Memory writes
32
+ i++;
33
+ }
34
+ return array;
35
+ }
36
+
37
+ export function recursiveFunction(n: number): number {
38
+ if (n <= 1) {
39
+ return lightweightFunction(n); // Light operation at leaf
40
+ }
41
+ if (n > 5) return n; // Prevent exponential explosion
42
+ return recursiveFunction(n - 1) + recursiveFunction(n - 2); // Limited recursion
43
+ }
44
+
45
+ export function nestedCallsFunction(x: number): number {
46
+ const light = lightweightFunction(x); // Depth 2
47
+ const medium = mediumFunction(light); // Depth 2
48
+ const heavy = heavyFunction(medium); // Depth 2
49
+ return heavy;
50
+ }
51
+
52
+ export function mixedPerformanceFunction(iterations: number): number {
53
+ let result = 0;
54
+ let i = 0;
55
+
56
+ while (i < iterations) {
57
+ if (i % 3 === 0) {
58
+ result += lightweightFunction(i); // Fast path
59
+ } else if (i % 3 === 1) {
60
+ result += mediumFunction(i); // Medium path
61
+ } else {
62
+ result += heavyFunction(i); // Slow path
63
+ }
64
+ i++;
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ // Function that demonstrates different performance characteristics
71
+ export function performanceVariableFunction(mode: 'fast' | 'medium' | 'slow'): number {
72
+ switch (mode) {
73
+ case 'fast':
74
+ return lightweightFunction(42);
75
+ case 'medium':
76
+ return mediumFunction(42);
77
+ case 'slow':
78
+ return heavyFunction(42);
79
+ default:
80
+ return 0;
81
+ }
82
+ }
@@ -0,0 +1,79 @@
1
+ // Example file to demonstrate test quality metrics
2
+
3
+ export function simpleFunction(x: number): number {
4
+ return x * 2;
5
+ }
6
+
7
+ export function complexFunction(data: any): any {
8
+ if (data === null || data === undefined) {
9
+ throw new Error('Data is required');
10
+ }
11
+
12
+ if (typeof data === 'string') {
13
+ return data.toUpperCase();
14
+ } else if (typeof data === 'number') {
15
+ if (data < 0) {
16
+ return Math.abs(data);
17
+ } else if (data === 0) {
18
+ return 1;
19
+ } else {
20
+ return data * data;
21
+ }
22
+ } else if (Array.isArray(data)) {
23
+ return data.map(item => item * 2);
24
+ } else {
25
+ return JSON.stringify(data);
26
+ }
27
+ }
28
+
29
+ export async function asyncFunction(delay: number): Promise<string> {
30
+ return new Promise((resolve, reject) => {
31
+ if (delay < 0) {
32
+ reject(new Error('Delay cannot be negative'));
33
+ } else {
34
+ setTimeout(() => {
35
+ resolve(`Completed after ${delay}ms`);
36
+ }, delay);
37
+ }
38
+ });
39
+ }
40
+
41
+ export function errorProneFunction(input: any): string {
42
+ // This function has potential issues
43
+ return input.toString().toUpperCase();
44
+ }
45
+
46
+ export function wellTestedFunction(a: number, b: number): number {
47
+ if (a === null || a === undefined) {
48
+ throw new Error('Parameter a is required');
49
+ }
50
+ if (b === null || b === undefined) {
51
+ throw new Error('Parameter b is required');
52
+ }
53
+ if (typeof a !== 'number' || typeof b !== 'number') {
54
+ throw new Error('Both parameters must be numbers');
55
+ }
56
+ if (a < 0 || b < 0) {
57
+ throw new Error('Parameters must be non-negative');
58
+ }
59
+
60
+ return a + b;
61
+ }
62
+
63
+ export class Calculator {
64
+ private history: number[] = [];
65
+
66
+ add(a: number, b: number): number {
67
+ const result = a + b;
68
+ this.history.push(result);
69
+ return result;
70
+ }
71
+
72
+ getHistory(): number[] {
73
+ return [...this.history];
74
+ }
75
+
76
+ clearHistory(): void {
77
+ this.history = [];
78
+ }
79
+ }
@@ -0,0 +1,19 @@
1
+ // This file contains functions with absolutely terrible tests that guarantee survived mutations
2
+
3
+ export function demonstrateSurvivedMutations(input: number): number {
4
+ if (input === 42) {
5
+ return 100;
6
+ } else if (input > 50) {
7
+ return input * 2;
8
+ } else {
9
+ return input + 10;
10
+ }
11
+ }
12
+
13
+ export function anotherWeakFunction(x: number, y: number): boolean {
14
+ if (x > y) {
15
+ return true;
16
+ } else {
17
+ return false;
18
+ }
19
+ }
@@ -0,0 +1,37 @@
1
+ // This file contains functions with absolutely terrible tests that guarantee survived mutations
2
+
3
+ export function definitelyWillSurvive(x: number): number {
4
+ if (!(!(x === 5))) {
5
+ return 42;
6
+ } else if (x > 10) {
7
+ return x + 100;
8
+ } else {
9
+ return x * 2;
10
+ }
11
+ }
12
+ export function anotherSurvivor(a: number, b: number): number {
13
+ const sum = a + b;
14
+ if (sum > 50) {
15
+ return sum - 10;
16
+ } else {
17
+ return sum / 2;
18
+ }
19
+ }
20
+ export function booleanSurvivor(x: number): boolean {
21
+ if (x > 0) {
22
+ return true;
23
+ } else {
24
+ return false;
25
+ }
26
+ }
27
+ export function guaranteedSurvivor(input: number): number {
28
+ if (input === 10) {
29
+ const result = 100;
30
+ return result;
31
+ } else if (input === 20) {
32
+ const result = 200;
33
+ return result;
34
+ } else {
35
+ return input + 5;
36
+ }
37
+ }