@mutineerjs/mutineer 0.6.0 → 0.8.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 (69) hide show
  1. package/README.md +32 -15
  2. package/dist/bin/__tests__/mutineer.spec.js +67 -2
  3. package/dist/bin/mutineer.d.ts +6 -1
  4. package/dist/bin/mutineer.js +58 -2
  5. package/dist/core/__tests__/schemata.spec.d.ts +1 -0
  6. package/dist/core/__tests__/schemata.spec.js +165 -0
  7. package/dist/core/schemata.d.ts +22 -0
  8. package/dist/core/schemata.js +236 -0
  9. package/dist/mutators/__tests__/operator.spec.js +97 -1
  10. package/dist/mutators/__tests__/registry.spec.js +8 -0
  11. package/dist/mutators/operator.d.ts +8 -0
  12. package/dist/mutators/operator.js +58 -1
  13. package/dist/mutators/registry.js +9 -1
  14. package/dist/mutators/utils.d.ts +2 -0
  15. package/dist/mutators/utils.js +58 -1
  16. package/dist/runner/__tests__/args.spec.js +89 -1
  17. package/dist/runner/__tests__/cache.spec.js +65 -8
  18. package/dist/runner/__tests__/cleanup.spec.js +37 -0
  19. package/dist/runner/__tests__/coverage-resolver.spec.js +5 -0
  20. package/dist/runner/__tests__/discover.spec.js +128 -0
  21. package/dist/runner/__tests__/orchestrator.spec.js +332 -2
  22. package/dist/runner/__tests__/pool-executor.spec.js +107 -1
  23. package/dist/runner/__tests__/ts-checker.spec.d.ts +1 -0
  24. package/dist/runner/__tests__/ts-checker.spec.js +115 -0
  25. package/dist/runner/args.d.ts +18 -0
  26. package/dist/runner/args.js +37 -0
  27. package/dist/runner/cache.d.ts +19 -3
  28. package/dist/runner/cache.js +14 -7
  29. package/dist/runner/cleanup.d.ts +3 -1
  30. package/dist/runner/cleanup.js +19 -2
  31. package/dist/runner/coverage-resolver.js +1 -1
  32. package/dist/runner/discover.d.ts +1 -1
  33. package/dist/runner/discover.js +30 -20
  34. package/dist/runner/orchestrator.d.ts +1 -0
  35. package/dist/runner/orchestrator.js +114 -19
  36. package/dist/runner/pool-executor.d.ts +7 -0
  37. package/dist/runner/pool-executor.js +29 -7
  38. package/dist/runner/shared/__tests__/mutant-paths.spec.js +30 -1
  39. package/dist/runner/shared/index.d.ts +1 -1
  40. package/dist/runner/shared/index.js +1 -1
  41. package/dist/runner/shared/mutant-paths.d.ts +17 -0
  42. package/dist/runner/shared/mutant-paths.js +24 -0
  43. package/dist/runner/ts-checker-worker.d.ts +5 -0
  44. package/dist/runner/ts-checker-worker.js +66 -0
  45. package/dist/runner/ts-checker.d.ts +36 -0
  46. package/dist/runner/ts-checker.js +210 -0
  47. package/dist/runner/types.d.ts +2 -0
  48. package/dist/runner/vitest/__tests__/adapter.spec.js +41 -0
  49. package/dist/runner/vitest/__tests__/plugin.spec.js +151 -0
  50. package/dist/runner/vitest/__tests__/pool.spec.js +85 -0
  51. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +126 -0
  52. package/dist/runner/vitest/adapter.js +14 -9
  53. package/dist/runner/vitest/plugin.d.ts +3 -0
  54. package/dist/runner/vitest/plugin.js +49 -11
  55. package/dist/runner/vitest/pool.d.ts +4 -1
  56. package/dist/runner/vitest/pool.js +25 -4
  57. package/dist/runner/vitest/worker-runtime.d.ts +1 -0
  58. package/dist/runner/vitest/worker-runtime.js +57 -18
  59. package/dist/runner/vitest/worker.mjs +10 -0
  60. package/dist/types/config.d.ts +16 -0
  61. package/dist/types/mutant.d.ts +5 -2
  62. package/dist/utils/CompileErrors.d.ts +7 -0
  63. package/dist/utils/CompileErrors.js +24 -0
  64. package/dist/utils/__tests__/CompileErrors.spec.d.ts +1 -0
  65. package/dist/utils/__tests__/CompileErrors.spec.js +96 -0
  66. package/dist/utils/__tests__/summary.spec.js +126 -2
  67. package/dist/utils/summary.d.ts +23 -1
  68. package/dist/utils/summary.js +63 -3
  69. package/package.json +2 -1
@@ -40,4 +40,20 @@ export interface MutineerConfig {
40
40
  readonly perTestCoverage?: boolean;
41
41
  /** Per-mutant test timeout in milliseconds (default: 30000) */
42
42
  readonly timeout?: number;
43
+ /** Output report format: 'text' (default) or 'json' (writes mutineer-report.json) */
44
+ readonly report?: 'text' | 'json';
45
+ /**
46
+ * Enable TypeScript type checking to pre-filter mutants that produce compile errors.
47
+ * true = enable (requires tsconfig.json), false = disable,
48
+ * object = enable with optional custom tsconfig path.
49
+ * Defaults to auto-detect (enabled if tsconfig.json found in cwd).
50
+ */
51
+ readonly typescript?: boolean | {
52
+ readonly tsconfig?: string;
53
+ };
54
+ /**
55
+ * Filter mutations to a specific Vitest workspace project.
56
+ * Requires a vitest.config.ts with test.projects configured.
57
+ */
58
+ readonly vitestProject?: string | readonly string[];
43
59
  }
@@ -4,7 +4,7 @@
4
4
  * Centralises the shapes used across the runner and adapters so we
5
5
  * don't duplicate unions or object shapes in multiple modules.
6
6
  */
7
- export type MutantStatus = 'killed' | 'escaped' | 'skipped' | 'error' | 'timeout';
7
+ export type MutantStatus = 'killed' | 'escaped' | 'skipped' | 'error' | 'timeout' | 'compile-error';
8
8
  export type MutantRunStatus = MutantStatus;
9
9
  export interface MutantLocation {
10
10
  readonly file: string;
@@ -17,7 +17,10 @@ export interface MutantDescriptor extends MutantLocation {
17
17
  readonly code: string;
18
18
  }
19
19
  /** Payload passed to workers/pools for execution. */
20
- export type MutantPayload = MutantDescriptor;
20
+ export interface MutantPayload extends MutantDescriptor {
21
+ /** When true, this mutant must use the legacy redirect path instead of the schema path. */
22
+ readonly isFallback?: boolean;
23
+ }
21
24
  /** Variant with attached test files. */
22
25
  export interface Variant extends MutantDescriptor {
23
26
  readonly tests: readonly string[];
@@ -0,0 +1,7 @@
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 {};
@@ -0,0 +1,24 @@
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
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
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { computeSummary, printSummary, summarise } from '../summary.js';
2
+ import { computeSummary, printSummary, summarise, buildJsonReport, } from '../summary.js';
3
3
  /** Strip ANSI escape codes for clean text assertions */
4
4
  const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
5
5
  function makeEntry(overrides) {
@@ -18,13 +18,16 @@ describe('summary', () => {
18
18
  a: makeEntry({ status: 'killed' }),
19
19
  b: makeEntry({ status: 'escaped' }),
20
20
  c: makeEntry({ status: 'skipped' }),
21
+ d: makeEntry({ status: 'timeout' }),
21
22
  };
22
23
  const s = computeSummary(cache);
23
24
  expect(s).toEqual({
24
- total: 3,
25
+ total: 4,
25
26
  killed: 1,
26
27
  escaped: 1,
27
28
  skipped: 1,
29
+ timeouts: 1,
30
+ compileErrors: 0,
28
31
  evaluated: 2,
29
32
  killRate: 50,
30
33
  });
@@ -43,6 +46,40 @@ describe('summary', () => {
43
46
  expect(lines.some((l) => l.includes('Duration: 1.50s'))).toBe(true);
44
47
  logSpy.mockRestore();
45
48
  });
49
+ it('prints Timed Out Mutants section when timeouts exist', () => {
50
+ const cache = {
51
+ a: makeEntry({ status: 'timeout', file: '/tmp/a.ts', mutator: 'flip' }),
52
+ };
53
+ const summary = computeSummary(cache);
54
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
55
+ printSummary(summary, cache);
56
+ const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
57
+ expect(lines.some((l) => l.includes('Timed Out Mutants'))).toBe(true);
58
+ logSpy.mockRestore();
59
+ });
60
+ it('shows Timeouts count in stat line when timeouts > 0', () => {
61
+ const cache = {
62
+ a: makeEntry({ status: 'timeout', file: '/tmp/a.ts' }),
63
+ b: makeEntry({ status: 'killed', file: '/tmp/b.ts' }),
64
+ };
65
+ const summary = computeSummary(cache);
66
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
67
+ printSummary(summary, cache);
68
+ const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
69
+ expect(lines.some((l) => l.includes('Timeouts: 1'))).toBe(true);
70
+ logSpy.mockRestore();
71
+ });
72
+ it('shows Timeouts: 0 in stat line when timeouts is zero', () => {
73
+ const cache = {
74
+ a: makeEntry({ status: 'killed', file: '/tmp/a.ts' }),
75
+ };
76
+ const summary = computeSummary(cache);
77
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
78
+ printSummary(summary, cache);
79
+ const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
80
+ expect(lines.some((l) => l.includes('Timeouts: 0'))).toBe(true);
81
+ logSpy.mockRestore();
82
+ });
46
83
  it('prints diff lines for escaped mutant with snippets', () => {
47
84
  const cache = {
48
85
  a: makeEntry({
@@ -97,6 +134,93 @@ describe('summary', () => {
97
134
  expect(lines.some((l) => l.includes('↳'))).toBe(false);
98
135
  logSpy.mockRestore();
99
136
  });
137
+ it('buildJsonReport includes schemaVersion, timestamp, summary, and mutants', () => {
138
+ const cache = {
139
+ a: makeEntry({ status: 'killed', file: '/tmp/a.ts', mutator: 'flip' }),
140
+ b: makeEntry({ status: 'escaped', file: '/tmp/b.ts', mutator: 'wrap' }),
141
+ };
142
+ const summary = computeSummary(cache);
143
+ const report = buildJsonReport(summary, cache, 1000);
144
+ expect(report.schemaVersion).toBe(1);
145
+ expect(typeof report.timestamp).toBe('string');
146
+ expect(report.durationMs).toBe(1000);
147
+ expect(report.summary).toEqual(summary);
148
+ expect(report.mutants).toHaveLength(2);
149
+ });
150
+ it('buildJsonReport mutant entries have required fields', () => {
151
+ const cache = {
152
+ a: makeEntry({
153
+ status: 'escaped',
154
+ file: '/tmp/a.ts',
155
+ mutator: 'flip',
156
+ originalSnippet: 'a === b',
157
+ mutatedSnippet: 'a !== b',
158
+ coveringTests: ['/tmp/a.spec.ts'],
159
+ }),
160
+ };
161
+ const summary = computeSummary(cache);
162
+ const report = buildJsonReport(summary, cache);
163
+ const mutant = report.mutants[0];
164
+ expect(mutant.file).toBe('/tmp/a.ts');
165
+ expect(mutant.status).toBe('escaped');
166
+ expect(mutant.mutator).toBe('flip');
167
+ expect(mutant.originalSnippet).toBe('a === b');
168
+ expect(mutant.mutatedSnippet).toBe('a !== b');
169
+ expect(mutant.coveringTests).toEqual(['/tmp/a.spec.ts']);
170
+ });
171
+ it('buildJsonReport omits optional fields when absent', () => {
172
+ const cache = { a: makeEntry({ status: 'killed' }) };
173
+ const summary = computeSummary(cache);
174
+ const report = buildJsonReport(summary, cache);
175
+ expect('durationMs' in report).toBe(false);
176
+ expect('originalSnippet' in report.mutants[0]).toBe(false);
177
+ expect('coveringTests' in report.mutants[0]).toBe(false);
178
+ });
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
+ };
187
+ const summary = computeSummary(cache);
188
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
189
+ printSummary(summary, cache);
190
+ const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
191
+ expect(lines.some((l) => l.includes('Compile Error Mutants'))).toBe(true);
192
+ logSpy.mockRestore();
193
+ });
194
+ it('skips compile error section when skipCompileErrors is true', () => {
195
+ const cache = {
196
+ a: makeEntry({
197
+ status: 'compile-error',
198
+ file: '/tmp/a.ts',
199
+ mutator: 'returnToNull',
200
+ }),
201
+ };
202
+ const summary = computeSummary(cache);
203
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
204
+ printSummary(summary, cache, undefined, { skipCompileErrors: true });
205
+ const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
206
+ expect(lines.some((l) => l.includes('Compile Error Mutants'))).toBe(false);
207
+ logSpy.mockRestore();
208
+ });
209
+ it('shows compile error section when skipCompileErrors is false', () => {
210
+ const cache = {
211
+ a: makeEntry({
212
+ status: 'compile-error',
213
+ file: '/tmp/a.ts',
214
+ mutator: 'returnToNull',
215
+ }),
216
+ };
217
+ const summary = computeSummary(cache);
218
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
219
+ printSummary(summary, cache, undefined, { skipCompileErrors: false });
220
+ 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
+ });
100
224
  it('summarise returns summary and prints', () => {
101
225
  const cache = { a: makeEntry({ status: 'killed' }) };
102
226
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
@@ -4,9 +4,31 @@ export interface Summary {
4
4
  readonly killed: number;
5
5
  readonly escaped: number;
6
6
  readonly skipped: number;
7
+ readonly timeouts: number;
8
+ readonly compileErrors: number;
7
9
  readonly evaluated: number;
8
10
  readonly killRate: number;
9
11
  }
10
12
  export declare function computeSummary(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
11
- export declare function printSummary(summary: Summary, cache?: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): void;
13
+ export declare function printSummary(summary: Summary, cache?: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number, opts?: {
14
+ skipCompileErrors?: boolean;
15
+ }): void;
16
+ export interface JsonMutant {
17
+ readonly file: string;
18
+ readonly line: number;
19
+ readonly col: number;
20
+ readonly mutator: string;
21
+ readonly status: string;
22
+ readonly originalSnippet?: string;
23
+ readonly mutatedSnippet?: string;
24
+ readonly coveringTests?: readonly string[];
25
+ }
26
+ export interface JsonReport {
27
+ readonly schemaVersion: 1;
28
+ readonly timestamp: string;
29
+ readonly durationMs?: number;
30
+ readonly summary: Summary;
31
+ readonly mutants: JsonMutant[];
32
+ }
33
+ export declare function buildJsonReport(summary: Summary, cache: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): JsonReport;
12
34
  export declare function summarise(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
@@ -6,18 +6,33 @@ export function computeSummary(cache) {
6
6
  let killed = 0;
7
7
  let escaped = 0;
8
8
  let skipped = 0;
9
+ let timeouts = 0;
10
+ let compileErrors = 0;
9
11
  for (const entry of allEntries) {
10
12
  if (entry.status === 'killed')
11
13
  killed++;
12
14
  else if (entry.status === 'escaped')
13
15
  escaped++;
16
+ else if (entry.status === 'compile-error')
17
+ compileErrors++;
18
+ else if (entry.status === 'timeout')
19
+ timeouts++;
14
20
  else
15
21
  skipped++;
16
22
  }
17
23
  const evaluated = killed + escaped;
18
24
  const total = allEntries.length;
19
25
  const killRate = evaluated === 0 ? 0 : (killed / evaluated) * 100;
20
- return { total, killed, escaped, skipped, evaluated, killRate };
26
+ return {
27
+ total,
28
+ killed,
29
+ escaped,
30
+ skipped,
31
+ timeouts,
32
+ compileErrors,
33
+ evaluated,
34
+ killRate,
35
+ };
21
36
  }
22
37
  function formatDuration(ms) {
23
38
  if (ms < 1000)
@@ -29,7 +44,7 @@ function formatDuration(ms) {
29
44
  const remainingSeconds = seconds % 60;
30
45
  return `${minutes}m ${remainingSeconds.toFixed(1)}s`;
31
46
  }
32
- export function printSummary(summary, cache, durationMs) {
47
+ export function printSummary(summary, cache, durationMs, opts) {
33
48
  console.log('\n' + chalk.dim(SEPARATOR));
34
49
  console.log(chalk.bold(' Mutineer Test Suite Summary'));
35
50
  console.log(chalk.dim(SEPARATOR));
@@ -57,6 +72,8 @@ export function printSummary(summary, cache, durationMs) {
57
72
  const entriesByStatus = {
58
73
  killed: [],
59
74
  escaped: [],
75
+ compileErrors: [],
76
+ timeouts: [],
60
77
  skipped: [],
61
78
  };
62
79
  for (const entry of allEntries) {
@@ -64,6 +81,10 @@ export function printSummary(summary, cache, durationMs) {
64
81
  entriesByStatus.killed.push(entry);
65
82
  else if (entry.status === 'escaped')
66
83
  entriesByStatus.escaped.push(entry);
84
+ else if (entry.status === 'compile-error')
85
+ entriesByStatus.compileErrors.push(entry);
86
+ else if (entry.status === 'timeout')
87
+ entriesByStatus.timeouts.push(entry);
67
88
  else
68
89
  entriesByStatus.skipped.push(entry);
69
90
  }
@@ -92,13 +113,27 @@ export function printSummary(summary, cache, durationMs) {
92
113
  }
93
114
  }
94
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
+ if (entriesByStatus.timeouts.length) {
122
+ console.log('\n' + chalk.yellow.bold('Timed Out Mutants:'));
123
+ for (const entry of entriesByStatus.timeouts)
124
+ console.log(' ' + formatRow(entry));
125
+ }
95
126
  if (entriesByStatus.skipped.length) {
96
127
  console.log('\n' + chalk.dim('Skipped Mutants:'));
97
128
  for (const entry of entriesByStatus.skipped)
98
129
  console.log(' ' + formatRow(entry));
99
130
  }
100
131
  console.log('\n' + chalk.dim(SEPARATOR));
101
- console.log(`Total: ${summary.total} \u2014 ${chalk.green(`Killed: ${summary.killed}`)}, ${chalk.red(`Escaped: ${summary.escaped}`)}, ${chalk.dim(`Skipped: ${summary.skipped}`)}`);
132
+ const compileErrorStr = summary.compileErrors > 0
133
+ ? `, ${chalk.dim(`Compile Errors: ${summary.compileErrors}`)}`
134
+ : '';
135
+ const timeoutStr = `, ${chalk.yellow(`Timeouts: ${summary.timeouts}`)}`;
136
+ console.log(`Total: ${summary.total} \u2014 ${chalk.green(`Killed: ${summary.killed}`)}, ${chalk.red(`Escaped: ${summary.escaped}`)}, ${chalk.dim(`Skipped: ${summary.skipped}`)}${timeoutStr}${compileErrorStr}`);
102
137
  if (summary.evaluated === 0) {
103
138
  console.log(`Kill rate: ${chalk.dim('0.00% (no mutants executed)')}`);
104
139
  }
@@ -115,6 +150,31 @@ export function printSummary(summary, cache, durationMs) {
115
150
  }
116
151
  console.log(chalk.dim(SEPARATOR) + '\n');
117
152
  }
153
+ export function buildJsonReport(summary, cache, durationMs) {
154
+ const mutants = Object.values(cache).map((entry) => ({
155
+ file: entry.file,
156
+ line: entry.line,
157
+ col: entry.col,
158
+ mutator: entry.mutator,
159
+ status: entry.status,
160
+ ...(entry.originalSnippet !== undefined && {
161
+ originalSnippet: entry.originalSnippet,
162
+ }),
163
+ ...(entry.mutatedSnippet !== undefined && {
164
+ mutatedSnippet: entry.mutatedSnippet,
165
+ }),
166
+ ...(entry.coveringTests !== undefined && {
167
+ coveringTests: entry.coveringTests,
168
+ }),
169
+ }));
170
+ return {
171
+ schemaVersion: 1,
172
+ timestamp: new Date().toISOString(),
173
+ ...(durationMs !== undefined && { durationMs }),
174
+ summary,
175
+ mutants,
176
+ };
177
+ }
118
178
  export function summarise(cache) {
119
179
  const s = computeSummary(cache);
120
180
  printSummary(s, cache);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutineerjs/mutineer",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "A fast, targeted mutation testing framework for JavaScript and TypeScript",
5
5
  "type": "module",
6
6
  "private": false,
@@ -105,6 +105,7 @@
105
105
  "@types/react": "^19.2.14",
106
106
  "@typescript-eslint/eslint-plugin": "^8.56.1",
107
107
  "@typescript-eslint/parser": "^8.47.0",
108
+ "@vitest/coverage-v8": "^4.0.15",
108
109
  "eslint": "^10.0.3",
109
110
  "husky": "^9.1.7",
110
111
  "jsdom": "^28.1.0",