jest-test-lineage-reporter 2.0.1 → 2.1.0

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.
Files changed (34) hide show
  1. package/README.md +252 -0
  2. package/bin/jest-lineage.js +20 -0
  3. package/package.json +14 -5
  4. package/src/MutationTester.js +1154 -0
  5. package/src/__tests__/assertion-test.test.ts +59 -0
  6. package/src/__tests__/calculator.test.ts +30 -0
  7. package/src/__tests__/depth-example.test.ts +237 -0
  8. package/src/__tests__/gc-pressure-example.test.ts +169 -0
  9. package/src/__tests__/performance-example.test.ts +83 -0
  10. package/src/__tests__/quality-example.test.ts +122 -0
  11. package/src/__tests__/survived-mutations-example.test.ts +32 -0
  12. package/src/__tests__/truly-weak-example.test.ts +90 -0
  13. package/src/__tests__/weak-test-example.test.ts +222 -0
  14. package/src/babel-plugin-mutation-tester.js +402 -0
  15. package/src/calculator.ts +12 -0
  16. package/src/cli/commands/analyze.js +91 -0
  17. package/src/cli/commands/mutate.js +89 -0
  18. package/src/cli/commands/query.js +107 -0
  19. package/src/cli/commands/report.js +65 -0
  20. package/src/cli/commands/test.js +56 -0
  21. package/src/cli/index.js +89 -0
  22. package/src/cli/utils/config-loader.js +114 -0
  23. package/src/cli/utils/data-loader.js +118 -0
  24. package/src/cli/utils/jest-runner.js +105 -0
  25. package/src/cli/utils/output-formatter.js +126 -0
  26. package/src/depth-example.ts +66 -0
  27. package/src/gc-pressure-example.ts +158 -0
  28. package/src/global.d.ts +7 -0
  29. package/src/mcp/server.js +469 -0
  30. package/src/performance-example.ts +82 -0
  31. package/src/quality-example.ts +79 -0
  32. package/src/survived-mutations-example.ts +19 -0
  33. package/src/truly-weak-example.ts +37 -0
  34. package/src/weak-test-example.ts +91 -0
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Babel Plugin for Mutation Testing
3
+ * Creates targeted mutations for specific lines based on lineage tracking data
4
+ */
5
+
6
+ /**
7
+ * Creates a mutation testing plugin that targets a specific line and mutation type
8
+ * @param {number} targetLine - Line number to mutate
9
+ * @param {string} mutationType - Type of mutation to apply
10
+ * @param {Object} config - Mutation configuration
11
+ */
12
+ function createMutationPlugin(targetLine, mutationType, config = {}) {
13
+ return function({ types: t }, options = {}) {
14
+ return {
15
+ name: 'mutation-tester',
16
+ visitor: {
17
+ Program: {
18
+ enter(path, state) {
19
+ state.filename = state.file.opts.filename;
20
+ state.shouldMutate = false;
21
+ state.mutationApplied = false;
22
+ state.targetLine = targetLine;
23
+ state.mutationType = mutationType;
24
+ state.config = { ...config, ...options };
25
+ }
26
+ },
27
+
28
+ // ARITHMETIC OPERATORS: +, -, *, /, %
29
+ BinaryExpression(path, state) {
30
+ if (!shouldApplyMutation(path, state, 'arithmetic')) return;
31
+
32
+ const operator = path.node.operator;
33
+ const mutations = {
34
+ '+': '-', // Addition to Subtraction
35
+ '-': '+', // Subtraction to Addition
36
+ '*': '/', // Multiplication to Division
37
+ '/': '*', // Division to Multiplication
38
+ '%': '*', // Modulo to Multiplication
39
+ '==': '!=', // Equality to Inequality
40
+ '!=': '==', // Inequality to Equality
41
+ '===': '!==', // Strict equality to strict inequality
42
+ '!==': '===', // Strict inequality to strict equality
43
+ '<': '>=', // Less than to Greater/Equal
44
+ '>': '<=', // Greater than to Less/Equal
45
+ '<=': '>', // Less/Equal to Greater
46
+ '>=': '<' // Greater/Equal to Less
47
+ };
48
+
49
+ if (mutations[operator]) {
50
+ path.node.operator = mutations[operator];
51
+ state.mutationApplied = true;
52
+ logMutation(state, `${operator} → ${mutations[operator]}`);
53
+ }
54
+ },
55
+
56
+ // LOGICAL OPERATORS: &&, ||
57
+ LogicalExpression(path, state) {
58
+ if (!shouldApplyMutation(path, state, 'logical')) return;
59
+
60
+ const operator = path.node.operator;
61
+ const mutations = {
62
+ '&&': '||', // AND to OR
63
+ '||': '&&' // OR to AND
64
+ };
65
+
66
+ if (mutations[operator]) {
67
+ path.node.operator = mutations[operator];
68
+ state.mutationApplied = true;
69
+ logMutation(state, `${operator} → ${mutations[operator]}`);
70
+ }
71
+ },
72
+
73
+ // UNARY OPERATORS: !, ++, --
74
+ UnaryExpression(path, state) {
75
+ if (!shouldApplyMutation(path, state, 'logical')) return;
76
+
77
+ if (path.node.operator === '!') {
78
+ // Remove negation: !condition → condition
79
+ path.replaceWith(path.node.argument);
80
+ state.mutationApplied = true;
81
+ logMutation(state, '! → (removed)');
82
+ }
83
+ },
84
+
85
+ // UPDATE EXPRESSIONS: ++, --
86
+ UpdateExpression(path, state) {
87
+ if (!shouldApplyMutation(path, state, 'increments')) return;
88
+
89
+ const operator = path.node.operator;
90
+ const mutations = {
91
+ '++': '--', // Increment to Decrement
92
+ '--': '++' // Decrement to Increment
93
+ };
94
+
95
+ if (mutations[operator]) {
96
+ path.node.operator = mutations[operator];
97
+ state.mutationApplied = true;
98
+ logMutation(state, `${operator} → ${mutations[operator]}`);
99
+ }
100
+ },
101
+
102
+ // ASSIGNMENT OPERATORS: =, +=, -=, etc.
103
+ AssignmentExpression(path, state) {
104
+ if (!shouldApplyMutation(path, state, 'assignment')) return;
105
+
106
+ const operator = path.node.operator;
107
+ const mutations = {
108
+ '+=': '-=', // Add-assign to Subtract-assign
109
+ '-=': '+=', // Subtract-assign to Add-assign
110
+ '*=': '/=', // Multiply-assign to Divide-assign
111
+ '/=': '*=', // Divide-assign to Multiply-assign
112
+ '%=': '*=' // Modulo-assign to Multiply-assign
113
+ };
114
+
115
+ if (mutations[operator]) {
116
+ path.node.operator = mutations[operator];
117
+ state.mutationApplied = true;
118
+ logMutation(state, `${operator} → ${mutations[operator]}`);
119
+ }
120
+ },
121
+
122
+ // CONDITIONAL STATEMENTS: if, while, for
123
+ IfStatement(path, state) {
124
+ if (!shouldApplyMutation(path, state, 'conditional')) return;
125
+
126
+ // Negate condition: if (x > 0) → if (!(x > 0))
127
+ const { types: t } = require('@babel/core');
128
+ path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
129
+ state.mutationApplied = true;
130
+ logMutation(state, 'if condition → !(condition)');
131
+ },
132
+
133
+ // WHILE LOOPS
134
+ WhileStatement(path, state) {
135
+ if (!shouldApplyMutation(path, state, 'conditional')) return;
136
+
137
+ const { types: t } = require('@babel/core');
138
+ path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
139
+ state.mutationApplied = true;
140
+ logMutation(state, 'while condition → !(condition)');
141
+ },
142
+
143
+ // FOR LOOPS
144
+ ForStatement(path, state) {
145
+ if (!shouldApplyMutation(path, state, 'conditional')) return;
146
+
147
+ // Only mutate if there's a test condition
148
+ if (path.node.test) {
149
+ const { types: t } = require('@babel/core');
150
+ path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
151
+ state.mutationApplied = true;
152
+ logMutation(state, 'for condition → !(condition)');
153
+ }
154
+ },
155
+
156
+ // RETURN STATEMENTS
157
+ ReturnStatement(path, state) {
158
+ if (!shouldApplyMutation(path, state, 'returns')) return;
159
+
160
+ const { types: t } = require('@babel/core');
161
+
162
+ if (path.node.argument) {
163
+ // Apply type-safe mutations based on the argument type
164
+ if (t.isNumericLiteral(path.node.argument)) {
165
+ // For numbers, change to 0 instead of null to avoid type errors
166
+ const originalValue = path.node.argument.value;
167
+ const newValue = originalValue === 0 ? 1 : 0;
168
+ path.node.argument = t.numericLiteral(newValue);
169
+ state.mutationApplied = true;
170
+ logMutation(state, `return ${originalValue} → return ${newValue}`);
171
+ } else if (t.isBooleanLiteral(path.node.argument)) {
172
+ // For booleans, flip the value
173
+ const originalValue = path.node.argument.value;
174
+ path.node.argument = t.booleanLiteral(!originalValue);
175
+ state.mutationApplied = true;
176
+ logMutation(state, `return ${originalValue} → return ${!originalValue}`);
177
+ } else if (t.isStringLiteral(path.node.argument)) {
178
+ // For strings, change to empty string
179
+ const originalValue = path.node.argument.value;
180
+ path.node.argument = t.stringLiteral("");
181
+ state.mutationApplied = true;
182
+ logMutation(state, `return "${originalValue}" → return ""`);
183
+ } else if (t.isBinaryExpression(path.node.argument)) {
184
+ // For binary expressions like a + b, try to mutate the operator or operands
185
+ const expr = path.node.argument;
186
+ if (expr.operator === '+') {
187
+ // Change + to - for arithmetic expressions
188
+ expr.operator = '-';
189
+ state.mutationApplied = true;
190
+ logMutation(state, `return a + b → return a - b`);
191
+ } else if (expr.operator === '-') {
192
+ // Change - to + for arithmetic expressions
193
+ expr.operator = '+';
194
+ state.mutationApplied = true;
195
+ logMutation(state, `return a - b → return a + b`);
196
+ } else if (expr.operator === '*') {
197
+ // Change * to / for arithmetic expressions
198
+ expr.operator = '/';
199
+ state.mutationApplied = true;
200
+ logMutation(state, `return a * b → return a / b`);
201
+ } else if (expr.operator === '/') {
202
+ // Change / to * for arithmetic expressions
203
+ expr.operator = '*';
204
+ state.mutationApplied = true;
205
+ logMutation(state, `return a / b → return a * b`);
206
+ } else {
207
+ // For other binary expressions, skip to avoid breaking module loading
208
+ logMutation(state, `return expression → skipped (complex binary expression)`);
209
+ }
210
+ } else {
211
+ // For other complex expressions, skip mutation to avoid breaking module loading
212
+ // This is more conservative but safer
213
+ logMutation(state, `return expression → skipped (complex expression)`);
214
+ }
215
+ }
216
+ },
217
+
218
+ // LITERAL VALUES: numbers, booleans, strings
219
+ Literal(path, state) {
220
+ if (!shouldApplyMutation(path, state, 'literals')) return;
221
+
222
+ const value = path.node.value;
223
+
224
+ if (typeof value === 'number') {
225
+ // Mutate numbers: 5 → 0, 0 → 1, negative → positive
226
+ const newValue = value === 0 ? 1 : (value > 0 ? 0 : Math.abs(value));
227
+ path.node.value = newValue;
228
+ state.mutationApplied = true;
229
+ logMutation(state, `${value} → ${newValue}`);
230
+ } else if (typeof value === 'boolean') {
231
+ // Flip booleans: true → false, false → true
232
+ path.node.value = !value;
233
+ state.mutationApplied = true;
234
+ logMutation(state, `${value} → ${!value}`);
235
+ } else if (typeof value === 'string' && value.length > 0) {
236
+ // Empty strings: "hello" → ""
237
+ path.node.value = "";
238
+ state.mutationApplied = true;
239
+ logMutation(state, `"${value}" → ""`);
240
+ }
241
+ },
242
+
243
+ // NUMERIC LITERALS (for newer Babel versions)
244
+ NumericLiteral(path, state) {
245
+ if (!shouldApplyMutation(path, state, 'literals')) return;
246
+
247
+ const value = path.node.value;
248
+ const newValue = value === 0 ? 1 : (value > 0 ? 0 : Math.abs(value));
249
+ path.node.value = newValue;
250
+ state.mutationApplied = true;
251
+ logMutation(state, `${value} → ${newValue}`);
252
+ },
253
+
254
+ // BOOLEAN LITERALS
255
+ BooleanLiteral(path, state) {
256
+ if (!shouldApplyMutation(path, state, 'literals')) return;
257
+
258
+ const value = path.node.value;
259
+ path.node.value = !value;
260
+ state.mutationApplied = true;
261
+ logMutation(state, `${value} → ${!value}`);
262
+ },
263
+
264
+ // STRING LITERALS
265
+ StringLiteral(path, state) {
266
+ if (!shouldApplyMutation(path, state, 'literals')) return;
267
+
268
+ const value = path.node.value;
269
+ if (value.length > 0) {
270
+ path.node.value = "";
271
+ state.mutationApplied = true;
272
+ logMutation(state, `"${value}" → ""`);
273
+ }
274
+ }
275
+ }
276
+ };
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Determines if a mutation should be applied to the current node
282
+ */
283
+ function shouldApplyMutation(path, state, operatorType) {
284
+ const lineNumber = path.node.loc?.start.line;
285
+
286
+ // Check if this is the target line
287
+ if (lineNumber !== state.targetLine) {
288
+ return false;
289
+ }
290
+
291
+ // Check if this operator type is enabled
292
+ if (state.config.mutationOperators && !state.config.mutationOperators[operatorType]) {
293
+ return false;
294
+ }
295
+
296
+ // Check if we've already applied a mutation (one per line)
297
+ if (state.mutationApplied) {
298
+ return false;
299
+ }
300
+
301
+ return true;
302
+ }
303
+
304
+ /**
305
+ * Logs mutation information
306
+ */
307
+ function logMutation(state, description) {
308
+ if (state.config.enableDebugLogging) {
309
+ console.log(`🧬 Mutation applied at ${state.filename}:${state.targetLine} - ${description}`);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Gets all possible mutations for a given line of code by analyzing the AST
315
+ */
316
+ function getPossibleMutations(filePath, lineNumber, sourceCode) {
317
+ try {
318
+ const babel = require('@babel/core');
319
+ const fs = require('fs');
320
+
321
+ // Read the full file content
322
+ const fullFileContent = fs.readFileSync(filePath, 'utf8');
323
+ const lines = fullFileContent.split('\n');
324
+ const targetLine = lines[lineNumber - 1];
325
+
326
+ if (!targetLine) return [];
327
+
328
+ const possibleMutations = [];
329
+
330
+ // Parse the entire file to get proper AST context
331
+ const ast = babel.parseSync(fullFileContent, {
332
+ filename: filePath,
333
+ parserOpts: {
334
+ sourceType: "module",
335
+ allowImportExportEverywhere: true,
336
+ plugins: ["typescript", "jsx"],
337
+ },
338
+ });
339
+
340
+ // Traverse the AST to find nodes on the target line
341
+ babel.traverse(ast, {
342
+ enter(path) {
343
+ const nodeLineNumber = path.node.loc?.start.line;
344
+ if (nodeLineNumber !== lineNumber) return;
345
+
346
+ // Check what types of mutations are possible based on the AST nodes
347
+ if (path.isBinaryExpression()) {
348
+ const operator = path.node.operator;
349
+ if (['+', '-', '*', '/', '%'].includes(operator)) {
350
+ possibleMutations.push('arithmetic');
351
+ }
352
+ if (['==', '!=', '===', '!==', '<', '>', '<=', '>='].includes(operator)) {
353
+ possibleMutations.push('comparison');
354
+ }
355
+ }
356
+
357
+ if (path.isLogicalExpression()) {
358
+ possibleMutations.push('logical');
359
+ }
360
+
361
+ if (path.isUnaryExpression() && path.node.operator === '!') {
362
+ possibleMutations.push('logical');
363
+ }
364
+
365
+ if (path.isUpdateExpression()) {
366
+ possibleMutations.push('increments');
367
+ }
368
+
369
+ if (path.isAssignmentExpression() && path.node.operator !== '=') {
370
+ possibleMutations.push('assignment');
371
+ }
372
+
373
+ // Only add conditional mutations for actual conditional statements
374
+ if (path.isIfStatement() || path.isWhileStatement() ||
375
+ (path.isForStatement() && path.node.test)) {
376
+ possibleMutations.push('conditional');
377
+ }
378
+
379
+ if (path.isReturnStatement()) {
380
+ possibleMutations.push('returns');
381
+ }
382
+
383
+ if (path.isLiteral() || path.isNumericLiteral() ||
384
+ path.isBooleanLiteral() || path.isStringLiteral()) {
385
+ possibleMutations.push('literals');
386
+ }
387
+ }
388
+ });
389
+
390
+ // Remove duplicates
391
+ return [...new Set(possibleMutations)];
392
+
393
+ } catch (error) {
394
+ // If AST analysis fails, return empty array to skip this line
395
+ return [];
396
+ }
397
+ }
398
+
399
+ module.exports = {
400
+ createMutationPlugin,
401
+ getPossibleMutations
402
+ };
@@ -0,0 +1,12 @@
1
+ export function add(a: number, b: number): number {
2
+ return a - b;
3
+ }
4
+ export function subtract(a: number, b: number): number {
5
+ if (!(!!(a < b))) {
6
+ return b - a;
7
+ }
8
+ return a - b;
9
+ }
10
+ export function multiply(a: number, b: number): number {
11
+ return a * b;
12
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Analyze Command
3
+ * Full workflow: test + mutation + report
4
+ */
5
+
6
+ const testCommand = require('./test');
7
+ const mutateCommand = require('./mutate');
8
+ const reportCommand = require('./report');
9
+ const { lineageDataExists } = require('../utils/data-loader');
10
+ const { section, success, error, info } = require('../utils/output-formatter');
11
+ const chalk = require('chalk');
12
+
13
+ async function analyzeCommand(options) {
14
+ try {
15
+ section('🚀 Full Analysis Workflow');
16
+
17
+ // Step 1: Run tests (unless skip-tests is specified)
18
+ if (!options.skipTests) {
19
+ info('Step 1: Running tests with lineage tracking...\n');
20
+
21
+ try {
22
+ await testCommand([], {
23
+ lineage: options.lineage !== false,
24
+ performance: options.performance !== false,
25
+ quality: options.quality !== false,
26
+ config: options.config,
27
+ quiet: false
28
+ });
29
+ } catch (err) {
30
+ // testCommand handles its own exit, this catch is for safety
31
+ error('Tests failed. Analysis stopped.');
32
+ process.exit(1);
33
+ }
34
+ } else {
35
+ // Verify lineage data exists
36
+ if (!lineageDataExists()) {
37
+ error('Lineage data not found. Remove --skip-tests or run jest-lineage test first.');
38
+ process.exit(1);
39
+ }
40
+ info('Step 1: Skipped (using existing lineage data)\n');
41
+ }
42
+
43
+ // Step 2: Run mutation testing (unless skip-mutation is specified)
44
+ if (!options.skipMutation) {
45
+ console.log(); // Add spacing
46
+ info('Step 2: Running mutation testing...\n');
47
+
48
+ try {
49
+ await mutateCommand({
50
+ data: '.jest-lineage-data.json',
51
+ threshold: options.threshold,
52
+ timeout: options.timeout || '5000',
53
+ verbose: false
54
+ });
55
+ } catch (err) {
56
+ // Don't fail the entire workflow if mutation testing fails
57
+ error('Mutation testing failed, but continuing with report generation...');
58
+ }
59
+ } else {
60
+ info('Step 2: Skipped mutation testing\n');
61
+ }
62
+
63
+ // Step 3: Generate report
64
+ console.log(); // Add spacing
65
+ info('Step 3: Generating HTML report...\n');
66
+
67
+ await reportCommand({
68
+ data: '.jest-lineage-data.json',
69
+ output: options.output,
70
+ open: options.open
71
+ });
72
+
73
+ // Success summary
74
+ console.log();
75
+ section('✨ Analysis Complete');
76
+ success('Full analysis workflow completed successfully!');
77
+ console.log(chalk.gray(`\nGenerated files:`));
78
+ console.log(chalk.gray(` • .jest-lineage-data.json (lineage data)`));
79
+ console.log(chalk.gray(` • ${options.output} (HTML report)`));
80
+
81
+ process.exit(0);
82
+ } catch (err) {
83
+ error(`Analysis workflow failed: ${err.message}`);
84
+ if (process.env.JEST_LINEAGE_DEBUG === 'true') {
85
+ console.error(err.stack);
86
+ }
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ module.exports = analyzeCommand;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Mutate Command
3
+ * Run mutation testing on existing lineage data
4
+ */
5
+
6
+ const MutationTester = require('../../MutationTester');
7
+ const { loadLineageData, processLineageDataForMutation } = require('../utils/data-loader');
8
+ const { loadFullConfig } = require('../utils/config-loader');
9
+ const { spinner, printMutationSummary, error, success, info, printLineageDataSummary } = require('../utils/output-formatter');
10
+ const chalk = require('chalk');
11
+
12
+ async function mutateCommand(options) {
13
+ let mutationTester = null;
14
+
15
+ try {
16
+ // Load configuration
17
+ const config = loadFullConfig(options);
18
+
19
+ // Load lineage data
20
+ info(`Loading lineage data from: ${chalk.yellow(options.data)}`);
21
+ const rawData = loadLineageData(options.data);
22
+
23
+ if (!options.verbose) {
24
+ printLineageDataSummary(rawData);
25
+ }
26
+
27
+ // Process data for mutation testing
28
+ const lineageData = processLineageDataForMutation(rawData);
29
+ const fileCount = Object.keys(lineageData).length;
30
+ const lineCount = Object.values(lineageData).reduce(
31
+ (sum, lines) => sum + Object.keys(lines).length,
32
+ 0
33
+ );
34
+
35
+ if (fileCount === 0 || lineCount === 0) {
36
+ error('No coverage data found in lineage file. Run tests first.');
37
+ process.exit(1);
38
+ }
39
+
40
+ info(`Processing ${chalk.cyan(fileCount)} files with ${chalk.cyan(lineCount)} covered lines\n`);
41
+
42
+ // Create mutation tester
43
+ mutationTester = new MutationTester(config);
44
+ mutationTester.setLineageData(lineageData);
45
+
46
+ // Run mutation testing
47
+ const spin = spinner('Running mutation testing...');
48
+ if (!options.verbose) {
49
+ spin.start();
50
+ }
51
+
52
+ const results = await mutationTester.runMutationTesting();
53
+
54
+ if (!options.verbose) {
55
+ spin.succeed('Mutation testing completed!');
56
+ }
57
+
58
+ // Print results
59
+ printMutationSummary(results);
60
+
61
+ // Check threshold
62
+ const threshold = parseInt(options.threshold) || 80;
63
+ if (results.mutationScore < threshold) {
64
+ console.log(chalk.yellow(`\n⚠️ Mutation score ${results.mutationScore.toFixed(1)}% is below threshold ${threshold}%`));
65
+ process.exit(1);
66
+ } else {
67
+ success(`Mutation score meets threshold (${threshold}%)`);
68
+ process.exit(0);
69
+ }
70
+ } catch (err) {
71
+ error(`Mutation testing failed: ${err.message}`);
72
+ if (options.verbose) {
73
+ console.error(err.stack);
74
+ }
75
+
76
+ // Cleanup on error
77
+ if (mutationTester) {
78
+ try {
79
+ await mutationTester.cleanup();
80
+ } catch (cleanupErr) {
81
+ // Ignore cleanup errors
82
+ }
83
+ }
84
+
85
+ process.exit(1);
86
+ }
87
+ }
88
+
89
+ module.exports = mutateCommand;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Query Command
3
+ * Query test coverage for specific files/lines
4
+ */
5
+
6
+ const { loadLineageData, processLineageDataForMutation } = require('../utils/data-loader');
7
+ const { section, error, formatPath } = require('../utils/output-formatter');
8
+ const Table = require('cli-table3');
9
+ const chalk = require('chalk');
10
+ const path = require('path');
11
+
12
+ async function queryCommand(file, line, options) {
13
+ try {
14
+ // Load lineage data
15
+ const rawData = loadLineageData(options.data);
16
+ const lineageData = processLineageDataForMutation(rawData);
17
+
18
+ // Normalize file path
19
+ const normalizedFile = path.normalize(file);
20
+
21
+ // Find matching files
22
+ const matchingFiles = Object.keys(lineageData).filter(f =>
23
+ f.includes(normalizedFile) || normalizedFile.includes(path.basename(f))
24
+ );
25
+
26
+ if (matchingFiles.length === 0) {
27
+ error(`No coverage data found for file: ${chalk.yellow(file)}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ // If multiple matches, show them
32
+ if (matchingFiles.length > 1) {
33
+ console.log(chalk.yellow(`Found multiple matching files:`));
34
+ matchingFiles.forEach(f => console.log(` - ${formatPath(f)}`));
35
+ console.log(chalk.yellow(`\nUsing first match: ${matchingFiles[0]}\n`));
36
+ }
37
+
38
+ const targetFile = matchingFiles[0];
39
+ const fileCoverage = lineageData[targetFile];
40
+
41
+ // If line specified, show coverage for that line
42
+ if (line) {
43
+ const lineNumber = line.toString();
44
+ if (!fileCoverage[lineNumber]) {
45
+ error(`No coverage data for line ${chalk.yellow(lineNumber)} in ${chalk.yellow(file)}`);
46
+ process.exit(1);
47
+ }
48
+
49
+ section(`📍 Coverage for ${formatPath(targetFile)}:${lineNumber}`);
50
+
51
+ const tests = fileCoverage[lineNumber];
52
+ const table = new Table({
53
+ head: [chalk.cyan('Test Name'), chalk.cyan('File'), chalk.cyan('Exec Count')],
54
+ colWidths: [50, 30, 12]
55
+ });
56
+
57
+ tests.forEach(test => {
58
+ table.push([
59
+ test.testName,
60
+ path.basename(test.testFile),
61
+ test.executionCount
62
+ ]);
63
+ });
64
+
65
+ console.log(table.toString());
66
+ console.log(chalk.gray(`\nTotal: ${tests.length} test(s) cover this line`));
67
+ } else {
68
+ // Show coverage for entire file
69
+ section(`📁 Coverage for ${formatPath(targetFile)}`);
70
+
71
+ const lines = Object.keys(fileCoverage).sort((a, b) => parseInt(a) - parseInt(b));
72
+ const totalTests = new Set(
73
+ lines.flatMap(lineNum => fileCoverage[lineNum].map(t => t.testName))
74
+ ).size;
75
+
76
+ console.log(chalk.gray(`Lines covered: ${lines.length}`));
77
+ console.log(chalk.gray(`Tests covering file: ${totalTests}\n`));
78
+
79
+ // Show sample of lines
80
+ const sampleSize = 10;
81
+ const sampled = lines.slice(0, sampleSize);
82
+
83
+ sampled.forEach(lineNum => {
84
+ const tests = fileCoverage[lineNum];
85
+ console.log(chalk.cyan(`Line ${lineNum}:`));
86
+ tests.slice(0, 3).forEach(test => {
87
+ console.log(` ${chalk.gray('•')} ${test.testName} ${chalk.gray(`(${path.basename(test.testFile)})`)}`);
88
+ });
89
+ if (tests.length > 3) {
90
+ console.log(chalk.gray(` ... and ${tests.length - 3} more test(s)`));
91
+ }
92
+ });
93
+
94
+ if (lines.length > sampleSize) {
95
+ console.log(chalk.gray(`\n... and ${lines.length - sampleSize} more lines`));
96
+ console.log(chalk.yellow(`\nTip: Specify a line number to see details: jest-lineage query ${file} <line>`));
97
+ }
98
+ }
99
+
100
+ process.exit(0);
101
+ } catch (err) {
102
+ error(`Query failed: ${err.message}`);
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ module.exports = queryCommand;