@mutineerjs/mutineer 0.9.0 → 0.11.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 (83) hide show
  1. package/README.md +52 -47
  2. package/dist/__tests__/index.spec.js +8 -0
  3. package/dist/bin/__tests__/mutineer.spec.js +7 -7
  4. package/dist/bin/mutineer.d.ts +1 -1
  5. package/dist/bin/mutineer.js +7 -4
  6. package/dist/core/__tests__/schemata.spec.js +62 -0
  7. package/dist/core/__tests__/sfc.spec.js +41 -1
  8. package/dist/core/schemata.js +15 -21
  9. package/dist/core/sfc.js +0 -4
  10. package/dist/core/variant-utils.js +0 -4
  11. package/dist/mutators/__tests__/utils.spec.js +65 -1
  12. package/dist/mutators/operator.js +13 -27
  13. package/dist/mutators/return-value.js +3 -7
  14. package/dist/mutators/utils.d.ts +2 -2
  15. package/dist/mutators/utils.js +59 -96
  16. package/dist/runner/__tests__/args.spec.js +8 -4
  17. package/dist/runner/__tests__/cache.spec.js +24 -0
  18. package/dist/runner/__tests__/changed.spec.js +75 -0
  19. package/dist/runner/__tests__/config.spec.js +50 -1
  20. package/dist/runner/__tests__/coverage-resolver.spec.js +88 -1
  21. package/dist/runner/__tests__/discover.spec.js +179 -0
  22. package/dist/runner/__tests__/orchestrator.spec.js +336 -11
  23. package/dist/runner/__tests__/pool-executor.spec.js +77 -0
  24. package/dist/runner/__tests__/ts-checker-worker.spec.d.ts +1 -0
  25. package/dist/runner/__tests__/ts-checker-worker.spec.js +66 -0
  26. package/dist/runner/__tests__/ts-checker.spec.js +89 -2
  27. package/dist/runner/args.d.ts +1 -1
  28. package/dist/runner/args.js +2 -2
  29. package/dist/runner/config.js +3 -4
  30. package/dist/runner/coverage-resolver.js +2 -1
  31. package/dist/runner/discover.js +2 -2
  32. package/dist/runner/jest/__tests__/adapter.spec.js +169 -0
  33. package/dist/runner/jest/__tests__/pool.spec.js +223 -1
  34. package/dist/runner/jest/adapter.js +3 -45
  35. package/dist/runner/jest/pool.js +4 -10
  36. package/dist/runner/jest/worker-runtime.js +2 -1
  37. package/dist/runner/orchestrator.js +8 -7
  38. package/dist/runner/pool-executor.js +7 -12
  39. package/dist/runner/shared/__tests__/strip-mutineer-args.spec.d.ts +1 -0
  40. package/dist/runner/shared/__tests__/strip-mutineer-args.spec.js +104 -0
  41. package/dist/runner/shared/__tests__/worker-script.spec.d.ts +1 -0
  42. package/dist/runner/shared/__tests__/worker-script.spec.js +32 -0
  43. package/dist/runner/shared/index.d.ts +4 -0
  44. package/dist/runner/shared/index.js +2 -0
  45. package/dist/runner/shared/pending-task.d.ts +9 -0
  46. package/dist/runner/shared/pending-task.js +1 -0
  47. package/dist/runner/shared/strip-mutineer-args.d.ts +11 -0
  48. package/dist/runner/shared/strip-mutineer-args.js +47 -0
  49. package/dist/runner/shared/worker-script.d.ts +5 -0
  50. package/dist/runner/shared/worker-script.js +12 -0
  51. package/dist/runner/ts-checker-worker.d.ts +10 -1
  52. package/dist/runner/ts-checker-worker.js +27 -25
  53. package/dist/runner/ts-checker.d.ts +6 -0
  54. package/dist/runner/ts-checker.js +1 -1
  55. package/dist/runner/vitest/__tests__/adapter.spec.js +294 -0
  56. package/dist/runner/vitest/__tests__/plugin.spec.js +28 -1
  57. package/dist/runner/vitest/__tests__/pool.spec.js +711 -0
  58. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +116 -1
  59. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +81 -0
  60. package/dist/runner/vitest/adapter.js +14 -46
  61. package/dist/runner/vitest/plugin.js +1 -7
  62. package/dist/runner/vitest/pool.js +6 -19
  63. package/dist/runner/vitest/redirect-loader.js +3 -1
  64. package/dist/runner/vitest/worker-runtime.js +16 -1
  65. package/dist/runner/vitest/worker.mjs +1 -0
  66. package/dist/types/config.d.ts +2 -2
  67. package/dist/types/mutant.d.ts +3 -0
  68. package/dist/utils/__tests__/PoolSpinner.spec.d.ts +1 -0
  69. package/dist/utils/__tests__/PoolSpinner.spec.js +15 -0
  70. package/dist/utils/__tests__/coverage.spec.js +89 -0
  71. package/dist/utils/__tests__/logger.spec.js +9 -0
  72. package/dist/utils/__tests__/progress.spec.js +38 -0
  73. package/dist/utils/__tests__/summary.spec.js +70 -31
  74. package/dist/utils/coverage.js +3 -4
  75. package/dist/utils/errors.d.ts +4 -0
  76. package/dist/utils/errors.js +6 -0
  77. package/dist/utils/summary.d.ts +2 -3
  78. package/dist/utils/summary.js +5 -6
  79. package/package.json +1 -1
  80. package/dist/utils/CompileErrors.d.ts +0 -7
  81. package/dist/utils/CompileErrors.js +0 -24
  82. package/dist/utils/__tests__/CompileErrors.spec.js +0 -96
  83. /package/dist/{utils/__tests__/CompileErrors.spec.d.ts → __tests__/index.spec.d.ts} +0 -0
@@ -123,6 +123,42 @@ describe('summary', () => {
123
123
  expect(lines.some((l) => l.includes('foo.spec.ts'))).toBe(true);
124
124
  logSpy.mockRestore();
125
125
  });
126
+ it('handles printSummary with no cache argument when total > 0', () => {
127
+ const summary = {
128
+ total: 1,
129
+ killed: 1,
130
+ escaped: 0,
131
+ skipped: 0,
132
+ timeouts: 0,
133
+ compileErrors: 0,
134
+ evaluated: 1,
135
+ killRate: 100,
136
+ };
137
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
138
+ // No cache passed: allEntries=[], maxPathLen=25 (|| 25), maxMutatorLen=10 (|| 10)
139
+ printSummary(summary);
140
+ expect(logSpy).toHaveBeenCalled();
141
+ logSpy.mockRestore();
142
+ });
143
+ it('buildJsonReport includes passingTests when present', () => {
144
+ const cache = {
145
+ a: makeEntry({
146
+ status: 'escaped',
147
+ file: '/tmp/a.ts',
148
+ mutator: 'flip',
149
+ passingTests: ['Suite > test one'],
150
+ }),
151
+ };
152
+ const summary = computeSummary(cache);
153
+ const report = buildJsonReport(summary, cache);
154
+ expect(report.mutants[0].passingTests).toEqual(['Suite > test one']);
155
+ });
156
+ it('buildJsonReport omits passingTests when absent', () => {
157
+ const cache = { a: makeEntry({ status: 'escaped' }) };
158
+ const summary = computeSummary(cache);
159
+ const report = buildJsonReport(summary, cache);
160
+ expect('passingTests' in report.mutants[0]).toBe(false);
161
+ });
126
162
  it('does not print covering tests when array is absent', () => {
127
163
  const cache = {
128
164
  a: makeEntry({ status: 'escaped' }),
@@ -176,57 +212,60 @@ describe('summary', () => {
176
212
  expect('originalSnippet' in report.mutants[0]).toBe(false);
177
213
  expect('coveringTests' in report.mutants[0]).toBe(false);
178
214
  });
179
- it('prints compile error mutants section by default', () => {
180
- const cache = {
181
- a: makeEntry({
182
- status: 'compile-error',
183
- file: '/tmp/a.ts',
184
- mutator: 'returnToNull',
185
- }),
186
- };
215
+ it('prints report hint line', () => {
216
+ const cache = { a: makeEntry({ status: 'killed' }) };
187
217
  const summary = computeSummary(cache);
188
218
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
189
219
  printSummary(summary, cache);
190
220
  const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
191
- expect(lines.some((l) => l.includes('Compile Error Mutants'))).toBe(true);
221
+ expect(lines.some((l) => l.includes('Run with --report json to see full mutation details.'))).toBe(true);
222
+ logSpy.mockRestore();
223
+ });
224
+ it('summarise returns summary and prints', () => {
225
+ const cache = { a: makeEntry({ status: 'killed' }) };
226
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
227
+ const s = summarise(cache);
228
+ expect(s.total).toBe(1);
229
+ expect(logSpy).toHaveBeenCalled();
192
230
  logSpy.mockRestore();
193
231
  });
194
- it('skips compile error section when skipCompileErrors is true', () => {
232
+ it('counts compile-error status in compileErrors field', () => {
233
+ const cache = { a: makeEntry({ status: 'compile-error' }) };
234
+ const s = computeSummary(cache);
235
+ expect(s.compileErrors).toBe(1);
236
+ expect(s.killed).toBe(0);
237
+ });
238
+ it('categorizes compile-error entries without throwing in printSummary', () => {
195
239
  const cache = {
196
- a: makeEntry({
197
- status: 'compile-error',
198
- file: '/tmp/a.ts',
199
- mutator: 'returnToNull',
200
- }),
240
+ a: makeEntry({ status: 'compile-error', file: '/tmp/a.ts' }),
201
241
  };
202
242
  const summary = computeSummary(cache);
203
243
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
204
- printSummary(summary, cache, undefined, { skipCompileErrors: true });
244
+ printSummary(summary, cache);
245
+ expect(summary.compileErrors).toBe(1);
246
+ logSpy.mockRestore();
247
+ });
248
+ it('formats duration in minutes when duration >= 60s', () => {
249
+ const cache = { a: makeEntry({ status: 'killed' }) };
250
+ const summary = computeSummary(cache);
251
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
252
+ printSummary(summary, cache, 90000); // 90 seconds = 1m 30.0s
205
253
  const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
206
- expect(lines.some((l) => l.includes('Compile Error Mutants'))).toBe(false);
254
+ expect(lines.some((l) => l.includes('1m 30.0s'))).toBe(true);
207
255
  logSpy.mockRestore();
208
256
  });
209
- it('shows compile error section when skipCompileErrors is false', () => {
257
+ it('prints +N more when escaped mutant has more than 2 covering tests', () => {
210
258
  const cache = {
211
259
  a: makeEntry({
212
- status: 'compile-error',
213
- file: '/tmp/a.ts',
214
- mutator: 'returnToNull',
260
+ status: 'escaped',
261
+ coveringTests: ['/t1.spec.ts', '/t2.spec.ts', '/t3.spec.ts'],
215
262
  }),
216
263
  };
217
264
  const summary = computeSummary(cache);
218
265
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
219
- printSummary(summary, cache, undefined, { skipCompileErrors: false });
266
+ printSummary(summary, cache);
220
267
  const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
221
- expect(lines.some((l) => l.includes('Compile Error Mutants'))).toBe(true);
222
- logSpy.mockRestore();
223
- });
224
- it('summarise returns summary and prints', () => {
225
- const cache = { a: makeEntry({ status: 'killed' }) };
226
- const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
227
- const s = summarise(cache);
228
- expect(s.total).toBe(1);
229
- expect(logSpy).toHaveBeenCalled();
268
+ expect(lines.some((l) => l.includes('+1 more'))).toBe(true);
230
269
  logSpy.mockRestore();
231
270
  });
232
271
  });
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { toErrorMessage } from './errors.js';
3
4
  /**
4
5
  * Load and parse Istanbul-format coverage JSON file.
5
6
  * Supports both coverage-final.json (from Istanbul) and Vitest's coverage output.
@@ -17,16 +18,14 @@ export async function loadCoverageData(coverageFile, cwd) {
17
18
  raw = await fs.readFile(absPath, 'utf8');
18
19
  }
19
20
  catch (err) {
20
- const msg = err instanceof Error ? err.message : String(err);
21
- throw new Error(`Failed to read coverage file "${absPath}": ${msg}`);
21
+ throw new Error(`Failed to read coverage file "${absPath}": ${toErrorMessage(err)}`);
22
22
  }
23
23
  let data;
24
24
  try {
25
25
  data = JSON.parse(raw);
26
26
  }
27
27
  catch (err) {
28
- const msg = err instanceof Error ? err.message : String(err);
29
- throw new Error(`Failed to parse coverage file "${absPath}" as JSON: ${msg}`);
28
+ throw new Error(`Failed to parse coverage file "${absPath}" as JSON: ${toErrorMessage(err)}`);
30
29
  }
31
30
  const coveredLines = new Map();
32
31
  for (const [filePath, fileCoverage] of Object.entries(data)) {
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Error message extraction utility.
3
+ */
4
+ export declare function toErrorMessage(err: unknown): string;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Error message extraction utility.
3
+ */
4
+ export function toErrorMessage(err) {
5
+ return err instanceof Error ? err.message : String(err);
6
+ }
@@ -10,9 +10,7 @@ export interface Summary {
10
10
  readonly killRate: number;
11
11
  }
12
12
  export declare function computeSummary(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
13
- export declare function printSummary(summary: Summary, cache?: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number, opts?: {
14
- skipCompileErrors?: boolean;
15
- }): void;
13
+ export declare function printSummary(summary: Summary, cache?: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): void;
16
14
  export interface JsonMutant {
17
15
  readonly file: string;
18
16
  readonly line: number;
@@ -22,6 +20,7 @@ export interface JsonMutant {
22
20
  readonly originalSnippet?: string;
23
21
  readonly mutatedSnippet?: string;
24
22
  readonly coveringTests?: readonly string[];
23
+ readonly passingTests?: readonly string[];
25
24
  }
26
25
  export interface JsonReport {
27
26
  readonly schemaVersion: 1;
@@ -44,7 +44,7 @@ function formatDuration(ms) {
44
44
  const remainingSeconds = seconds % 60;
45
45
  return `${minutes}m ${remainingSeconds.toFixed(1)}s`;
46
46
  }
47
- export function printSummary(summary, cache, durationMs, opts) {
47
+ export function printSummary(summary, cache, durationMs) {
48
48
  console.log('\n' + chalk.dim(SEPARATOR));
49
49
  console.log(chalk.bold(' Mutineer Test Suite Summary'));
50
50
  console.log(chalk.dim(SEPARATOR));
@@ -113,11 +113,6 @@ export function printSummary(summary, cache, durationMs, opts) {
113
113
  }
114
114
  }
115
115
  }
116
- if (entriesByStatus.compileErrors.length && !opts?.skipCompileErrors) {
117
- console.log('\n' + chalk.dim('Compile Error Mutants (type-filtered):'));
118
- for (const entry of entriesByStatus.compileErrors)
119
- console.log(' ' + formatRow(entry));
120
- }
121
116
  if (entriesByStatus.timeouts.length) {
122
117
  console.log('\n' + chalk.yellow.bold('Timed Out Mutants:'));
123
118
  for (const entry of entriesByStatus.timeouts)
@@ -148,6 +143,7 @@ export function printSummary(summary, cache, durationMs, opts) {
148
143
  if (durationMs !== undefined) {
149
144
  console.log(`Duration: ${chalk.cyan(formatDuration(durationMs))}`);
150
145
  }
146
+ console.log(chalk.dim('Run with --report json to see full mutation details.'));
151
147
  console.log(chalk.dim(SEPARATOR) + '\n');
152
148
  }
153
149
  export function buildJsonReport(summary, cache, durationMs) {
@@ -166,6 +162,9 @@ export function buildJsonReport(summary, cache, durationMs) {
166
162
  ...(entry.coveringTests !== undefined && {
167
163
  coveringTests: entry.coveringTests,
168
164
  }),
165
+ ...(entry.passingTests !== undefined && {
166
+ passingTests: entry.passingTests,
167
+ }),
169
168
  }));
170
169
  return {
171
170
  schemaVersion: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutineerjs/mutineer",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "A fast, targeted mutation testing framework for JavaScript and TypeScript",
5
5
  "type": "module",
6
6
  "private": false,
@@ -1,7 +0,0 @@
1
- import type { MutantCacheEntry } from '../types/mutant.js';
2
- interface Props {
3
- entries: MutantCacheEntry[];
4
- cwd: string;
5
- }
6
- export declare function CompileErrors({ entries, cwd }: Props): import("react/jsx-runtime").JSX.Element;
7
- export {};
@@ -1,24 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import path from 'node:path';
3
- import { Box, Text, useInput, useApp } from 'ink';
4
- import { useState, useEffect } from 'react';
5
- export function CompileErrors({ entries, cwd }) {
6
- const { exit } = useApp();
7
- const [expanded, setExpanded] = useState(false);
8
- useEffect(() => {
9
- if (expanded)
10
- exit();
11
- }, [expanded, exit]);
12
- useInput((input, key) => {
13
- if (input === 'e') {
14
- setExpanded(true);
15
- }
16
- else if (key.return || input === 'q') {
17
- exit();
18
- }
19
- });
20
- if (expanded) {
21
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Compile Error Mutants (type-filtered):" }), entries.map((entry, i) => (_jsxs(Text, { dimColor: true, children: [' \u2022 ', path.relative(cwd, entry.file), "@", entry.line, ",", entry.col, ' ', entry.mutator] }, i)))] }));
22
- }
23
- return (_jsxs(Box, { gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["Compile Error Mutants (type-filtered): ", entries.length] }), _jsx(Text, { dimColor: true, children: "e expand return skip" })] }));
24
- }
@@ -1,96 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- const mockExit = vi.fn();
3
- const mockSetExpanded = vi.fn();
4
- let inputHandler;
5
- let effectCallback;
6
- vi.mock('ink', () => ({
7
- Box: ({ children }) => children,
8
- Text: ({ children }) => children,
9
- useInput: vi.fn((fn) => {
10
- inputHandler = fn;
11
- }),
12
- useApp: () => ({ exit: mockExit }),
13
- }));
14
- vi.mock('react', async (importOriginal) => {
15
- const actual = await importOriginal();
16
- return {
17
- ...actual,
18
- useState: vi.fn((init) => [init, mockSetExpanded]),
19
- useEffect: vi.fn((fn) => {
20
- effectCallback = fn;
21
- }),
22
- };
23
- });
24
- import { CompileErrors } from '../CompileErrors.js';
25
- import { useState } from 'react';
26
- const entries = [
27
- {
28
- status: 'compile-error',
29
- file: '/cwd/src/foo.ts',
30
- line: 10,
31
- col: 5,
32
- mutator: 'returnToNull',
33
- },
34
- {
35
- status: 'compile-error',
36
- file: '/cwd/src/bar.ts',
37
- line: 20,
38
- col: 3,
39
- mutator: 'returnFlipBool',
40
- },
41
- ];
42
- describe('CompileErrors', () => {
43
- beforeEach(() => {
44
- mockExit.mockClear();
45
- mockSetExpanded.mockClear();
46
- inputHandler = undefined;
47
- effectCallback = undefined;
48
- vi.mocked(useState).mockImplementation(((init) => [
49
- init,
50
- mockSetExpanded,
51
- ]));
52
- });
53
- it('registers a useInput handler on render', () => {
54
- CompileErrors({ entries, cwd: '/cwd' });
55
- expect(inputHandler).toBeDefined();
56
- });
57
- it('calls setExpanded(true) when "e" is pressed', () => {
58
- CompileErrors({ entries, cwd: '/cwd' });
59
- inputHandler('e', { return: false });
60
- expect(mockSetExpanded).toHaveBeenCalledWith(true);
61
- });
62
- it('calls exit() when return is pressed', () => {
63
- CompileErrors({ entries, cwd: '/cwd' });
64
- inputHandler('', { return: true });
65
- expect(mockExit).toHaveBeenCalled();
66
- });
67
- it('calls exit() when "q" is pressed', () => {
68
- CompileErrors({ entries, cwd: '/cwd' });
69
- inputHandler('q', { return: false });
70
- expect(mockExit).toHaveBeenCalled();
71
- });
72
- it('does not call exit() or setExpanded for other keys', () => {
73
- CompileErrors({ entries, cwd: '/cwd' });
74
- inputHandler('x', { return: false });
75
- expect(mockExit).not.toHaveBeenCalled();
76
- expect(mockSetExpanded).not.toHaveBeenCalled();
77
- });
78
- it('registers a useEffect handler on render', () => {
79
- CompileErrors({ entries, cwd: '/cwd' });
80
- expect(effectCallback).toBeDefined();
81
- });
82
- it('useEffect calls exit() when expanded is true', () => {
83
- vi.mocked(useState).mockReturnValueOnce([
84
- true,
85
- mockSetExpanded,
86
- ]);
87
- CompileErrors({ entries, cwd: '/cwd' });
88
- effectCallback();
89
- expect(mockExit).toHaveBeenCalled();
90
- });
91
- it('useEffect does not call exit() when expanded is false', () => {
92
- CompileErrors({ entries, cwd: '/cwd' });
93
- effectCallback();
94
- expect(mockExit).not.toHaveBeenCalled();
95
- });
96
- });