@mutineerjs/mutineer 0.2.3 → 0.3.2

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 (162) hide show
  1. package/README.md +48 -42
  2. package/dist/bin/mutineer.js +0 -0
  3. package/dist/core/__tests__/module.spec.js +66 -3
  4. package/dist/core/__tests__/sfc.spec.js +76 -0
  5. package/dist/core/__tests__/variant-utils.spec.js +93 -0
  6. package/dist/mutators/__tests__/operator.spec.js +169 -0
  7. package/dist/mutators/__tests__/registry.spec.js +6 -0
  8. package/dist/mutators/__tests__/return-value.spec.js +239 -0
  9. package/dist/mutators/__tests__/utils.spec.js +68 -1
  10. package/dist/mutators/operator.d.ts +25 -0
  11. package/dist/mutators/operator.js +50 -0
  12. package/dist/mutators/registry.d.ts +6 -28
  13. package/dist/mutators/registry.js +14 -66
  14. package/dist/mutators/return-value.d.ts +39 -0
  15. package/dist/mutators/return-value.js +104 -0
  16. package/dist/mutators/utils.d.ts +21 -0
  17. package/dist/mutators/utils.js +44 -27
  18. package/dist/runner/__tests__/args.spec.js +225 -0
  19. package/dist/runner/__tests__/cache.spec.js +180 -0
  20. package/dist/runner/__tests__/changed.spec.js +227 -0
  21. package/dist/runner/__tests__/cleanup.spec.js +41 -0
  22. package/dist/runner/__tests__/config.spec.js +71 -0
  23. package/dist/runner/__tests__/coverage-resolver.spec.js +171 -0
  24. package/dist/runner/__tests__/pool-executor.spec.js +211 -0
  25. package/dist/runner/__tests__/tasks.spec.js +95 -0
  26. package/dist/runner/__tests__/variants.spec.js +261 -0
  27. package/dist/runner/args.d.ts +5 -0
  28. package/dist/runner/args.js +7 -0
  29. package/dist/runner/config.js +2 -2
  30. package/dist/runner/coverage-resolver.d.ts +21 -0
  31. package/dist/runner/coverage-resolver.js +96 -0
  32. package/dist/runner/discover.js +2 -1
  33. package/dist/runner/jest/__tests__/adapter.spec.js +1 -1
  34. package/dist/runner/jest/__tests__/pool.spec.d.ts +1 -0
  35. package/dist/runner/jest/__tests__/pool.spec.js +211 -0
  36. package/dist/runner/jest/__tests__/worker-runtime.spec.d.ts +1 -0
  37. package/dist/runner/jest/__tests__/worker-runtime.spec.js +148 -0
  38. package/dist/runner/jest/adapter.js +1 -1
  39. package/dist/runner/jest/pool.d.ts +1 -1
  40. package/dist/runner/jest/pool.js +6 -6
  41. package/dist/runner/jest/worker.mjs +1 -1
  42. package/dist/runner/orchestrator.js +43 -295
  43. package/dist/runner/pool-executor.d.ts +17 -0
  44. package/dist/runner/pool-executor.js +143 -0
  45. package/dist/runner/shared/__tests__/mutant-paths.spec.d.ts +1 -0
  46. package/dist/runner/shared/__tests__/mutant-paths.spec.js +66 -0
  47. package/dist/runner/shared/__tests__/redirect-state.spec.d.ts +1 -0
  48. package/dist/runner/shared/__tests__/redirect-state.spec.js +56 -0
  49. package/dist/runner/shared/index.d.ts +1 -1
  50. package/dist/runner/shared/index.js +1 -1
  51. package/dist/runner/shared/redirect-state.d.ts +2 -2
  52. package/dist/runner/shared/redirect-state.js +4 -4
  53. package/dist/runner/tasks.d.ts +12 -0
  54. package/dist/runner/tasks.js +25 -0
  55. package/dist/runner/types.d.ts +1 -1
  56. package/dist/runner/variants.d.ts +17 -2
  57. package/dist/runner/variants.js +33 -0
  58. package/dist/runner/vitest/__tests__/adapter.spec.js +1 -1
  59. package/dist/runner/vitest/__tests__/pool.spec.js +1 -1
  60. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -1
  61. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +84 -0
  62. package/dist/runner/vitest/adapter.js +1 -1
  63. package/dist/runner/vitest/index.d.ts +0 -1
  64. package/dist/runner/vitest/index.js +0 -1
  65. package/dist/runner/vitest/pool.d.ts +1 -1
  66. package/dist/runner/vitest/pool.js +7 -7
  67. package/dist/runner/vitest/redirect-loader.d.ts +1 -1
  68. package/dist/runner/vitest/redirect-loader.js +1 -1
  69. package/dist/runner/vitest/worker-runtime.js +3 -3
  70. package/dist/runner/vitest/worker.mjs +1 -1
  71. package/dist/utils/__tests__/coverage.spec.js +167 -0
  72. package/dist/utils/__tests__/logger.spec.d.ts +1 -0
  73. package/dist/utils/__tests__/logger.spec.js +61 -0
  74. package/dist/utils/__tests__/normalizePath.spec.d.ts +1 -0
  75. package/dist/utils/__tests__/normalizePath.spec.js +22 -0
  76. package/dist/utils/__tests__/progress.spec.js +96 -0
  77. package/package.json +71 -22
  78. package/dist/admin/assets/index-B7nXq-e7.js +0 -32
  79. package/dist/admin/assets/index-B7nXq-e7.js.map +0 -1
  80. package/dist/admin/assets/index-BDQLkBUE.js +0 -32
  81. package/dist/admin/assets/index-BDQLkBUE.js.map +0 -1
  82. package/dist/admin/assets/index-DVkP-Tc7.css +0 -1
  83. package/dist/admin/index.html +0 -13
  84. package/dist/admin/server/admin.d.ts +0 -6
  85. package/dist/admin/server/admin.js +0 -234
  86. package/dist/bin/mutate-vitest.d.ts +0 -2
  87. package/dist/bin/mutate-vitest.js +0 -90
  88. package/dist/plugin/viteMutate.d.ts +0 -15
  89. package/dist/plugin/viteMutate.js +0 -52
  90. package/dist/plugin/vitest.setup.d.ts +0 -47
  91. package/dist/plugin/vitest.setup.js +0 -118
  92. package/dist/plugin/withVitest.d.ts +0 -13
  93. package/dist/plugin/withVitest.js +0 -30
  94. package/dist/runner/__tests__/orchestrator.spec.js +0 -55
  95. package/dist/runner/adapters/__tests__/jest.spec.js +0 -88
  96. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +0 -59
  97. package/dist/runner/adapters/__tests__/vitest.spec.js +0 -118
  98. package/dist/runner/adapters/index.d.ts +0 -10
  99. package/dist/runner/adapters/index.js +0 -9
  100. package/dist/runner/adapters/jest/__tests__/index.spec.js +0 -88
  101. package/dist/runner/adapters/jest/index.d.ts +0 -24
  102. package/dist/runner/adapters/jest/index.js +0 -216
  103. package/dist/runner/adapters/jest/worker-runtime.d.ts +0 -37
  104. package/dist/runner/adapters/jest/worker-runtime.js +0 -171
  105. package/dist/runner/adapters/jest-worker-runtime.d.ts +0 -37
  106. package/dist/runner/adapters/jest-worker-runtime.js +0 -171
  107. package/dist/runner/adapters/jest.d.ts +0 -24
  108. package/dist/runner/adapters/jest.js +0 -216
  109. package/dist/runner/adapters/types.d.ts +0 -89
  110. package/dist/runner/adapters/types.js +0 -8
  111. package/dist/runner/adapters/vitest/__tests__/index.spec.js +0 -118
  112. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +0 -59
  113. package/dist/runner/adapters/vitest/index.d.ts +0 -33
  114. package/dist/runner/adapters/vitest/index.js +0 -267
  115. package/dist/runner/adapters/vitest/worker-runtime.d.ts +0 -25
  116. package/dist/runner/adapters/vitest/worker-runtime.js +0 -118
  117. package/dist/runner/adapters/vitest-worker-runtime.d.ts +0 -25
  118. package/dist/runner/adapters/vitest-worker-runtime.js +0 -118
  119. package/dist/runner/adapters/vitest.d.ts +0 -33
  120. package/dist/runner/adapters/vitest.js +0 -267
  121. package/dist/runner/pool/__tests__/index.spec.js +0 -83
  122. package/dist/runner/pool/__tests__/pool-plugin.spec.js +0 -59
  123. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +0 -78
  124. package/dist/runner/pool/index.d.ts +0 -8
  125. package/dist/runner/pool/index.js +0 -9
  126. package/dist/runner/pool/jest/pool.d.ts +0 -52
  127. package/dist/runner/pool/jest/pool.js +0 -309
  128. package/dist/runner/pool/jest/worker.mjs +0 -60
  129. package/dist/runner/pool/jest-pool.d.ts +0 -52
  130. package/dist/runner/pool/jest-pool.js +0 -309
  131. package/dist/runner/pool/jest-worker.mjs +0 -60
  132. package/dist/runner/pool/plugin.d.ts +0 -18
  133. package/dist/runner/pool/plugin.js +0 -60
  134. package/dist/runner/pool/pool-plugin.d.ts +0 -18
  135. package/dist/runner/pool/pool-plugin.js +0 -60
  136. package/dist/runner/pool/pool-redirect-loader.d.ts +0 -19
  137. package/dist/runner/pool/pool-redirect-loader.js +0 -116
  138. package/dist/runner/pool/pool-redirect-loader.mjs +0 -146
  139. package/dist/runner/pool/redirect-loader.d.ts +0 -19
  140. package/dist/runner/pool/redirect-loader.js +0 -116
  141. package/dist/runner/pool/vitest/pool.d.ts +0 -70
  142. package/dist/runner/pool/vitest/pool.js +0 -376
  143. package/dist/runner/pool/vitest/worker.d.mts +0 -15
  144. package/dist/runner/pool/vitest/worker.mjs +0 -96
  145. package/dist/runner/pool/vitest-worker.d.mts +0 -15
  146. package/dist/runner/pool/vitest-worker.mjs +0 -96
  147. package/dist/runner/shared-module-redirect.d.ts +0 -56
  148. package/dist/runner/shared-module-redirect.js +0 -84
  149. package/dist/types/api.d.ts +0 -20
  150. /package/dist/{runner/__tests__/orchestrator.spec.d.ts → core/__tests__/sfc.spec.d.ts} +0 -0
  151. /package/dist/{runner/adapters/__tests__/jest.spec.d.ts → core/__tests__/variant-utils.spec.d.ts} +0 -0
  152. /package/dist/{runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts → mutators/__tests__/operator.spec.d.ts} +0 -0
  153. /package/dist/{runner/adapters/__tests__/vitest.spec.d.ts → mutators/__tests__/return-value.spec.d.ts} +0 -0
  154. /package/dist/runner/{adapters/jest/__tests__/index.spec.d.ts → __tests__/args.spec.d.ts} +0 -0
  155. /package/dist/runner/{adapters/vitest/__tests__/index.spec.d.ts → __tests__/cache.spec.d.ts} +0 -0
  156. /package/dist/runner/{adapters/vitest/__tests__/worker-runtime.spec.d.ts → __tests__/changed.spec.d.ts} +0 -0
  157. /package/dist/runner/{pool/__tests__/index.spec.d.ts → __tests__/cleanup.spec.d.ts} +0 -0
  158. /package/dist/runner/{pool/__tests__/pool-plugin.spec.d.ts → __tests__/config.spec.d.ts} +0 -0
  159. /package/dist/runner/{pool/__tests__/pool-redirect-loader.spec.d.ts → __tests__/coverage-resolver.spec.d.ts} +0 -0
  160. /package/dist/runner/{pool/jest-worker.d.mts → __tests__/pool-executor.spec.d.ts} +0 -0
  161. /package/dist/runner/{pool/jest/worker.d.mts → __tests__/tasks.spec.d.ts} +0 -0
  162. /package/dist/{types/api.js → runner/__tests__/variants.spec.d.ts} +0 -0
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import { loadCoverageData, loadPerTestCoverageData, } from '../utils/coverage.js';
3
+ import { isCoverageRequestedInArgs } from './vitest/index.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+ const log = createLogger('coverage-resolver');
6
+ /**
7
+ * Resolve all coverage-related configuration from CLI options, config, and adapter detection.
8
+ * Returns a unified resolution object used by the orchestrator.
9
+ */
10
+ export async function resolveCoverageConfig(opts, cfg, adapter, cliArgs) {
11
+ const coverageConfig = await adapter.detectCoverageConfig();
12
+ const wantsPerTestCoverageFromConfig = coverageConfig.perTestEnabled;
13
+ const coveragePreference = cfg.coverage;
14
+ const wantsCoverageRun = coveragePreference === true
15
+ ? true
16
+ : coveragePreference === false
17
+ ? false
18
+ : isCoverageRequestedInArgs([...cliArgs]) || coverageConfig.coverageEnabled;
19
+ // Load pre-existing coverage data if provided
20
+ let coverageData = null;
21
+ if (opts.coverageFilePath) {
22
+ log.info(`Loading coverage data from ${opts.coverageFilePath}...`);
23
+ coverageData = await loadCoverageData(opts.coverageFilePath, process.cwd());
24
+ log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
25
+ }
26
+ const needsCoverageFromBaseline = opts.wantsOnlyCoveredLines && !coverageData;
27
+ const hasCoverageProviderInstalled = adapter.hasCoverageProvider();
28
+ const rawPerTestCoverage = opts.wantsPerTestCoverage ||
29
+ wantsPerTestCoverageFromConfig ||
30
+ (opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
31
+ const wantsPerTestCoverage = opts.runner === 'jest' ? false : rawPerTestCoverage;
32
+ if (opts.runner === 'jest' && rawPerTestCoverage) {
33
+ log.warn('Per-test coverage is not supported for Jest; continuing without per-test coverage.');
34
+ }
35
+ if (needsCoverageFromBaseline && !hasCoverageProviderInstalled) {
36
+ log.warn('The "onlyCoveredLines" option requires a coverage provider to generate coverage data.');
37
+ log.warn('Please install the appropriate coverage package (or disable onlyCoveredLines).');
38
+ process.exitCode = 1;
39
+ return {
40
+ coverageData: null,
41
+ perTestCoverage: null,
42
+ enableCoverageForBaseline: false,
43
+ wantsPerTestCoverage: false,
44
+ needsCoverageFromBaseline,
45
+ };
46
+ }
47
+ if (opts.wantsOnlyCoveredLines &&
48
+ coverageData &&
49
+ !hasCoverageProviderInstalled) {
50
+ log.warn('The "onlyCoveredLines" option is enabled, but no coverage provider is installed.');
51
+ log.warn('Running baseline tests without injecting per-test coverage; existing coverageFile will be used for filtering.');
52
+ }
53
+ const enableCoverageForBaseline = needsCoverageFromBaseline ||
54
+ wantsPerTestCoverage ||
55
+ wantsCoverageRun ||
56
+ (opts.wantsOnlyCoveredLines && hasCoverageProviderInstalled);
57
+ return {
58
+ coverageData,
59
+ perTestCoverage: null,
60
+ enableCoverageForBaseline,
61
+ wantsPerTestCoverage,
62
+ needsCoverageFromBaseline,
63
+ };
64
+ }
65
+ /**
66
+ * Load coverage data produced during the baseline run.
67
+ * Mutates and returns an updated CoverageResolution.
68
+ */
69
+ export async function loadCoverageAfterBaseline(resolution, cwd) {
70
+ let { coverageData, perTestCoverage } = resolution;
71
+ if (resolution.needsCoverageFromBaseline) {
72
+ const defaultCoveragePath = path.join(cwd, 'coverage', 'coverage-final.json');
73
+ log.info(`Loading coverage data from ${defaultCoveragePath}...`);
74
+ try {
75
+ coverageData = await loadCoverageData(defaultCoveragePath, cwd);
76
+ log.info(`Loaded coverage for ${coverageData.coveredLines.size} files`);
77
+ }
78
+ catch (err) {
79
+ const msg = err instanceof Error ? err.message : String(err);
80
+ log.warn(`Warning: Could not load coverage data: ${msg}`);
81
+ log.warn('Continuing without coverage filtering.');
82
+ }
83
+ }
84
+ if (resolution.wantsPerTestCoverage) {
85
+ const reportsDir = path.join(cwd, 'coverage');
86
+ log.info('Loading per-test coverage data...');
87
+ perTestCoverage = await loadPerTestCoverageData(reportsDir, cwd);
88
+ if (!perTestCoverage) {
89
+ log.warn('Per-test coverage data not found. Continuing without per-test test pruning.');
90
+ }
91
+ else {
92
+ log.info(`Loaded per-test coverage for ${perTestCoverage.size} tests`);
93
+ }
94
+ }
95
+ return { ...resolution, coverageData, perTestCoverage };
96
+ }
@@ -85,7 +85,8 @@ async function createViteResolver(rootAbs, exts) {
85
85
  let plugins = [];
86
86
  if (exts.has('.vue')) {
87
87
  try {
88
- const mod = await import('@vitejs/plugin-vue');
88
+ const mod = await import(
89
+ /* @vite-ignore */ '@vitejs/plugin-vue');
89
90
  const vue = mod.default ?? mod;
90
91
  plugins = typeof vue === 'function' ? [vue()] : [];
91
92
  }
@@ -39,7 +39,7 @@ describe('Jest adapter', () => {
39
39
  afterEach(() => {
40
40
  vi.useRealTimers();
41
41
  });
42
- it('initializes pool with override concurrency', async () => {
42
+ it('initialises pool with override concurrency', async () => {
43
43
  const adapter = makeAdapter();
44
44
  await adapter.init(4);
45
45
  expect(poolInstance?.init).toHaveBeenCalledTimes(1);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,211 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { JestPool, runWithJestPool } from '../pool.js';
3
+ // We'll use the createWorker option to inject mock workers instead of forking processes
4
+ function makeMockWorker(id) {
5
+ const worker = {
6
+ id,
7
+ _ready: true,
8
+ _busy: false,
9
+ _mockResult: { killed: true, durationMs: 10 },
10
+ on: vi.fn(),
11
+ once: vi.fn(),
12
+ isReady: vi.fn(() => worker._ready),
13
+ isBusy: vi.fn(() => worker._busy),
14
+ start: vi.fn(async () => {
15
+ // Simulate worker startup
16
+ }),
17
+ run: vi.fn(async () => worker._mockResult),
18
+ shutdown: vi.fn(async () => { }),
19
+ kill: vi.fn(),
20
+ emit: vi.fn(),
21
+ };
22
+ return worker;
23
+ }
24
+ const dummyMutant = {
25
+ id: 'test#1',
26
+ name: 'flipEQ',
27
+ file: '/src/foo.ts',
28
+ code: 'mutated',
29
+ line: 1,
30
+ col: 0,
31
+ };
32
+ describe('JestPool', () => {
33
+ it('throws if run is called before init', async () => {
34
+ const pool = new JestPool({ cwd: '/tmp', concurrency: 1 });
35
+ await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialised');
36
+ });
37
+ it('initialises with the specified concurrency', async () => {
38
+ const workers = [];
39
+ const pool = new JestPool({
40
+ cwd: '/tmp',
41
+ concurrency: 2,
42
+ createWorker: (id) => {
43
+ const w = makeMockWorker(id);
44
+ workers.push(w);
45
+ return w;
46
+ },
47
+ });
48
+ await pool.init();
49
+ expect(workers).toHaveLength(2);
50
+ expect(workers[0].start).toHaveBeenCalled();
51
+ expect(workers[1].start).toHaveBeenCalled();
52
+ });
53
+ it('does not re-initialise if already initialised', async () => {
54
+ const workers = [];
55
+ const pool = new JestPool({
56
+ cwd: '/tmp',
57
+ concurrency: 1,
58
+ createWorker: (id) => {
59
+ const w = makeMockWorker(id);
60
+ workers.push(w);
61
+ return w;
62
+ },
63
+ });
64
+ await pool.init();
65
+ await pool.init(); // second call should be no-op
66
+ expect(workers).toHaveLength(1);
67
+ });
68
+ it('runs a mutant via a worker', async () => {
69
+ const workers = [];
70
+ const pool = new JestPool({
71
+ cwd: '/tmp',
72
+ concurrency: 1,
73
+ createWorker: (id) => {
74
+ const w = makeMockWorker(id);
75
+ w._mockResult = { killed: true, durationMs: 50 };
76
+ workers.push(w);
77
+ return w;
78
+ },
79
+ });
80
+ await pool.init();
81
+ const result = await pool.run(dummyMutant, ['test.ts']);
82
+ expect(result.killed).toBe(true);
83
+ expect(result.durationMs).toBe(50);
84
+ });
85
+ it('shuts down all workers', async () => {
86
+ const workers = [];
87
+ const pool = new JestPool({
88
+ cwd: '/tmp',
89
+ concurrency: 2,
90
+ createWorker: (id) => {
91
+ const w = makeMockWorker(id);
92
+ workers.push(w);
93
+ return w;
94
+ },
95
+ });
96
+ await pool.init();
97
+ await pool.shutdown();
98
+ for (const w of workers) {
99
+ expect(w.shutdown).toHaveBeenCalled();
100
+ }
101
+ });
102
+ it('throws if run is called after shutdown', async () => {
103
+ const pool = new JestPool({
104
+ cwd: '/tmp',
105
+ concurrency: 1,
106
+ createWorker: (id) => makeMockWorker(id),
107
+ });
108
+ await pool.init();
109
+ await pool.shutdown();
110
+ // After shutdown, initialised is set to false, so "not initialised" check fires first
111
+ await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialised');
112
+ });
113
+ it('does not double-shutdown', async () => {
114
+ const workers = [];
115
+ const pool = new JestPool({
116
+ cwd: '/tmp',
117
+ concurrency: 1,
118
+ createWorker: (id) => {
119
+ const w = makeMockWorker(id);
120
+ workers.push(w);
121
+ return w;
122
+ },
123
+ });
124
+ await pool.init();
125
+ await pool.shutdown();
126
+ await pool.shutdown(); // should not throw
127
+ expect(workers[0].shutdown).toHaveBeenCalledTimes(1);
128
+ });
129
+ });
130
+ describe('runWithJestPool', () => {
131
+ it('maps killed result correctly', async () => {
132
+ const pool = new JestPool({
133
+ cwd: '/tmp',
134
+ concurrency: 1,
135
+ createWorker: (id) => {
136
+ const w = makeMockWorker(id);
137
+ w._mockResult = { killed: true, durationMs: 10 };
138
+ return w;
139
+ },
140
+ });
141
+ await pool.init();
142
+ const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
143
+ expect(result.status).toBe('killed');
144
+ expect(result.durationMs).toBe(10);
145
+ await pool.shutdown();
146
+ });
147
+ it('maps escaped result correctly', async () => {
148
+ const pool = new JestPool({
149
+ cwd: '/tmp',
150
+ concurrency: 1,
151
+ createWorker: (id) => {
152
+ const w = makeMockWorker(id);
153
+ w._mockResult = { killed: false, durationMs: 20 };
154
+ return w;
155
+ },
156
+ });
157
+ await pool.init();
158
+ const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
159
+ expect(result.status).toBe('escaped');
160
+ await pool.shutdown();
161
+ });
162
+ it('maps timeout error correctly', async () => {
163
+ const pool = new JestPool({
164
+ cwd: '/tmp',
165
+ concurrency: 1,
166
+ createWorker: (id) => {
167
+ const w = makeMockWorker(id);
168
+ w._mockResult = { killed: true, durationMs: 5000, error: 'timeout' };
169
+ return w;
170
+ },
171
+ });
172
+ await pool.init();
173
+ const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
174
+ expect(result.status).toBe('timeout');
175
+ expect(result.error).toBe('timeout');
176
+ await pool.shutdown();
177
+ });
178
+ it('maps non-timeout error with !killed to error status', async () => {
179
+ const pool = new JestPool({
180
+ cwd: '/tmp',
181
+ concurrency: 1,
182
+ createWorker: (id) => {
183
+ const w = makeMockWorker(id);
184
+ w._mockResult = { killed: false, durationMs: 10, error: 'crash' };
185
+ return w;
186
+ },
187
+ });
188
+ await pool.init();
189
+ const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
190
+ expect(result.status).toBe('error');
191
+ expect(result.error).toBe('crash');
192
+ await pool.shutdown();
193
+ });
194
+ it('handles pool.run throwing an error', async () => {
195
+ const pool = new JestPool({
196
+ cwd: '/tmp',
197
+ concurrency: 1,
198
+ createWorker: (id) => {
199
+ const w = makeMockWorker(id);
200
+ w.run = vi.fn().mockRejectedValue(new Error('pool exploded'));
201
+ return w;
202
+ },
203
+ });
204
+ await pool.init();
205
+ const result = await runWithJestPool(pool, dummyMutant, ['test.ts']);
206
+ expect(result.status).toBe('error');
207
+ expect(result.error).toBe('pool exploded');
208
+ expect(result.durationMs).toBe(0);
209
+ await pool.shutdown();
210
+ });
211
+ });
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { JestWorkerRuntime, createJestWorkerRuntime, } from '../worker-runtime.js';
3
+ // Mock the shared utilities
4
+ vi.mock('../../shared/index.js', () => ({
5
+ getMutantFilePath: vi.fn((id) => `/tmp/__mutineer__/mutant_${id}.ts`),
6
+ setRedirect: vi.fn(),
7
+ clearRedirect: vi.fn(),
8
+ }));
9
+ // Mock fs sync operations
10
+ const writeFileSyncMock = vi.fn();
11
+ const rmSyncMock = vi.fn();
12
+ vi.mock('node:fs', () => ({
13
+ default: {
14
+ existsSync: vi.fn(() => true),
15
+ writeFileSync: (...args) => writeFileSyncMock(...args),
16
+ rmSync: (...args) => rmSyncMock(...args),
17
+ },
18
+ existsSync: vi.fn(() => true),
19
+ writeFileSync: (...args) => writeFileSyncMock(...args),
20
+ rmSync: (...args) => rmSyncMock(...args),
21
+ }));
22
+ // Mock @jest/core
23
+ const mockRunCLI = vi.fn();
24
+ vi.mock('@jest/core', () => ({
25
+ runCLI: (...args) => mockRunCLI(...args),
26
+ }));
27
+ describe('JestWorkerRuntime', () => {
28
+ let runtime;
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ runtime = new JestWorkerRuntime({
32
+ workerId: 'w0',
33
+ cwd: '/project',
34
+ });
35
+ });
36
+ afterEach(() => {
37
+ // Clean up env vars
38
+ delete process.env.MUTINEER_REDIRECT_FROM;
39
+ delete process.env.MUTINEER_REDIRECT_TO;
40
+ });
41
+ it('init and shutdown are no-ops', async () => {
42
+ await expect(runtime.init()).resolves.toBeUndefined();
43
+ await expect(runtime.shutdown()).resolves.toBeUndefined();
44
+ });
45
+ it('runs a mutant and returns killed when tests fail', async () => {
46
+ mockRunCLI.mockResolvedValueOnce({
47
+ results: {
48
+ success: false,
49
+ numTotalTests: 3,
50
+ testResults: [{ failureMessage: 'Expected true to be false' }],
51
+ },
52
+ globalConfig: {},
53
+ });
54
+ const result = await runtime.run({
55
+ id: 'foo.ts#1',
56
+ name: 'flipEQ',
57
+ file: '/project/src/foo.ts',
58
+ code: 'mutated code',
59
+ line: 1,
60
+ col: 0,
61
+ }, ['/project/tests/foo.test.ts']);
62
+ expect(result.killed).toBe(true);
63
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
64
+ expect(result.error).toBe('Expected true to be false');
65
+ });
66
+ it('returns not killed when tests pass', async () => {
67
+ mockRunCLI.mockResolvedValueOnce({
68
+ results: {
69
+ success: true,
70
+ numTotalTests: 3,
71
+ testResults: [],
72
+ },
73
+ globalConfig: {},
74
+ });
75
+ const result = await runtime.run({
76
+ id: 'foo.ts#1',
77
+ name: 'flipEQ',
78
+ file: '/project/src/foo.ts',
79
+ code: 'mutated code',
80
+ line: 1,
81
+ col: 0,
82
+ }, ['/project/tests/foo.test.ts']);
83
+ expect(result.killed).toBe(false);
84
+ });
85
+ it('returns killed on runCLI error', async () => {
86
+ mockRunCLI.mockRejectedValueOnce(new Error('Jest crashed'));
87
+ const result = await runtime.run({
88
+ id: 'foo.ts#1',
89
+ name: 'flipEQ',
90
+ file: '/project/src/foo.ts',
91
+ code: 'mutated code',
92
+ line: 1,
93
+ col: 0,
94
+ }, ['/project/tests/foo.test.ts']);
95
+ expect(result.killed).toBe(true);
96
+ expect(result.error).toBe('Jest crashed');
97
+ });
98
+ it('writes the mutant file and cleans up after run', async () => {
99
+ mockRunCLI.mockResolvedValueOnce({
100
+ results: { success: true, testResults: [] },
101
+ globalConfig: {},
102
+ });
103
+ const { getMutantFilePath, setRedirect, clearRedirect } = await import('../../shared/index.js');
104
+ await runtime.run({
105
+ id: 'foo.ts#1',
106
+ name: 'flipEQ',
107
+ file: '/project/src/foo.ts',
108
+ code: 'mutated code',
109
+ line: 1,
110
+ col: 0,
111
+ }, ['/project/tests/foo.test.ts']);
112
+ expect(getMutantFilePath).toHaveBeenCalled();
113
+ expect(writeFileSyncMock).toHaveBeenCalled();
114
+ expect(setRedirect).toHaveBeenCalled();
115
+ expect(clearRedirect).toHaveBeenCalled();
116
+ expect(rmSyncMock).toHaveBeenCalled();
117
+ });
118
+ it('uses jest config when provided', async () => {
119
+ const runtimeWithConfig = new JestWorkerRuntime({
120
+ workerId: 'w0',
121
+ cwd: '/project',
122
+ jestConfigPath: 'jest.config.ts',
123
+ });
124
+ mockRunCLI.mockResolvedValueOnce({
125
+ results: { success: true, testResults: [] },
126
+ globalConfig: {},
127
+ });
128
+ await runtimeWithConfig.run({
129
+ id: 'foo.ts#1',
130
+ name: 'flipEQ',
131
+ file: '/project/src/foo.ts',
132
+ code: 'mutated code',
133
+ line: 1,
134
+ col: 0,
135
+ }, ['/project/tests/foo.test.ts']);
136
+ const callArgs = mockRunCLI.mock.calls[0][0];
137
+ expect(callArgs.config).toBe('jest.config.ts');
138
+ });
139
+ });
140
+ describe('createJestWorkerRuntime', () => {
141
+ it('returns a JestWorkerRuntime instance', () => {
142
+ const runtime = createJestWorkerRuntime({
143
+ workerId: 'w1',
144
+ cwd: '/project',
145
+ });
146
+ expect(runtime).toBeInstanceOf(JestWorkerRuntime);
147
+ });
148
+ });
@@ -121,7 +121,7 @@ export class JestAdapter {
121
121
  }
122
122
  async runMutant(mutant, tests) {
123
123
  if (!this.pool) {
124
- throw new Error('JestAdapter not initialized. Call init() first.');
124
+ throw new Error('JestAdapter not initialised. Call init() first.');
125
125
  }
126
126
  try {
127
127
  const result = await this.pool.run(mutant, [...tests]);
@@ -33,7 +33,7 @@ export declare class JestPool {
33
33
  private availableWorkers;
34
34
  private waitingTasks;
35
35
  private readonly options;
36
- private initialized;
36
+ private initialised;
37
37
  private shuttingDown;
38
38
  constructor(options: JestPoolOptions);
39
39
  init(): Promise<void>;
@@ -170,7 +170,7 @@ export class JestPool {
170
170
  this.workers = [];
171
171
  this.availableWorkers = [];
172
172
  this.waitingTasks = [];
173
- this.initialized = false;
173
+ this.initialised = false;
174
174
  this.shuttingDown = false;
175
175
  this.options = {
176
176
  cwd: options.cwd,
@@ -181,7 +181,7 @@ export class JestPool {
181
181
  };
182
182
  }
183
183
  async init() {
184
- if (this.initialized)
184
+ if (this.initialised)
185
185
  return;
186
186
  const startPromises = [];
187
187
  for (let i = 0; i < this.options.concurrency; i++) {
@@ -200,7 +200,7 @@ export class JestPool {
200
200
  }));
201
201
  }
202
202
  await Promise.all(startPromises);
203
- this.initialized = true;
203
+ this.initialised = true;
204
204
  }
205
205
  handleWorkerExit(worker) {
206
206
  const availIdx = this.availableWorkers.indexOf(worker);
@@ -249,8 +249,8 @@ export class JestPool {
249
249
  }
250
250
  }
251
251
  async run(mutant, tests) {
252
- if (!this.initialized) {
253
- throw new Error('Pool not initialized. Call init() first.');
252
+ if (!this.initialised) {
253
+ throw new Error('Pool not initialised. Call init() first.');
254
254
  }
255
255
  if (this.shuttingDown) {
256
256
  throw new Error('Pool is shutting down');
@@ -272,7 +272,7 @@ export class JestPool {
272
272
  await Promise.all(this.workers.map((w) => w.shutdown()));
273
273
  this.workers = [];
274
274
  this.availableWorkers = [];
275
- this.initialized = false;
275
+ this.initialised = false;
276
276
  }
277
277
  }
278
278
  export async function runWithJestPool(pool, mutant, tests) {
@@ -16,7 +16,7 @@ async function main() {
16
16
  await runtime.init();
17
17
  }
18
18
  catch (err) {
19
- log.error(`Failed to initialize: ${err}`);
19
+ log.error(`Failed to initialise: ${err}`);
20
20
  process.exit(1);
21
21
  }
22
22
  process.send?.({ type: 'ready', workerId });