@mutineerjs/mutineer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +218 -0
  3. package/dist/admin/assets/index-B7nXq-e7.js +32 -0
  4. package/dist/admin/assets/index-B7nXq-e7.js.map +1 -0
  5. package/dist/admin/assets/index-BDQLkBUE.js +32 -0
  6. package/dist/admin/assets/index-BDQLkBUE.js.map +1 -0
  7. package/dist/admin/assets/index-DVkP-Tc7.css +1 -0
  8. package/dist/admin/index.html +13 -0
  9. package/dist/admin/server/admin.d.ts +6 -0
  10. package/dist/admin/server/admin.js +234 -0
  11. package/dist/bin/mutate-vitest.d.ts +2 -0
  12. package/dist/bin/mutate-vitest.js +90 -0
  13. package/dist/bin/mutineer.d.ts +2 -0
  14. package/dist/bin/mutineer.js +46 -0
  15. package/dist/core/__tests__/module.spec.d.ts +1 -0
  16. package/dist/core/__tests__/module.spec.js +6 -0
  17. package/dist/core/module.d.ts +11 -0
  18. package/dist/core/module.js +14 -0
  19. package/dist/core/sfc.d.ts +12 -0
  20. package/dist/core/sfc.js +54 -0
  21. package/dist/core/types.d.ts +6 -0
  22. package/dist/core/types.js +1 -0
  23. package/dist/core/variant-utils.d.ts +30 -0
  24. package/dist/core/variant-utils.js +54 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.js +3 -0
  27. package/dist/mutators/__tests__/registry.spec.d.ts +1 -0
  28. package/dist/mutators/__tests__/registry.spec.js +43 -0
  29. package/dist/mutators/__tests__/utils.spec.d.ts +1 -0
  30. package/dist/mutators/__tests__/utils.spec.js +15 -0
  31. package/dist/mutators/registry.d.ts +37 -0
  32. package/dist/mutators/registry.js +101 -0
  33. package/dist/mutators/types.d.ts +39 -0
  34. package/dist/mutators/types.js +7 -0
  35. package/dist/mutators/utils.d.ts +37 -0
  36. package/dist/mutators/utils.js +151 -0
  37. package/dist/plugin/viteMutate.d.ts +15 -0
  38. package/dist/plugin/viteMutate.js +52 -0
  39. package/dist/plugin/vitest.setup.d.ts +47 -0
  40. package/dist/plugin/vitest.setup.js +118 -0
  41. package/dist/plugin/withVitest.d.ts +13 -0
  42. package/dist/plugin/withVitest.js +30 -0
  43. package/dist/runner/__tests__/discover.spec.d.ts +1 -0
  44. package/dist/runner/__tests__/discover.spec.js +59 -0
  45. package/dist/runner/__tests__/orchestrator.spec.d.ts +1 -0
  46. package/dist/runner/__tests__/orchestrator.spec.js +55 -0
  47. package/dist/runner/adapters/__tests__/jest.spec.d.ts +1 -0
  48. package/dist/runner/adapters/__tests__/jest.spec.js +88 -0
  49. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +1 -0
  50. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +59 -0
  51. package/dist/runner/adapters/__tests__/vitest.spec.d.ts +1 -0
  52. package/dist/runner/adapters/__tests__/vitest.spec.js +118 -0
  53. package/dist/runner/adapters/index.d.ts +10 -0
  54. package/dist/runner/adapters/index.js +9 -0
  55. package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +1 -0
  56. package/dist/runner/adapters/jest/__tests__/index.spec.js +88 -0
  57. package/dist/runner/adapters/jest/index.d.ts +24 -0
  58. package/dist/runner/adapters/jest/index.js +216 -0
  59. package/dist/runner/adapters/jest/worker-runtime.d.ts +37 -0
  60. package/dist/runner/adapters/jest/worker-runtime.js +171 -0
  61. package/dist/runner/adapters/jest-worker-runtime.d.ts +37 -0
  62. package/dist/runner/adapters/jest-worker-runtime.js +171 -0
  63. package/dist/runner/adapters/jest.d.ts +24 -0
  64. package/dist/runner/adapters/jest.js +216 -0
  65. package/dist/runner/adapters/types.d.ts +89 -0
  66. package/dist/runner/adapters/types.js +8 -0
  67. package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +1 -0
  68. package/dist/runner/adapters/vitest/__tests__/index.spec.js +118 -0
  69. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
  70. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +59 -0
  71. package/dist/runner/adapters/vitest/index.d.ts +33 -0
  72. package/dist/runner/adapters/vitest/index.js +267 -0
  73. package/dist/runner/adapters/vitest/worker-runtime.d.ts +25 -0
  74. package/dist/runner/adapters/vitest/worker-runtime.js +118 -0
  75. package/dist/runner/adapters/vitest-worker-runtime.d.ts +25 -0
  76. package/dist/runner/adapters/vitest-worker-runtime.js +118 -0
  77. package/dist/runner/adapters/vitest.d.ts +33 -0
  78. package/dist/runner/adapters/vitest.js +267 -0
  79. package/dist/runner/args.d.ts +50 -0
  80. package/dist/runner/args.js +123 -0
  81. package/dist/runner/cache.d.ts +38 -0
  82. package/dist/runner/cache.js +118 -0
  83. package/dist/runner/changed.d.ts +22 -0
  84. package/dist/runner/changed.js +210 -0
  85. package/dist/runner/cleanup.d.ts +4 -0
  86. package/dist/runner/cleanup.js +21 -0
  87. package/dist/runner/config.d.ts +13 -0
  88. package/dist/runner/config.js +94 -0
  89. package/dist/runner/discover.d.ts +7 -0
  90. package/dist/runner/discover.js +258 -0
  91. package/dist/runner/jest/__tests__/adapter.spec.d.ts +1 -0
  92. package/dist/runner/jest/__tests__/adapter.spec.js +110 -0
  93. package/dist/runner/jest/adapter.d.ts +24 -0
  94. package/dist/runner/jest/adapter.js +191 -0
  95. package/dist/runner/jest/index.d.ts +8 -0
  96. package/dist/runner/jest/index.js +7 -0
  97. package/dist/runner/jest/pool.d.ts +47 -0
  98. package/dist/runner/jest/pool.js +307 -0
  99. package/dist/runner/jest/resolver.cjs +61 -0
  100. package/dist/runner/jest/resolver.d.cts +11 -0
  101. package/dist/runner/jest/worker-runtime.d.ts +30 -0
  102. package/dist/runner/jest/worker-runtime.js +98 -0
  103. package/dist/runner/jest/worker.d.mts +1 -0
  104. package/dist/runner/jest/worker.mjs +55 -0
  105. package/dist/runner/orchestrator.d.ts +13 -0
  106. package/dist/runner/orchestrator.js +387 -0
  107. package/dist/runner/pool/__tests__/index.spec.d.ts +1 -0
  108. package/dist/runner/pool/__tests__/index.spec.js +83 -0
  109. package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +1 -0
  110. package/dist/runner/pool/__tests__/pool-plugin.spec.js +59 -0
  111. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +1 -0
  112. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +78 -0
  113. package/dist/runner/pool/index.d.ts +8 -0
  114. package/dist/runner/pool/index.js +9 -0
  115. package/dist/runner/pool/jest/pool.d.ts +52 -0
  116. package/dist/runner/pool/jest/pool.js +309 -0
  117. package/dist/runner/pool/jest/worker.d.mts +1 -0
  118. package/dist/runner/pool/jest/worker.mjs +60 -0
  119. package/dist/runner/pool/jest-pool.d.ts +52 -0
  120. package/dist/runner/pool/jest-pool.js +309 -0
  121. package/dist/runner/pool/jest-worker.d.mts +1 -0
  122. package/dist/runner/pool/jest-worker.mjs +60 -0
  123. package/dist/runner/pool/plugin.d.ts +18 -0
  124. package/dist/runner/pool/plugin.js +60 -0
  125. package/dist/runner/pool/pool-plugin.d.ts +18 -0
  126. package/dist/runner/pool/pool-plugin.js +60 -0
  127. package/dist/runner/pool/pool-redirect-loader.d.ts +19 -0
  128. package/dist/runner/pool/pool-redirect-loader.js +116 -0
  129. package/dist/runner/pool/pool-redirect-loader.mjs +146 -0
  130. package/dist/runner/pool/redirect-loader.d.ts +19 -0
  131. package/dist/runner/pool/redirect-loader.js +116 -0
  132. package/dist/runner/pool/vitest/pool.d.ts +70 -0
  133. package/dist/runner/pool/vitest/pool.js +376 -0
  134. package/dist/runner/pool/vitest/worker.d.mts +15 -0
  135. package/dist/runner/pool/vitest/worker.mjs +96 -0
  136. package/dist/runner/pool/vitest-worker.d.mts +15 -0
  137. package/dist/runner/pool/vitest-worker.mjs +96 -0
  138. package/dist/runner/shared/index.d.ts +9 -0
  139. package/dist/runner/shared/index.js +8 -0
  140. package/dist/runner/shared/mutant-paths.d.ts +15 -0
  141. package/dist/runner/shared/mutant-paths.js +30 -0
  142. package/dist/runner/shared/redirect-state.d.ts +45 -0
  143. package/dist/runner/shared/redirect-state.js +50 -0
  144. package/dist/runner/shared-module-redirect.d.ts +56 -0
  145. package/dist/runner/shared-module-redirect.js +84 -0
  146. package/dist/runner/types.d.ts +88 -0
  147. package/dist/runner/types.js +8 -0
  148. package/dist/runner/variants.d.ts +21 -0
  149. package/dist/runner/variants.js +66 -0
  150. package/dist/runner/vitest/__tests__/adapter.spec.d.ts +1 -0
  151. package/dist/runner/vitest/__tests__/adapter.spec.js +131 -0
  152. package/dist/runner/vitest/__tests__/plugin.spec.d.ts +1 -0
  153. package/dist/runner/vitest/__tests__/plugin.spec.js +65 -0
  154. package/dist/runner/vitest/__tests__/pool.spec.d.ts +1 -0
  155. package/dist/runner/vitest/__tests__/pool.spec.js +106 -0
  156. package/dist/runner/vitest/__tests__/redirect-loader.spec.d.ts +1 -0
  157. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -0
  158. package/dist/runner/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
  159. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +75 -0
  160. package/dist/runner/vitest/adapter.d.ts +33 -0
  161. package/dist/runner/vitest/adapter.js +277 -0
  162. package/dist/runner/vitest/index.d.ts +11 -0
  163. package/dist/runner/vitest/index.js +10 -0
  164. package/dist/runner/vitest/plugin.d.ts +12 -0
  165. package/dist/runner/vitest/plugin.js +49 -0
  166. package/dist/runner/vitest/pool.d.ts +65 -0
  167. package/dist/runner/vitest/pool.js +376 -0
  168. package/dist/runner/vitest/redirect-loader.d.ts +30 -0
  169. package/dist/runner/vitest/redirect-loader.js +123 -0
  170. package/dist/runner/vitest/worker-runtime.d.ts +16 -0
  171. package/dist/runner/vitest/worker-runtime.js +105 -0
  172. package/dist/runner/vitest/worker.d.mts +15 -0
  173. package/dist/runner/vitest/worker.mjs +92 -0
  174. package/dist/types/api.d.ts +20 -0
  175. package/dist/types/api.js +1 -0
  176. package/dist/types/config.d.ts +48 -0
  177. package/dist/types/config.js +1 -0
  178. package/dist/types/index.d.ts +13 -0
  179. package/dist/types/index.js +11 -0
  180. package/dist/types/mutant.d.ts +44 -0
  181. package/dist/types/mutant.js +7 -0
  182. package/dist/utils/PoolSpinner.d.ts +5 -0
  183. package/dist/utils/PoolSpinner.js +6 -0
  184. package/dist/utils/ProgressBar.d.ts +11 -0
  185. package/dist/utils/ProgressBar.js +9 -0
  186. package/dist/utils/__tests__/coverage.spec.d.ts +1 -0
  187. package/dist/utils/__tests__/coverage.spec.js +91 -0
  188. package/dist/utils/__tests__/progress.spec.d.ts +1 -0
  189. package/dist/utils/__tests__/progress.spec.js +50 -0
  190. package/dist/utils/__tests__/summary.spec.d.ts +1 -0
  191. package/dist/utils/__tests__/summary.spec.js +54 -0
  192. package/dist/utils/coverage.d.ts +57 -0
  193. package/dist/utils/coverage.js +204 -0
  194. package/dist/utils/logger.d.ts +8 -0
  195. package/dist/utils/logger.js +18 -0
  196. package/dist/utils/progress.d.ts +25 -0
  197. package/dist/utils/progress.js +90 -0
  198. package/dist/utils/summary.d.ts +12 -0
  199. package/dist/utils/summary.js +107 -0
  200. package/package.json +59 -0
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { EventEmitter } from 'node:events';
3
+ import * as childProcess from 'node:child_process';
4
+ import * as readline from 'node:readline';
5
+ import { VitestPool, runWithPool } from '../pool.js';
6
+ vi.mock('node:child_process', () => ({ spawn: vi.fn() }));
7
+ vi.mock('node:readline', () => ({ createInterface: vi.fn() }));
8
+ describe('VitestPool', () => {
9
+ const mockProcesses = [];
10
+ const rlEmitters = [];
11
+ const fakeWorkers = [];
12
+ beforeEach(() => {
13
+ mockProcesses.length = 0;
14
+ rlEmitters.length = 0;
15
+ vi.mocked(childProcess.spawn).mockImplementation(() => {
16
+ const proc = new EventEmitter();
17
+ proc.stdout = new EventEmitter();
18
+ proc.stderr = new EventEmitter();
19
+ proc.stdin = {
20
+ writes: [],
21
+ write: (chunk) => {
22
+ proc.stdin.writes.push(chunk);
23
+ },
24
+ };
25
+ proc.kill = vi.fn();
26
+ mockProcesses.push(proc);
27
+ return proc;
28
+ });
29
+ vi.mocked(readline.createInterface).mockImplementation(() => {
30
+ const rl = new EventEmitter();
31
+ rlEmitters.push(rl);
32
+ return rl;
33
+ });
34
+ });
35
+ afterEach(() => {
36
+ vi.restoreAllMocks();
37
+ });
38
+ it('runs a mutant through a worker and returns result', async () => {
39
+ const pool = new VitestPool({
40
+ cwd: process.cwd(),
41
+ concurrency: 1,
42
+ timeoutMs: 5000,
43
+ createWorker: (id, opts) => {
44
+ const worker = new EventEmitter();
45
+ worker.id = id;
46
+ worker.start = vi.fn().mockResolvedValue(undefined);
47
+ worker.isReady = vi.fn().mockReturnValue(true);
48
+ worker.isBusy = vi.fn().mockReturnValue(false);
49
+ worker.run = vi
50
+ .fn()
51
+ .mockImplementation(async () => ({ killed: true, durationMs: 42 }));
52
+ worker.shutdown = vi.fn().mockResolvedValue(undefined);
53
+ worker.kill = vi.fn();
54
+ fakeWorkers.push(worker);
55
+ return worker;
56
+ },
57
+ });
58
+ await pool.init();
59
+ const mutant = {
60
+ id: '1',
61
+ name: 'mutant',
62
+ file: 'foo.ts',
63
+ code: 'x',
64
+ line: 1,
65
+ col: 1,
66
+ };
67
+ const tests = ['foo.spec.ts'];
68
+ const runPromise = pool.run(mutant, tests);
69
+ const result = await runPromise;
70
+ expect(result).toEqual({ killed: true, durationMs: 42, error: undefined });
71
+ expect(fakeWorkers[0].run).toHaveBeenCalledWith(mutant, tests, 5000);
72
+ await pool.shutdown();
73
+ });
74
+ it('maps runWithPool results to escaped when not killed', async () => {
75
+ const mockPool = {
76
+ run: vi.fn().mockResolvedValue({ killed: false, durationMs: 7 }),
77
+ };
78
+ const mutant = {
79
+ id: '2',
80
+ name: 'm',
81
+ file: 'bar.ts',
82
+ code: 'y',
83
+ line: 2,
84
+ col: 3,
85
+ };
86
+ const tests = ['bar.spec.ts'];
87
+ const result = await runWithPool(mockPool, mutant, tests);
88
+ expect(result).toEqual({ status: 'escaped', durationMs: 7 });
89
+ expect(mockPool.run).toHaveBeenCalledWith(mutant, ['bar.spec.ts']);
90
+ });
91
+ it('maps runWithPool errors to error status', async () => {
92
+ const mockPool = {
93
+ run: vi.fn().mockRejectedValue(new Error('boom')),
94
+ };
95
+ const mutant = {
96
+ id: '3',
97
+ name: 'err',
98
+ file: 'baz.ts',
99
+ code: 'z',
100
+ line: 3,
101
+ col: 4,
102
+ };
103
+ const result = await runWithPool(mockPool, mutant, []);
104
+ expect(result.status).toBe('error');
105
+ });
106
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { resolve as poolResolve } from '../redirect-loader.js';
7
+ describe('pool-redirect-loader resolve', () => {
8
+ afterEach(() => {
9
+ ;
10
+ globalThis.__mutineer_redirect__ = undefined;
11
+ vi.restoreAllMocks();
12
+ });
13
+ it('resolves .js to .ts in the same directory', async () => {
14
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-pool-loader-'));
15
+ const parentFile = path.join(tmpDir, 'src', 'index.ts');
16
+ const tsFile = path.join(tmpDir, 'src', 'foo.ts');
17
+ await fs.mkdir(path.dirname(parentFile), { recursive: true });
18
+ await fs.writeFile(parentFile, 'export {}', 'utf8');
19
+ await fs.writeFile(tsFile, 'export const foo = 1', 'utf8');
20
+ try {
21
+ const nextResolve = vi.fn();
22
+ const result = await poolResolve('./foo.js', { parentURL: pathToFileURL(parentFile).href }, nextResolve);
23
+ expect(nextResolve).not.toHaveBeenCalled();
24
+ expect(result).not.toBeNull();
25
+ expect(result.shortCircuit).toBe(true);
26
+ expect(result.url).toBe(pathToFileURL(tsFile).href);
27
+ }
28
+ finally {
29
+ await fs.rm(tmpDir, { recursive: true, force: true });
30
+ }
31
+ });
32
+ it('redirects to mutated file when target matches', async () => {
33
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-pool-loader-'));
34
+ const parentFile = path.join(tmpDir, 'src', 'index.ts');
35
+ const fromPath = path.join(tmpDir, 'src', 'target.ts');
36
+ const mutatedPath = path.join(tmpDir, 'mutated.ts');
37
+ await fs.mkdir(path.dirname(parentFile), { recursive: true });
38
+ await fs.writeFile(parentFile, 'export {}', 'utf8');
39
+ await fs.writeFile(fromPath, 'export const target = true', 'utf8');
40
+ await fs.writeFile(mutatedPath, 'export const mutated = true', 'utf8');
41
+ try {
42
+ ;
43
+ globalThis.__mutineer_redirect__ = {
44
+ from: fromPath,
45
+ to: mutatedPath,
46
+ };
47
+ const nextResolve = vi.fn();
48
+ const result = await poolResolve('./target.js', { parentURL: pathToFileURL(parentFile).href }, nextResolve);
49
+ expect(nextResolve).not.toHaveBeenCalled();
50
+ expect(result).not.toBeNull();
51
+ expect(result.shortCircuit).toBe(true);
52
+ expect(result.url).toBe(pathToFileURL(mutatedPath).href);
53
+ }
54
+ finally {
55
+ await fs.rm(tmpDir, { recursive: true, force: true });
56
+ }
57
+ });
58
+ it('redirects after delegated resolution', async () => {
59
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-pool-loader-'));
60
+ const parentFile = path.join(tmpDir, 'src', 'index.ts');
61
+ const fromPath = path.join(tmpDir, 'src', 'delegated.ts');
62
+ const mutatedPath = path.join(tmpDir, 'mutated.ts');
63
+ await fs.mkdir(path.dirname(parentFile), { recursive: true });
64
+ await fs.writeFile(parentFile, 'export {}', 'utf8');
65
+ await fs.writeFile(fromPath, 'export const delegated = true', 'utf8');
66
+ await fs.writeFile(mutatedPath, 'export const mutated = true', 'utf8');
67
+ try {
68
+ ;
69
+ globalThis.__mutineer_redirect__ = {
70
+ from: fromPath,
71
+ to: mutatedPath,
72
+ };
73
+ const nextResolve = vi.fn().mockResolvedValue({
74
+ url: pathToFileURL(fromPath).href,
75
+ shortCircuit: false,
76
+ });
77
+ const result = await poolResolve('./delegated', { parentURL: pathToFileURL(parentFile).href }, nextResolve);
78
+ expect(nextResolve).toHaveBeenCalledOnce();
79
+ expect(result).not.toBeNull();
80
+ expect(result.shortCircuit).toBe(true);
81
+ expect(result.url).toBe(pathToFileURL(mutatedPath).href);
82
+ }
83
+ finally {
84
+ await fs.rm(tmpDir, { recursive: true, force: true });
85
+ }
86
+ });
87
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { createVitestWorkerRuntime } from '../worker-runtime.js';
6
+ const initFn = vi.fn();
7
+ const closeFn = vi.fn();
8
+ const runSpecsFn = vi.fn();
9
+ const invalidateFn = vi.fn();
10
+ const getProjectByNameFn = vi.fn();
11
+ vi.mock('vitest/node', () => ({
12
+ createVitest: vi.fn(async () => ({
13
+ init: initFn,
14
+ close: closeFn,
15
+ runTestSpecifications: runSpecsFn,
16
+ invalidateFile: invalidateFn,
17
+ getProjectByName: getProjectByNameFn,
18
+ })),
19
+ }));
20
+ describe('VitestWorkerRuntime', () => {
21
+ const tmpFiles = [];
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ getProjectByNameFn.mockReturnValue({
25
+ createSpecification: (file) => ({ moduleId: file }),
26
+ });
27
+ runSpecsFn.mockResolvedValue({
28
+ testModules: [{ moduleId: 'a', ok: () => false }],
29
+ });
30
+ });
31
+ afterEach(() => {
32
+ for (const f of tmpFiles.splice(0)) {
33
+ try {
34
+ fs.rmSync(f, { recursive: true, force: true });
35
+ }
36
+ catch { }
37
+ }
38
+ });
39
+ it('runs specs and reports kill based on results', async () => {
40
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-worker-'));
41
+ tmpFiles.push(tmp);
42
+ const runtime = createVitestWorkerRuntime({ workerId: 'w1', cwd: tmp });
43
+ await runtime.init();
44
+ const result = await runtime.run({
45
+ id: 'mut#1',
46
+ name: 'm',
47
+ file: path.join(tmp, 'src.ts'),
48
+ code: 'export const x=1',
49
+ line: 1,
50
+ col: 1,
51
+ }, [path.join(tmp, 'test.ts')]);
52
+ expect(initFn).toHaveBeenCalled();
53
+ expect(runSpecsFn).toHaveBeenCalled();
54
+ expect(result.killed).toBe(true);
55
+ expect(fs.existsSync(path.join(tmp, 'src', '__mutineer__'))).toBe(false);
56
+ await runtime.shutdown();
57
+ });
58
+ it('returns escaped when no specs produced', async () => {
59
+ getProjectByNameFn.mockReturnValue({ createSpecification: () => null });
60
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mutineer-worker-'));
61
+ tmpFiles.push(tmp);
62
+ const runtime = createVitestWorkerRuntime({ workerId: 'w2', cwd: tmp });
63
+ await runtime.init();
64
+ const result = await runtime.run({
65
+ id: 'mut#2',
66
+ name: 'm',
67
+ file: path.join(tmp, 'src.ts'),
68
+ code: 'export const x=1',
69
+ line: 1,
70
+ col: 1,
71
+ }, [path.join(tmp, 'test.ts')]);
72
+ expect(result.killed).toBe(false);
73
+ await runtime.shutdown();
74
+ });
75
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Vitest Test Runner Adapter
3
+ *
4
+ * Implements the TestRunnerAdapter interface for Vitest.
5
+ * Handles baseline test runs, mutant execution via worker pool,
6
+ * and coverage configuration detection.
7
+ */
8
+ import type { TestRunnerAdapter, TestRunnerAdapterOptions, MutantPayload, MutantRunResult, BaselineOptions, CoverageConfig } from '../types.js';
9
+ /**
10
+ * Vitest adapter implementation.
11
+ */
12
+ export declare class VitestAdapter implements TestRunnerAdapter {
13
+ readonly name = "vitest";
14
+ private readonly options;
15
+ private readonly vitestPath;
16
+ private pool;
17
+ private baseArgs;
18
+ constructor(options: TestRunnerAdapterOptions);
19
+ init(concurrencyOverride?: number): Promise<void>;
20
+ runBaseline(tests: readonly string[], options: BaselineOptions): Promise<boolean>;
21
+ runMutant(mutant: MutantPayload, tests: readonly string[]): Promise<MutantRunResult>;
22
+ shutdown(): Promise<void>;
23
+ hasCoverageProvider(): boolean;
24
+ detectCoverageConfig(): Promise<CoverageConfig>;
25
+ }
26
+ /**
27
+ * Check if coverage is requested via CLI args.
28
+ */
29
+ export declare function isCoverageRequestedInArgs(args: string[]): boolean;
30
+ /**
31
+ * Factory function for creating VitestAdapter instances.
32
+ */
33
+ export declare function createVitestAdapter(options: TestRunnerAdapterOptions): VitestAdapter;
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Vitest Test Runner Adapter
3
+ *
4
+ * Implements the TestRunnerAdapter interface for Vitest.
5
+ * Handles baseline test runs, mutant execution via worker pool,
6
+ * and coverage configuration detection.
7
+ */
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import { spawn } from 'node:child_process';
11
+ import { createRequire } from 'node:module';
12
+ import { VitestPool } from './pool.js';
13
+ import { createLogger } from '../../utils/logger.js';
14
+ const require = createRequire(import.meta.url);
15
+ const log = createLogger('vitest-adapter');
16
+ /**
17
+ * Resolve the Vitest CLI entry point.
18
+ */
19
+ function resolveVitestPath() {
20
+ try {
21
+ return require.resolve('vitest/vitest.mjs');
22
+ }
23
+ catch {
24
+ const pkgJson = require.resolve('vitest/package.json');
25
+ return path.join(path.dirname(pkgJson), 'vitest.mjs');
26
+ }
27
+ }
28
+ /**
29
+ * Strip mutineer-specific CLI args that shouldn't be passed to Vitest.
30
+ */
31
+ function stripMutineerArgs(args) {
32
+ const out = [];
33
+ const consumeNext = new Set([
34
+ '--concurrency',
35
+ '--progress',
36
+ '--min-kill-percent',
37
+ '--config',
38
+ '-c',
39
+ '--coverage-file',
40
+ ]);
41
+ const dropExact = new Set([
42
+ '-m',
43
+ '--mutate',
44
+ '--changed',
45
+ '--changed-with-deps',
46
+ '--only-covered-lines',
47
+ '--per-test-coverage',
48
+ '--perTestCoverage',
49
+ ]);
50
+ for (let i = 0; i < args.length; i++) {
51
+ const a = args[i];
52
+ if (dropExact.has(a))
53
+ continue;
54
+ if (consumeNext.has(a)) {
55
+ i++;
56
+ continue;
57
+ }
58
+ if (a.startsWith('--min-kill-percent='))
59
+ continue;
60
+ if (a.startsWith('--config=') || a.startsWith('-c='))
61
+ continue;
62
+ out.push(a);
63
+ }
64
+ return out;
65
+ }
66
+ /**
67
+ * Ensure the Vitest config arg is included if specified.
68
+ */
69
+ function ensureConfigArg(args, vitestConfig, cwd) {
70
+ if (!vitestConfig)
71
+ return args;
72
+ if (args.some((a) => a === '--config' ||
73
+ a === '-c' ||
74
+ a.startsWith('--config=') ||
75
+ a.startsWith('-c='))) {
76
+ return args;
77
+ }
78
+ const resolved = cwd ? path.resolve(cwd, vitestConfig) : vitestConfig;
79
+ return [...args, '--config', resolved];
80
+ }
81
+ /**
82
+ * Build Vitest CLI arguments for the given mode.
83
+ */
84
+ function buildVitestArgs(args, mode) {
85
+ const result = [...args];
86
+ if (!result.includes('run') && !result.includes('--run'))
87
+ result.unshift('run');
88
+ if (!result.some((a) => a.startsWith('--watch')))
89
+ result.push('--watch=false');
90
+ if (!result.some((a) => a.startsWith('--passWithNoTests')))
91
+ result.push('--passWithNoTests');
92
+ if (mode === 'baseline-with-coverage') {
93
+ if (!result.some((a) => a.startsWith('--coverage'))) {
94
+ result.push('--coverage.enabled=true', '--coverage.reporter=json');
95
+ }
96
+ if (!result.some((a) => a.startsWith('--coverage.perTest='))) {
97
+ result.push('--coverage.perTest=true');
98
+ }
99
+ }
100
+ return result;
101
+ }
102
+ /**
103
+ * Vitest adapter implementation.
104
+ */
105
+ export class VitestAdapter {
106
+ constructor(options) {
107
+ this.name = 'vitest';
108
+ this.pool = null;
109
+ this.baseArgs = [];
110
+ this.options = options;
111
+ this.vitestPath = resolveVitestPath();
112
+ // Prepare base args by stripping mutineer-specific flags
113
+ const stripped = stripMutineerArgs(options.cliArgs);
114
+ this.baseArgs = ensureConfigArg(stripped, options.config.vitestConfig, options.cwd);
115
+ }
116
+ async init(concurrencyOverride) {
117
+ const workerCount = Math.max(1, concurrencyOverride ?? this.options.concurrency);
118
+ this.pool = new VitestPool({
119
+ cwd: this.options.cwd,
120
+ concurrency: workerCount,
121
+ vitestConfig: this.options.config.vitestConfig,
122
+ timeoutMs: this.options.timeoutMs,
123
+ });
124
+ await this.pool.init();
125
+ }
126
+ async runBaseline(tests, options) {
127
+ const mode = options.collectCoverage
128
+ ? 'baseline-with-coverage'
129
+ : 'baseline';
130
+ const args = buildVitestArgs(this.baseArgs, mode);
131
+ return new Promise((resolve) => {
132
+ const env = { ...process.env };
133
+ env.VITEST_WATCH = 'false';
134
+ if (!env.CI)
135
+ env.CI = '1';
136
+ const child = spawn(process.execPath, [this.vitestPath, ...args, ...tests], {
137
+ cwd: this.options.cwd,
138
+ stdio: ['ignore', 'inherit', 'inherit'],
139
+ env,
140
+ });
141
+ child.on('error', (err) => {
142
+ log.debug('Failed to spawn vitest process: ' + err.message);
143
+ resolve(false);
144
+ });
145
+ child.on('exit', (code) => {
146
+ resolve(code === 0);
147
+ });
148
+ });
149
+ }
150
+ async runMutant(mutant, tests) {
151
+ if (!this.pool) {
152
+ throw new Error('VitestAdapter not initialized. Call init() first.');
153
+ }
154
+ try {
155
+ const result = await this.pool.run(mutant, [...tests]);
156
+ if (result.error === 'timeout') {
157
+ return {
158
+ status: 'timeout',
159
+ durationMs: result.durationMs,
160
+ error: result.error,
161
+ };
162
+ }
163
+ if (result.error) {
164
+ return {
165
+ status: 'error',
166
+ durationMs: result.durationMs,
167
+ error: result.error,
168
+ };
169
+ }
170
+ return {
171
+ status: result.killed ? 'killed' : 'escaped',
172
+ durationMs: result.durationMs,
173
+ error: result.error,
174
+ };
175
+ }
176
+ catch (err) {
177
+ return {
178
+ status: 'error',
179
+ durationMs: 0,
180
+ error: err instanceof Error ? err.message : String(err),
181
+ };
182
+ }
183
+ }
184
+ async shutdown() {
185
+ if (this.pool) {
186
+ await this.pool.shutdown();
187
+ this.pool = null;
188
+ }
189
+ }
190
+ hasCoverageProvider() {
191
+ try {
192
+ require.resolve('@vitest/coverage-v8/package.json', {
193
+ paths: [this.options.cwd],
194
+ });
195
+ return true;
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ }
201
+ async detectCoverageConfig() {
202
+ const configPath = this.options.config.vitestConfig;
203
+ if (!configPath) {
204
+ return { perTestEnabled: false, coverageEnabled: false };
205
+ }
206
+ try {
207
+ const abs = path.isAbsolute(configPath)
208
+ ? configPath
209
+ : path.join(this.options.cwd, configPath);
210
+ const content = await fs.readFile(abs, 'utf8');
211
+ const perTestEnabled = /perTest\s*:\s*true/.test(content);
212
+ let coverageEnabled = false;
213
+ if (!/coverage\s*\.\s*enabled\s*:\s*false/.test(content) &&
214
+ !/coverage\s*:\s*false/.test(content)) {
215
+ coverageEnabled = /coverage\s*:/.test(content);
216
+ }
217
+ return { perTestEnabled, coverageEnabled };
218
+ }
219
+ catch {
220
+ return { perTestEnabled: false, coverageEnabled: false };
221
+ }
222
+ }
223
+ }
224
+ /**
225
+ * Check if coverage is requested via CLI args.
226
+ */
227
+ export function isCoverageRequestedInArgs(args) {
228
+ let requested = false;
229
+ let disabled = false;
230
+ const isFalsey = (v) => typeof v === 'string' && /^(false|0|off)$/i.test(v);
231
+ for (let i = 0; i < args.length; i++) {
232
+ const arg = args[i];
233
+ if (arg === '--no-coverage') {
234
+ disabled = true;
235
+ continue;
236
+ }
237
+ if (arg === '--coverage') {
238
+ requested = true;
239
+ continue;
240
+ }
241
+ if (arg === '--coverage.enabled') {
242
+ const next = args[i + 1];
243
+ if (isFalsey(next))
244
+ disabled = true;
245
+ else
246
+ requested = true;
247
+ continue;
248
+ }
249
+ if (arg.startsWith('--coverage.enabled=')) {
250
+ const val = arg.slice('--coverage.enabled='.length);
251
+ if (isFalsey(val))
252
+ disabled = true;
253
+ else
254
+ requested = true;
255
+ continue;
256
+ }
257
+ if (arg.startsWith('--coverage=')) {
258
+ const val = arg.slice('--coverage='.length);
259
+ if (isFalsey(val))
260
+ disabled = true;
261
+ else
262
+ requested = true;
263
+ continue;
264
+ }
265
+ if (arg.startsWith('--coverage.')) {
266
+ requested = true;
267
+ continue;
268
+ }
269
+ }
270
+ return requested && !disabled;
271
+ }
272
+ /**
273
+ * Factory function for creating VitestAdapter instances.
274
+ */
275
+ export function createVitestAdapter(options) {
276
+ return new VitestAdapter(options);
277
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Vitest Test Runner
3
+ *
4
+ * Complete Vitest test runner implementation including adapter, pool, worker runtime,
5
+ * and Vitest-specific plugin/loader utilities.
6
+ */
7
+ export { VitestAdapter, createVitestAdapter, isCoverageRequestedInArgs, } from './adapter.js';
8
+ export { VitestPool, runWithPool, type VitestPoolOptions } from './pool.js';
9
+ export { poolMutineerPlugin } from './plugin.js';
10
+ export { resolve as poolRedirectResolve } from './redirect-loader.js';
11
+ export type { MutantPayload, MutantRunResult, MutantRunSummary, } from '../../types/mutant.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Vitest Test Runner
3
+ *
4
+ * Complete Vitest test runner implementation including adapter, pool, worker runtime,
5
+ * and Vitest-specific plugin/loader utilities.
6
+ */
7
+ export { VitestAdapter, createVitestAdapter, isCoverageRequestedInArgs, } from './adapter.js';
8
+ export { VitestPool, runWithPool } from './pool.js';
9
+ export { poolMutineerPlugin } from './plugin.js';
10
+ export { resolve as poolRedirectResolve } from './redirect-loader.js';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Vite plugin for persistent Vitest workers.
3
+ *
4
+ * Unlike the standard viteMutineerPlugin which reads env vars once at init,
5
+ * this plugin reads from a global redirect map that can be updated dynamically
6
+ * between test runs.
7
+ *
8
+ * The worker process sets globalThis.__mutineer_redirect__ before each test run,
9
+ * and this plugin intercepts module loading to return the mutated code.
10
+ */
11
+ import type { PluginOption } from 'vite';
12
+ export declare function poolMutineerPlugin(): PluginOption;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Vite plugin for persistent Vitest workers.
3
+ *
4
+ * Unlike the standard viteMutineerPlugin which reads env vars once at init,
5
+ * this plugin reads from a global redirect map that can be updated dynamically
6
+ * between test runs.
7
+ *
8
+ * The worker process sets globalThis.__mutineer_redirect__ before each test run,
9
+ * and this plugin intercepts module loading to return the mutated code.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { getRedirect } from '../shared/index.js';
14
+ import { createLogger } from '../../utils/logger.js';
15
+ const log = createLogger('mutineer:swap');
16
+ export function poolMutineerPlugin() {
17
+ return {
18
+ name: 'mutineer:swap',
19
+ enforce: 'pre',
20
+ load(id) {
21
+ const redirect = getRedirect();
22
+ if (!redirect) {
23
+ return null;
24
+ }
25
+ // Normalize the module ID, handling query strings
26
+ const cleanId = id.split('?')[0];
27
+ let normalizedId;
28
+ try {
29
+ normalizedId = path.resolve(cleanId);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ // Check if this is the file we're redirecting
35
+ if (normalizedId === path.resolve(redirect.from)) {
36
+ // Read the mutated code from the temp file
37
+ try {
38
+ const mutatedCode = fs.readFileSync(redirect.to, 'utf8');
39
+ return mutatedCode;
40
+ }
41
+ catch (err) {
42
+ log.error(`Failed to read mutant file: ${redirect.to} ${err}`);
43
+ return null;
44
+ }
45
+ }
46
+ return null;
47
+ },
48
+ };
49
+ }