@mutineerjs/mutineer 0.2.4 → 0.4.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 (135) hide show
  1. package/README.md +48 -42
  2. package/dist/bin/mutineer.js +0 -0
  3. package/dist/mutators/__tests__/operator.spec.js +169 -0
  4. package/dist/mutators/__tests__/registry.spec.js +6 -0
  5. package/dist/mutators/__tests__/return-value.spec.js +239 -0
  6. package/dist/mutators/__tests__/utils.spec.js +68 -1
  7. package/dist/mutators/operator.d.ts +25 -0
  8. package/dist/mutators/operator.js +50 -0
  9. package/dist/mutators/registry.d.ts +6 -28
  10. package/dist/mutators/registry.js +14 -66
  11. package/dist/mutators/return-value.d.ts +39 -0
  12. package/dist/mutators/return-value.js +104 -0
  13. package/dist/mutators/utils.d.ts +21 -0
  14. package/dist/mutators/utils.js +44 -27
  15. package/dist/runner/__tests__/pool-executor.spec.js +95 -4
  16. package/dist/runner/__tests__/variants.spec.js +3 -1
  17. package/dist/runner/discover.js +2 -1
  18. package/dist/runner/jest/__tests__/adapter.spec.js +1 -1
  19. package/dist/runner/jest/__tests__/pool.spec.js +5 -6
  20. package/dist/runner/jest/__tests__/worker-runtime.spec.js +2 -2
  21. package/dist/runner/jest/adapter.js +1 -1
  22. package/dist/runner/jest/pool.d.ts +1 -1
  23. package/dist/runner/jest/pool.js +6 -6
  24. package/dist/runner/jest/worker.mjs +1 -1
  25. package/dist/runner/pool-executor.js +21 -1
  26. package/dist/runner/shared/__tests__/redirect-state.spec.js +3 -3
  27. package/dist/runner/shared/index.d.ts +1 -1
  28. package/dist/runner/shared/index.js +1 -1
  29. package/dist/runner/shared/redirect-state.d.ts +2 -2
  30. package/dist/runner/shared/redirect-state.js +4 -4
  31. package/dist/runner/types.d.ts +1 -1
  32. package/dist/runner/vitest/__tests__/adapter.spec.js +1 -1
  33. package/dist/runner/vitest/__tests__/pool.spec.js +1 -1
  34. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +83 -1
  35. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +84 -0
  36. package/dist/runner/vitest/adapter.js +1 -1
  37. package/dist/runner/vitest/index.d.ts +0 -1
  38. package/dist/runner/vitest/index.js +0 -1
  39. package/dist/runner/vitest/pool.d.ts +1 -1
  40. package/dist/runner/vitest/pool.js +7 -7
  41. package/dist/runner/vitest/redirect-loader.d.ts +1 -1
  42. package/dist/runner/vitest/redirect-loader.js +1 -1
  43. package/dist/runner/vitest/worker-runtime.js +3 -3
  44. package/dist/runner/vitest/worker.mjs +1 -1
  45. package/dist/types/mutant.d.ts +2 -0
  46. package/dist/utils/__tests__/coverage.spec.js +167 -0
  47. package/dist/utils/__tests__/progress.spec.js +96 -0
  48. package/dist/utils/__tests__/summary.spec.js +28 -0
  49. package/dist/utils/summary.js +7 -1
  50. package/package.json +71 -22
  51. package/dist/admin/assets/index-B7nXq-e7.js +0 -32
  52. package/dist/admin/assets/index-B7nXq-e7.js.map +0 -1
  53. package/dist/admin/assets/index-BDQLkBUE.js +0 -32
  54. package/dist/admin/assets/index-BDQLkBUE.js.map +0 -1
  55. package/dist/admin/assets/index-DVkP-Tc7.css +0 -1
  56. package/dist/admin/index.html +0 -13
  57. package/dist/admin/server/admin.d.ts +0 -6
  58. package/dist/admin/server/admin.js +0 -234
  59. package/dist/bin/mutate-vitest.d.ts +0 -2
  60. package/dist/bin/mutate-vitest.js +0 -90
  61. package/dist/plugin/viteMutate.d.ts +0 -15
  62. package/dist/plugin/viteMutate.js +0 -52
  63. package/dist/plugin/vitest.setup.d.ts +0 -47
  64. package/dist/plugin/vitest.setup.js +0 -118
  65. package/dist/plugin/withVitest.d.ts +0 -13
  66. package/dist/plugin/withVitest.js +0 -30
  67. package/dist/runner/__tests__/orchestrator.spec.js +0 -55
  68. package/dist/runner/adapters/__tests__/jest.spec.js +0 -88
  69. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +0 -1
  70. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +0 -59
  71. package/dist/runner/adapters/__tests__/vitest.spec.d.ts +0 -1
  72. package/dist/runner/adapters/__tests__/vitest.spec.js +0 -118
  73. package/dist/runner/adapters/index.d.ts +0 -10
  74. package/dist/runner/adapters/index.js +0 -9
  75. package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +0 -1
  76. package/dist/runner/adapters/jest/__tests__/index.spec.js +0 -88
  77. package/dist/runner/adapters/jest/index.d.ts +0 -24
  78. package/dist/runner/adapters/jest/index.js +0 -216
  79. package/dist/runner/adapters/jest/worker-runtime.d.ts +0 -37
  80. package/dist/runner/adapters/jest/worker-runtime.js +0 -171
  81. package/dist/runner/adapters/jest-worker-runtime.d.ts +0 -37
  82. package/dist/runner/adapters/jest-worker-runtime.js +0 -171
  83. package/dist/runner/adapters/jest.d.ts +0 -24
  84. package/dist/runner/adapters/jest.js +0 -216
  85. package/dist/runner/adapters/types.d.ts +0 -89
  86. package/dist/runner/adapters/types.js +0 -8
  87. package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +0 -1
  88. package/dist/runner/adapters/vitest/__tests__/index.spec.js +0 -118
  89. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +0 -1
  90. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +0 -59
  91. package/dist/runner/adapters/vitest/index.d.ts +0 -33
  92. package/dist/runner/adapters/vitest/index.js +0 -267
  93. package/dist/runner/adapters/vitest/worker-runtime.d.ts +0 -25
  94. package/dist/runner/adapters/vitest/worker-runtime.js +0 -118
  95. package/dist/runner/adapters/vitest-worker-runtime.d.ts +0 -25
  96. package/dist/runner/adapters/vitest-worker-runtime.js +0 -118
  97. package/dist/runner/adapters/vitest.d.ts +0 -33
  98. package/dist/runner/adapters/vitest.js +0 -267
  99. package/dist/runner/pool/__tests__/index.spec.d.ts +0 -1
  100. package/dist/runner/pool/__tests__/index.spec.js +0 -83
  101. package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +0 -1
  102. package/dist/runner/pool/__tests__/pool-plugin.spec.js +0 -59
  103. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +0 -1
  104. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +0 -78
  105. package/dist/runner/pool/index.d.ts +0 -8
  106. package/dist/runner/pool/index.js +0 -9
  107. package/dist/runner/pool/jest/pool.d.ts +0 -52
  108. package/dist/runner/pool/jest/pool.js +0 -309
  109. package/dist/runner/pool/jest/worker.d.mts +0 -1
  110. package/dist/runner/pool/jest/worker.mjs +0 -60
  111. package/dist/runner/pool/jest-pool.d.ts +0 -52
  112. package/dist/runner/pool/jest-pool.js +0 -309
  113. package/dist/runner/pool/jest-worker.d.mts +0 -1
  114. package/dist/runner/pool/jest-worker.mjs +0 -60
  115. package/dist/runner/pool/plugin.d.ts +0 -18
  116. package/dist/runner/pool/plugin.js +0 -60
  117. package/dist/runner/pool/pool-plugin.d.ts +0 -18
  118. package/dist/runner/pool/pool-plugin.js +0 -60
  119. package/dist/runner/pool/pool-redirect-loader.d.ts +0 -19
  120. package/dist/runner/pool/pool-redirect-loader.js +0 -116
  121. package/dist/runner/pool/pool-redirect-loader.mjs +0 -146
  122. package/dist/runner/pool/redirect-loader.d.ts +0 -19
  123. package/dist/runner/pool/redirect-loader.js +0 -116
  124. package/dist/runner/pool/vitest/pool.d.ts +0 -70
  125. package/dist/runner/pool/vitest/pool.js +0 -376
  126. package/dist/runner/pool/vitest/worker.d.mts +0 -15
  127. package/dist/runner/pool/vitest/worker.mjs +0 -96
  128. package/dist/runner/pool/vitest-worker.d.mts +0 -15
  129. package/dist/runner/pool/vitest-worker.mjs +0 -96
  130. package/dist/runner/shared-module-redirect.d.ts +0 -56
  131. package/dist/runner/shared-module-redirect.js +0 -84
  132. package/dist/types/api.d.ts +0 -20
  133. package/dist/types/api.js +0 -1
  134. /package/dist/{runner/__tests__/orchestrator.spec.d.ts → mutators/__tests__/operator.spec.d.ts} +0 -0
  135. /package/dist/{runner/adapters/__tests__/jest.spec.d.ts → mutators/__tests__/return-value.spec.d.ts} +0 -0
@@ -2,7 +2,6 @@ import { describe, it, expect, vi } from 'vitest';
2
2
  import { JestPool, runWithJestPool } from '../pool.js';
3
3
  // We'll use the createWorker option to inject mock workers instead of forking processes
4
4
  function makeMockWorker(id) {
5
- let readyResolve = null;
6
5
  const worker = {
7
6
  id,
8
7
  _ready: true,
@@ -33,9 +32,9 @@ const dummyMutant = {
33
32
  describe('JestPool', () => {
34
33
  it('throws if run is called before init', async () => {
35
34
  const pool = new JestPool({ cwd: '/tmp', concurrency: 1 });
36
- await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialized');
35
+ await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialised');
37
36
  });
38
- it('initializes with the specified concurrency', async () => {
37
+ it('initialises with the specified concurrency', async () => {
39
38
  const workers = [];
40
39
  const pool = new JestPool({
41
40
  cwd: '/tmp',
@@ -51,7 +50,7 @@ describe('JestPool', () => {
51
50
  expect(workers[0].start).toHaveBeenCalled();
52
51
  expect(workers[1].start).toHaveBeenCalled();
53
52
  });
54
- it('does not re-initialize if already initialized', async () => {
53
+ it('does not re-initialise if already initialised', async () => {
55
54
  const workers = [];
56
55
  const pool = new JestPool({
57
56
  cwd: '/tmp',
@@ -108,8 +107,8 @@ describe('JestPool', () => {
108
107
  });
109
108
  await pool.init();
110
109
  await pool.shutdown();
111
- // After shutdown, initialized is set to false, so "not initialized" check fires first
112
- await expect(pool.run(dummyMutant, ['test.ts'])).rejects.toThrow('Pool not initialized');
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');
113
112
  });
114
113
  it('does not double-shutdown', async () => {
115
114
  const workers = [];
@@ -1,8 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { JestWorkerRuntime, createJestWorkerRuntime } from '../worker-runtime.js';
2
+ import { JestWorkerRuntime, createJestWorkerRuntime, } from '../worker-runtime.js';
3
3
  // Mock the shared utilities
4
4
  vi.mock('../../shared/index.js', () => ({
5
- getMutantFilePath: vi.fn((file, id) => `/tmp/__mutineer__/mutant_${id}.ts`),
5
+ getMutantFilePath: vi.fn((id) => `/tmp/__mutineer__/mutant_${id}.ts`),
6
6
  setRedirect: vi.fn(),
7
7
  clearRedirect: vi.fn(),
8
8
  }));
@@ -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 });
@@ -1,3 +1,4 @@
1
+ import fs from 'node:fs';
1
2
  import { render } from 'ink';
2
3
  import { createElement } from 'react';
3
4
  import { Progress } from '../utils/progress.js';
@@ -42,7 +43,7 @@ export async function executePool(opts) {
42
43
  }
43
44
  }
44
45
  };
45
- // Initialize worker pool
46
+ // Initialise worker pool
46
47
  const workerLogSuffix = workerCount < concurrency ? ` (requested ${concurrency})` : '';
47
48
  log.info(`Initializing ${adapter.name} worker pool with ${workerCount} workers...${workerLogSuffix}`);
48
49
  const poolStart = Date.now();
@@ -93,12 +94,31 @@ export async function executePool(opts) {
93
94
  col: v.col,
94
95
  }, tests);
95
96
  const status = result.status;
97
+ let originalSnippet;
98
+ let mutatedSnippet;
99
+ if (status === 'escaped') {
100
+ try {
101
+ const originalLines = fs.readFileSync(v.file, 'utf8').split('\n');
102
+ const mutatedLines = v.code.split('\n');
103
+ const lineIdx = v.line - 1;
104
+ const orig = originalLines[lineIdx]?.trim();
105
+ const mutated = mutatedLines[lineIdx]?.trim();
106
+ if (orig !== undefined && mutated !== undefined && orig !== mutated) {
107
+ originalSnippet = orig;
108
+ mutatedSnippet = mutated;
109
+ }
110
+ }
111
+ catch {
112
+ // best-effort
113
+ }
114
+ }
96
115
  cache[key] = {
97
116
  status,
98
117
  file: v.file,
99
118
  line: v.line,
100
119
  col: v.col,
101
120
  mutator: v.name,
121
+ ...(originalSnippet !== undefined && { originalSnippet, mutatedSnippet }),
102
122
  };
103
123
  progress.update(status);
104
124
  }
@@ -1,10 +1,10 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { initializeRedirectState, setRedirect, getRedirect, clearRedirect, } from '../redirect-state.js';
2
+ import { initialiseRedirectState, setRedirect, getRedirect, clearRedirect, } from '../redirect-state.js';
3
3
  describe('redirect-state', () => {
4
4
  beforeEach(() => {
5
- initializeRedirectState();
5
+ initialiseRedirectState();
6
6
  });
7
- describe('initializeRedirectState', () => {
7
+ describe('initialiseRedirectState', () => {
8
8
  it('sets global redirect to null/null', () => {
9
9
  expect(globalThis.__mutineer_redirect__).toEqual({
10
10
  from: null,
@@ -5,5 +5,5 @@
5
5
  * including mutant file path generation and redirect state management.
6
6
  */
7
7
  export { getMutantFilePath } from './mutant-paths.js';
8
- export { setRedirect, getRedirect, clearRedirect, initializeRedirectState, } from './redirect-state.js';
8
+ export { setRedirect, getRedirect, clearRedirect, initialiseRedirectState, } from './redirect-state.js';
9
9
  export type { RedirectConfig } from './redirect-state.js';
@@ -5,4 +5,4 @@
5
5
  * including mutant file path generation and redirect state management.
6
6
  */
7
7
  export { getMutantFilePath } from './mutant-paths.js';
8
- export { setRedirect, getRedirect, clearRedirect, initializeRedirectState, } from './redirect-state.js';
8
+ export { setRedirect, getRedirect, clearRedirect, initialiseRedirectState, } from './redirect-state.js';
@@ -23,10 +23,10 @@ export interface RedirectConfig {
23
23
  readonly to: string;
24
24
  }
25
25
  /**
26
- * Initialize the global redirect state.
26
+ * Initialise the global redirect state.
27
27
  * Must be called once at module load time.
28
28
  */
29
- export declare function initializeRedirectState(): void;
29
+ export declare function initialiseRedirectState(): void;
30
30
  /**
31
31
  * Set the active redirect configuration.
32
32
  *
@@ -8,10 +8,10 @@
8
8
  * test execution.
9
9
  */
10
10
  /**
11
- * Initialize the global redirect state.
11
+ * Initialise the global redirect state.
12
12
  * Must be called once at module load time.
13
13
  */
14
- export function initializeRedirectState() {
14
+ export function initialiseRedirectState() {
15
15
  globalThis.__mutineer_redirect__ = { from: null, to: null };
16
16
  }
17
17
  /**
@@ -46,5 +46,5 @@ export function getRedirect() {
46
46
  export function clearRedirect() {
47
47
  globalThis.__mutineer_redirect__ = { from: null, to: null };
48
48
  }
49
- // Initialize on module load
50
- initializeRedirectState();
49
+ // Initialise on module load
50
+ initialiseRedirectState();
@@ -41,7 +41,7 @@ export interface TestRunnerAdapter {
41
41
  */
42
42
  readonly name: string;
43
43
  /**
44
- * Initialize the adapter (start worker pools, etc.).
44
+ * Initialise the adapter (start worker pools, etc.).
45
45
  * Must be called before running tests.
46
46
  */
47
47
  init(concurrencyOverride?: number): Promise<void>;
@@ -41,7 +41,7 @@ describe('Vitest adapter', () => {
41
41
  afterEach(() => {
42
42
  vi.useRealTimers();
43
43
  });
44
- it('initializes pool with override concurrency', async () => {
44
+ it('initialises pool with override concurrency', async () => {
45
45
  const adapter = makeAdapter();
46
46
  await adapter.init(5);
47
47
  expect(poolInstance?.init).toHaveBeenCalledTimes(1);
@@ -40,7 +40,7 @@ describe('VitestPool', () => {
40
40
  cwd: process.cwd(),
41
41
  concurrency: 1,
42
42
  timeoutMs: 5000,
43
- createWorker: (id, opts) => {
43
+ createWorker: (id) => {
44
44
  const worker = new EventEmitter();
45
45
  worker.id = id;
46
46
  worker.start = vi.fn().mockResolvedValue(undefined);
@@ -7,11 +7,12 @@ vi.mock('node:module', async (importOriginal) => {
7
7
  const actual = await importOriginal();
8
8
  return { ...actual, register: vi.fn() };
9
9
  });
10
- import { resolve as poolResolve } from '../redirect-loader.js';
10
+ import { resolve as poolResolve, initialise } from '../redirect-loader.js';
11
11
  describe('pool-redirect-loader resolve', () => {
12
12
  afterEach(() => {
13
13
  ;
14
14
  globalThis.__mutineer_redirect__ = undefined;
15
+ delete process.env.MUTINEER_DEBUG;
15
16
  vi.restoreAllMocks();
16
17
  });
17
18
  it('resolves .js to .ts in the same directory', async () => {
@@ -88,4 +89,85 @@ describe('pool-redirect-loader resolve', () => {
88
89
  await fs.rm(tmpDir, { recursive: true, force: true });
89
90
  }
90
91
  });
92
+ it('passes through non-.js specifiers to nextResolve', async () => {
93
+ const nextResolve = vi.fn().mockResolvedValue({
94
+ url: 'file:///some/module.ts',
95
+ shortCircuit: false,
96
+ });
97
+ const result = await poolResolve('./module', { parentURL: 'file:///src/index.ts' }, nextResolve);
98
+ expect(nextResolve).toHaveBeenCalled();
99
+ expect(result.url).toBe('file:///some/module.ts');
100
+ });
101
+ it('passes through builtin modules to nextResolve', async () => {
102
+ const nextResolve = vi.fn().mockResolvedValue({
103
+ url: 'node:fs',
104
+ shortCircuit: true,
105
+ });
106
+ const result = await poolResolve('node:fs', { parentURL: 'file:///src/index.ts' }, nextResolve);
107
+ expect(nextResolve).toHaveBeenCalled();
108
+ expect(result.url).toBe('node:fs');
109
+ });
110
+ it('skips .js->ts resolution for non-relative specifiers', async () => {
111
+ const nextResolve = vi.fn().mockResolvedValue({
112
+ url: 'file:///node_modules/pkg/index.js',
113
+ shortCircuit: false,
114
+ });
115
+ const result = await poolResolve('some-package/foo.js', { parentURL: 'file:///src/index.ts' }, nextResolve);
116
+ expect(nextResolve).toHaveBeenCalled();
117
+ expect(result.url).toBe('file:///node_modules/pkg/index.js');
118
+ });
119
+ it('resolves .js to .tsx when .ts does not exist', async () => {
120
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-pool-loader-'));
121
+ const parentFile = path.join(tmpDir, 'src', 'index.ts');
122
+ const tsxFile = path.join(tmpDir, 'src', 'comp.tsx');
123
+ await fs.mkdir(path.dirname(parentFile), { recursive: true });
124
+ await fs.writeFile(parentFile, 'export {}', 'utf8');
125
+ await fs.writeFile(tsxFile, 'export const Comp = () => null', 'utf8');
126
+ try {
127
+ const nextResolve = vi.fn();
128
+ const result = await poolResolve('./comp.js', { parentURL: pathToFileURL(parentFile).href }, nextResolve);
129
+ expect(nextResolve).not.toHaveBeenCalled();
130
+ expect(result.shortCircuit).toBe(true);
131
+ expect(result.url).toBe(pathToFileURL(tsxFile).href);
132
+ }
133
+ finally {
134
+ await fs.rm(tmpDir, { recursive: true, force: true });
135
+ }
136
+ });
137
+ it('tries parent of __mutineer__ directory for ts resolution', async () => {
138
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-pool-loader-'));
139
+ const srcDir = path.join(tmpDir, 'src');
140
+ const mutineerDir = path.join(srcDir, '__mutineer__');
141
+ await fs.mkdir(mutineerDir, { recursive: true });
142
+ const parentFile = path.join(mutineerDir, 'mutant.ts');
143
+ const tsFile = path.join(srcDir, 'sibling.ts');
144
+ await fs.writeFile(parentFile, 'export {}', 'utf8');
145
+ await fs.writeFile(tsFile, 'export const x = 1', 'utf8');
146
+ try {
147
+ const nextResolve = vi.fn();
148
+ const result = await poolResolve('./sibling.js', { parentURL: pathToFileURL(parentFile).href }, nextResolve);
149
+ expect(nextResolve).not.toHaveBeenCalled();
150
+ expect(result.shortCircuit).toBe(true);
151
+ expect(result.url).toBe(pathToFileURL(tsFile).href);
152
+ }
153
+ finally {
154
+ await fs.rm(tmpDir, { recursive: true, force: true });
155
+ }
156
+ });
157
+ it('initialise sets debug mode', () => {
158
+ // Should not throw
159
+ initialise({ debug: true });
160
+ initialise({ debug: false });
161
+ initialise(undefined);
162
+ });
163
+ it('returns null from tryResolveTsExtension when parentURL is invalid', async () => {
164
+ const nextResolve = vi.fn().mockResolvedValue({
165
+ url: 'file:///fallback.js',
166
+ shortCircuit: false,
167
+ });
168
+ // parentURL is not a valid file URL, so tryResolveTsExtension should return null
169
+ await poolResolve('./foo.js', { parentURL: 'not-a-valid-url' }, nextResolve);
170
+ // Falls through to nextResolve
171
+ expect(nextResolve).toHaveBeenCalled();
172
+ });
91
173
  });
@@ -55,6 +55,90 @@ describe('VitestWorkerRuntime', () => {
55
55
  expect(fs.existsSync(path.join(tmp, 'src', '__mutineer__'))).toBe(false);
56
56
  await runtime.shutdown();
57
57
  });
58
+ it('throws when run is called before init', async () => {
59
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-worker-'));
60
+ tmpFiles.push(tmp);
61
+ const runtime = createVitestWorkerRuntime({
62
+ workerId: 'w-noinit',
63
+ cwd: tmp,
64
+ });
65
+ await expect(runtime.run({
66
+ id: 'mut#err',
67
+ name: 'm',
68
+ file: path.join(tmp, 'src.ts'),
69
+ code: 'export const x=1',
70
+ line: 1,
71
+ col: 1,
72
+ }, [path.join(tmp, 'test.ts')])).rejects.toThrow('Vitest runtime not initialised');
73
+ });
74
+ it('shutdown is a no-op when not initialised', async () => {
75
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-worker-'));
76
+ tmpFiles.push(tmp);
77
+ const runtime = createVitestWorkerRuntime({
78
+ workerId: 'w-noinit2',
79
+ cwd: tmp,
80
+ });
81
+ // Should not throw
82
+ await runtime.shutdown();
83
+ expect(closeFn).not.toHaveBeenCalled();
84
+ });
85
+ it('passes vitestConfigPath option to createVitest', async () => {
86
+ const { createVitest } = await import('vitest/node');
87
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-worker-'));
88
+ tmpFiles.push(tmp);
89
+ const runtime = createVitestWorkerRuntime({
90
+ workerId: 'w-config',
91
+ cwd: tmp,
92
+ vitestConfigPath: '/custom/vitest.config.ts',
93
+ });
94
+ await runtime.init();
95
+ expect(createVitest).toHaveBeenCalledWith('test', expect.objectContaining({ config: '/custom/vitest.config.ts' }), expect.any(Object));
96
+ await runtime.shutdown();
97
+ });
98
+ it('handles non-Error thrown during run', async () => {
99
+ runSpecsFn.mockRejectedValue('string error');
100
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-worker-'));
101
+ tmpFiles.push(tmp);
102
+ const runtime = createVitestWorkerRuntime({
103
+ workerId: 'w-strerr',
104
+ cwd: tmp,
105
+ });
106
+ await runtime.init();
107
+ const result = await runtime.run({
108
+ id: 'mut#3',
109
+ name: 'm',
110
+ file: path.join(tmp, 'src.ts'),
111
+ code: 'export const x=1',
112
+ line: 1,
113
+ col: 1,
114
+ }, [path.join(tmp, 'test.ts')]);
115
+ expect(result.killed).toBe(true);
116
+ expect(result.error).toBe('string error');
117
+ await runtime.shutdown();
118
+ });
119
+ it('falls back to all testModules when no relevant modules match', async () => {
120
+ runSpecsFn.mockResolvedValue({
121
+ testModules: [{ moduleId: 'unknown-module', ok: () => true }],
122
+ });
123
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-worker-'));
124
+ tmpFiles.push(tmp);
125
+ const runtime = createVitestWorkerRuntime({
126
+ workerId: 'w-fallback',
127
+ cwd: tmp,
128
+ });
129
+ await runtime.init();
130
+ const result = await runtime.run({
131
+ id: 'mut#4',
132
+ name: 'm',
133
+ file: path.join(tmp, 'src.ts'),
134
+ code: 'export const x=1',
135
+ line: 1,
136
+ col: 1,
137
+ }, [path.join(tmp, 'test.ts')]);
138
+ // Falls back to all testModules; 'unknown-module' reports ok() = true
139
+ expect(result.killed).toBe(false);
140
+ await runtime.shutdown();
141
+ });
58
142
  it('returns escaped when no specs produced', async () => {
59
143
  getProjectByNameFn.mockReturnValue({ createSpecification: () => null });
60
144
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-worker-'));
@@ -154,7 +154,7 @@ export class VitestAdapter {
154
154
  }
155
155
  async runMutant(mutant, tests) {
156
156
  if (!this.pool) {
157
- throw new Error('VitestAdapter not initialized. Call init() first.');
157
+ throw new Error('VitestAdapter not initialised. Call init() first.');
158
158
  }
159
159
  try {
160
160
  const result = await this.pool.run(mutant, [...tests]);
@@ -7,5 +7,4 @@
7
7
  export { VitestAdapter, createVitestAdapter, isCoverageRequestedInArgs, } from './adapter.js';
8
8
  export { VitestPool, runWithPool, type VitestPoolOptions } from './pool.js';
9
9
  export { poolMutineerPlugin } from './plugin.js';
10
- export { resolve as poolRedirectResolve } from './redirect-loader.js';
11
10
  export type { MutantPayload, MutantRunResult, MutantRunSummary, } from '../../types/mutant.js';
@@ -7,4 +7,3 @@
7
7
  export { VitestAdapter, createVitestAdapter, isCoverageRequestedInArgs, } from './adapter.js';
8
8
  export { VitestPool, runWithPool } from './pool.js';
9
9
  export { poolMutineerPlugin } from './plugin.js';
10
- export { resolve as poolRedirectResolve } from './redirect-loader.js';
@@ -47,7 +47,7 @@ export declare class VitestPool {
47
47
  private availableWorkers;
48
48
  private waitingTasks;
49
49
  private readonly options;
50
- private initialized;
50
+ private initialised;
51
51
  private shuttingDown;
52
52
  constructor(options: VitestPoolOptions);
53
53
  init(): Promise<void>;
@@ -221,7 +221,7 @@ export class VitestPool {
221
221
  this.workers = [];
222
222
  this.availableWorkers = [];
223
223
  this.waitingTasks = [];
224
- this.initialized = false;
224
+ this.initialised = false;
225
225
  this.shuttingDown = false;
226
226
  this.options = {
227
227
  cwd: options.cwd,
@@ -232,7 +232,7 @@ export class VitestPool {
232
232
  };
233
233
  }
234
234
  async init() {
235
- if (this.initialized)
235
+ if (this.initialised)
236
236
  return;
237
237
  poolLog.debug(`Initializing pool with ${this.options.concurrency} workers`);
238
238
  const startPromises = [];
@@ -254,8 +254,8 @@ export class VitestPool {
254
254
  }));
255
255
  }
256
256
  await Promise.all(startPromises);
257
- this.initialized = true;
258
- poolLog.debug('Pool initialized');
257
+ this.initialised = true;
258
+ poolLog.debug('Pool initialised');
259
259
  }
260
260
  handleWorkerExit(worker) {
261
261
  // Remove from available list
@@ -313,8 +313,8 @@ export class VitestPool {
313
313
  }
314
314
  }
315
315
  async run(mutant, tests) {
316
- if (!this.initialized) {
317
- throw new Error('Pool not initialized. Call init() first.');
316
+ if (!this.initialised) {
317
+ throw new Error('Pool not initialised. Call init() first.');
318
318
  }
319
319
  if (this.shuttingDown) {
320
320
  throw new Error('Pool is shutting down');
@@ -336,7 +336,7 @@ export class VitestPool {
336
336
  await Promise.all(this.workers.map((w) => w.shutdown()));
337
337
  this.workers = [];
338
338
  this.availableWorkers = [];
339
- this.initialized = false;
339
+ this.initialised = false;
340
340
  poolLog.debug('Pool shut down');
341
341
  }
342
342
  }
@@ -13,7 +13,7 @@ declare global {
13
13
  to: string | null;
14
14
  } | undefined;
15
15
  }
16
- export declare function initialize(data: {
16
+ export declare function initialise(data: {
17
17
  debug?: boolean;
18
18
  } | undefined): void;
19
19
  export declare function resolve(specifier: string, context: {
@@ -17,7 +17,7 @@ register(import.meta.url, {
17
17
  data: { debug: process.env.MUTINEER_DEBUG === '1' },
18
18
  });
19
19
  let DEBUG = process.env.MUTINEER_DEBUG === '1';
20
- export function initialize(data) {
20
+ export function initialise(data) {
21
21
  if (data?.debug !== undefined) {
22
22
  DEBUG = data.debug;
23
23
  }
@@ -25,10 +25,10 @@ export class VitestWorkerRuntime {
25
25
  plugins: [poolMutineerPlugin()],
26
26
  });
27
27
  await this.vitest.init();
28
- log.debug(`Vitest initialized for worker ${this.options.workerId}`);
28
+ log.debug(`Vitest initialised for worker ${this.options.workerId}`);
29
29
  }
30
30
  catch (err) {
31
- log.error(`Failed to initialize Vitest: ${err}`);
31
+ log.error(`Failed to initialise Vitest: ${err}`);
32
32
  throw err;
33
33
  }
34
34
  }
@@ -40,7 +40,7 @@ export class VitestWorkerRuntime {
40
40
  }
41
41
  async run(mutant, tests) {
42
42
  if (!this.vitest) {
43
- throw new Error('Vitest runtime not initialized');
43
+ throw new Error('Vitest runtime not initialised');
44
44
  }
45
45
  const start = Date.now();
46
46
  try {
@@ -36,7 +36,7 @@ async function main() {
36
36
  await runtime.init();
37
37
  }
38
38
  catch (err) {
39
- log.error(`Failed to initialize Vitest: ${err}`);
39
+ log.error(`Failed to initialise Vitest: ${err}`);
40
40
  process.exit(1);
41
41
  }
42
42
  // Signal ready
@@ -25,6 +25,8 @@ export interface Variant extends MutantDescriptor {
25
25
  export interface MutantCacheEntry extends MutantLocation {
26
26
  readonly status: MutantStatus;
27
27
  readonly mutator: string;
28
+ readonly originalSnippet?: string;
29
+ readonly mutatedSnippet?: string;
28
30
  }
29
31
  export interface MutantResult extends MutantCacheEntry {
30
32
  readonly id: string;