@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,65 @@
1
+ /**
2
+ * Vitest Worker Pool
3
+ *
4
+ * Manages a pool of persistent Vitest worker processes that can run
5
+ * multiple mutations without restarting, providing significant speedup
6
+ * over the per-spawn approach.
7
+ *
8
+ * Each worker:
9
+ * - Starts Vitest in watch mode via programmatic API
10
+ * - Receives mutations via stdin (JSON)
11
+ * - Uses dynamic redirect loader to swap module at runtime
12
+ * - Returns results via stdout (JSON)
13
+ */
14
+ import { EventEmitter } from 'node:events';
15
+ import type { MutantPayload, MutantRunResult, MutantRunSummary } from '../../types/mutant.js';
16
+ declare class VitestWorker extends EventEmitter {
17
+ private readonly cwd;
18
+ private readonly vitestConfig?;
19
+ readonly id: string;
20
+ private process;
21
+ private rl;
22
+ private pendingTask;
23
+ private ready;
24
+ private shuttingDown;
25
+ constructor(id: string, cwd: string, vitestConfig?: string | undefined);
26
+ start(): Promise<void>;
27
+ private handleMessage;
28
+ private handleExit;
29
+ isReady(): boolean;
30
+ isBusy(): boolean;
31
+ run(mutant: MutantPayload, tests: string[], timeoutMs?: number): Promise<MutantRunSummary>;
32
+ shutdown(): Promise<void>;
33
+ kill(): void;
34
+ }
35
+ export interface VitestPoolOptions {
36
+ cwd: string;
37
+ concurrency: number;
38
+ vitestConfig?: string;
39
+ timeoutMs?: number;
40
+ createWorker?: (id: string, opts: {
41
+ cwd: string;
42
+ vitestConfig?: string;
43
+ }) => VitestWorker;
44
+ }
45
+ export declare class VitestPool {
46
+ private workers;
47
+ private availableWorkers;
48
+ private waitingTasks;
49
+ private readonly options;
50
+ private initialized;
51
+ private shuttingDown;
52
+ constructor(options: VitestPoolOptions);
53
+ init(): Promise<void>;
54
+ private handleWorkerExit;
55
+ private acquireWorker;
56
+ private releaseWorker;
57
+ run(mutant: MutantPayload, tests: string[]): Promise<MutantRunSummary>;
58
+ shutdown(): Promise<void>;
59
+ }
60
+ /**
61
+ * Run a single mutation using the pool.
62
+ * Convenience function for integration with orchestrator.
63
+ */
64
+ export declare function runWithPool(pool: VitestPool, mutant: MutantPayload, tests: readonly string[]): Promise<MutantRunResult>;
65
+ export type { MutantPayload, MutantRunResult, MutantRunSummary, } from '../../types/mutant.js';
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Vitest Worker Pool
3
+ *
4
+ * Manages a pool of persistent Vitest worker processes that can run
5
+ * multiple mutations without restarting, providing significant speedup
6
+ * over the per-spawn approach.
7
+ *
8
+ * Each worker:
9
+ * - Starts Vitest in watch mode via programmatic API
10
+ * - Receives mutations via stdin (JSON)
11
+ * - Uses dynamic redirect loader to swap module at runtime
12
+ * - Returns results via stdout (JSON)
13
+ */
14
+ import { spawn } from 'node:child_process';
15
+ import * as path from 'node:path';
16
+ import * as readline from 'node:readline';
17
+ import * as fs from 'node:fs';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { EventEmitter } from 'node:events';
20
+ import { createLogger, DEBUG } from '../../utils/logger.js';
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const workerLog = createLogger('VitestWorker');
23
+ const poolLog = createLogger('VitestPool');
24
+ class VitestWorker extends EventEmitter {
25
+ constructor(id, cwd, vitestConfig) {
26
+ super();
27
+ this.cwd = cwd;
28
+ this.vitestConfig = vitestConfig;
29
+ this.process = null;
30
+ this.rl = null;
31
+ this.pendingTask = null;
32
+ this.ready = false;
33
+ this.shuttingDown = false;
34
+ this.id = id;
35
+ }
36
+ async start() {
37
+ const workerJs = path.join(__dirname, 'worker.js');
38
+ const workerMts = path.join(__dirname, 'worker.mjs');
39
+ const workerTs = path.join(__dirname, 'worker.mts');
40
+ const workerScript = fs.existsSync(workerJs)
41
+ ? workerJs
42
+ : fs.existsSync(workerMts)
43
+ ? workerMts
44
+ : workerTs;
45
+ const loaderJs = path.join(__dirname, 'redirect-loader.js');
46
+ const loaderMjs = path.join(__dirname, 'redirect-loader.mjs');
47
+ const loaderTs = path.join(__dirname, 'redirect-loader.ts');
48
+ const loaderScript = fs.existsSync(loaderJs)
49
+ ? loaderJs
50
+ : fs.existsSync(loaderMjs)
51
+ ? loaderMjs
52
+ : loaderTs;
53
+ const env = {
54
+ ...process.env,
55
+ MUTINEER_WORKER_ID: this.id,
56
+ MUTINEER_CWD: this.cwd,
57
+ ...(this.vitestConfig
58
+ ? { MUTINEER_VITEST_CONFIG: this.vitestConfig }
59
+ : {}),
60
+ ...(DEBUG ? { MUTINEER_DEBUG: '1' } : {}),
61
+ };
62
+ workerLog.debug(`[${this.id}] Starting worker process`);
63
+ this.process = spawn(process.execPath, [
64
+ '--experimental-strip-types',
65
+ '--experimental-transform-types',
66
+ '--no-warnings',
67
+ '--import',
68
+ loaderScript,
69
+ workerScript,
70
+ ], {
71
+ cwd: this.cwd,
72
+ env,
73
+ stdio: ['pipe', 'pipe', 'pipe'],
74
+ });
75
+ // Handle stderr (debug/error output)
76
+ this.process.stderr?.on('data', (data) => {
77
+ if (DEBUG) {
78
+ process.stderr.write(`[worker-${this.id}] ${data}`);
79
+ }
80
+ });
81
+ // Set up line reader for stdout (JSON messages)
82
+ this.rl = readline.createInterface({
83
+ input: this.process.stdout,
84
+ terminal: false,
85
+ });
86
+ this.rl.on('line', (line) => this.handleMessage(line));
87
+ this.process.on('error', (err) => {
88
+ workerLog.debug(`[${this.id}] Process error: ${err.message}`);
89
+ this.handleExit(1);
90
+ });
91
+ this.process.on('exit', (code) => {
92
+ workerLog.debug(`[${this.id}] Process exited with code ${code}`);
93
+ this.handleExit(code ?? 1);
94
+ });
95
+ // Wait for ready signal
96
+ await new Promise((resolve, reject) => {
97
+ const timeoutMs = 120_000; // allow more time for Vitest init (coverage, large projects)
98
+ const timeout = setTimeout(() => {
99
+ reject(new Error(`Worker ${this.id} did not become ready in time (${timeoutMs}ms)`));
100
+ }, timeoutMs);
101
+ this.once('ready', () => {
102
+ clearTimeout(timeout);
103
+ resolve();
104
+ });
105
+ this.once('error', (err) => {
106
+ clearTimeout(timeout);
107
+ reject(err);
108
+ });
109
+ });
110
+ }
111
+ handleMessage(line) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed)
114
+ return;
115
+ // Only attempt to parse JSON lines; ignore any other stdout noise
116
+ if (!trimmed.startsWith('{')) {
117
+ workerLog.debug(`[${this.id}] Non-JSON stdout: ${trimmed}`);
118
+ return;
119
+ }
120
+ let msg;
121
+ try {
122
+ msg = JSON.parse(trimmed);
123
+ }
124
+ catch {
125
+ workerLog.debug(`[${this.id}] Invalid JSON from worker: ${line}`);
126
+ return;
127
+ }
128
+ if (msg.type === 'ready') {
129
+ this.ready = true;
130
+ this.emit('ready');
131
+ return;
132
+ }
133
+ if (msg.type === 'result') {
134
+ if (this.pendingTask) {
135
+ const { resolve, timeoutHandle } = this.pendingTask;
136
+ if (timeoutHandle)
137
+ clearTimeout(timeoutHandle);
138
+ this.pendingTask = null;
139
+ resolve({
140
+ killed: msg.killed ?? true,
141
+ durationMs: msg.durationMs ?? 0,
142
+ error: msg.error,
143
+ });
144
+ }
145
+ return;
146
+ }
147
+ if (msg.type === 'shutdown') {
148
+ this.emit('shutdown');
149
+ return;
150
+ }
151
+ }
152
+ handleExit(code) {
153
+ this.ready = false;
154
+ if (this.pendingTask && !this.shuttingDown) {
155
+ const { reject, timeoutHandle } = this.pendingTask;
156
+ if (timeoutHandle)
157
+ clearTimeout(timeoutHandle);
158
+ this.pendingTask = null;
159
+ reject(new Error(`Worker exited unexpectedly with code ${code}`));
160
+ }
161
+ this.emit('exit', code);
162
+ }
163
+ isReady() {
164
+ return this.ready && this.process !== null && !this.shuttingDown;
165
+ }
166
+ isBusy() {
167
+ return this.pendingTask !== null;
168
+ }
169
+ async run(mutant, tests, timeoutMs = 10_000) {
170
+ if (!this.isReady()) {
171
+ throw new Error(`Worker ${this.id} is not ready`);
172
+ }
173
+ if (this.isBusy()) {
174
+ throw new Error(`Worker ${this.id} is busy`);
175
+ }
176
+ return new Promise((resolve, reject) => {
177
+ const timeoutHandle = setTimeout(() => {
178
+ if (this.pendingTask) {
179
+ this.pendingTask = null;
180
+ // Kill and restart the worker on timeout
181
+ this.kill();
182
+ resolve({ killed: true, durationMs: timeoutMs, error: 'timeout' });
183
+ }
184
+ }, timeoutMs);
185
+ this.pendingTask = { resolve, reject, timeoutHandle };
186
+ const request = JSON.stringify({ type: 'run', mutant, tests });
187
+ this.process.stdin.write(request + '\n');
188
+ });
189
+ }
190
+ async shutdown() {
191
+ if (!this.process || this.shuttingDown)
192
+ return;
193
+ this.shuttingDown = true;
194
+ return new Promise((resolve) => {
195
+ const timeout = setTimeout(() => {
196
+ this.kill();
197
+ resolve();
198
+ }, 5000);
199
+ this.once('shutdown', () => {
200
+ clearTimeout(timeout);
201
+ resolve();
202
+ });
203
+ this.process.stdin.write(JSON.stringify({ type: 'shutdown' }) + '\n');
204
+ });
205
+ }
206
+ kill() {
207
+ if (this.process) {
208
+ try {
209
+ this.process.kill('SIGKILL');
210
+ }
211
+ catch {
212
+ // Ignore
213
+ }
214
+ this.process = null;
215
+ }
216
+ this.ready = false;
217
+ }
218
+ }
219
+ export class VitestPool {
220
+ constructor(options) {
221
+ this.workers = [];
222
+ this.availableWorkers = [];
223
+ this.waitingTasks = [];
224
+ this.initialized = false;
225
+ this.shuttingDown = false;
226
+ this.options = {
227
+ cwd: options.cwd,
228
+ concurrency: options.concurrency,
229
+ vitestConfig: options.vitestConfig,
230
+ timeoutMs: options.timeoutMs ?? 10_000,
231
+ createWorker: options.createWorker,
232
+ };
233
+ }
234
+ async init() {
235
+ if (this.initialized)
236
+ return;
237
+ poolLog.debug(`Initializing pool with ${this.options.concurrency} workers`);
238
+ const startPromises = [];
239
+ for (let i = 0; i < this.options.concurrency; i++) {
240
+ const worker = this.options.createWorker?.(`w${i}`, {
241
+ cwd: this.options.cwd,
242
+ vitestConfig: this.options.vitestConfig,
243
+ }) ??
244
+ new VitestWorker(`w${i}`, this.options.cwd, this.options.vitestConfig);
245
+ worker.on('exit', () => {
246
+ if (!this.shuttingDown) {
247
+ this.handleWorkerExit(worker);
248
+ }
249
+ });
250
+ this.workers.push(worker);
251
+ startPromises.push(worker.start().then(() => {
252
+ this.availableWorkers.push(worker);
253
+ poolLog.debug(`Worker ${worker.id} ready`);
254
+ }));
255
+ }
256
+ await Promise.all(startPromises);
257
+ this.initialized = true;
258
+ poolLog.debug('Pool initialized');
259
+ }
260
+ handleWorkerExit(worker) {
261
+ // Remove from available list
262
+ const availIdx = this.availableWorkers.indexOf(worker);
263
+ if (availIdx >= 0) {
264
+ this.availableWorkers.splice(availIdx, 1);
265
+ }
266
+ // Try to restart the worker
267
+ poolLog.debug(`Worker ${worker.id} exited, attempting restart`);
268
+ const newWorker = this.options.createWorker?.(worker.id, {
269
+ cwd: this.options.cwd,
270
+ vitestConfig: this.options.vitestConfig,
271
+ }) ??
272
+ new VitestWorker(worker.id, this.options.cwd, this.options.vitestConfig);
273
+ const idx = this.workers.indexOf(worker);
274
+ if (idx >= 0) {
275
+ this.workers[idx] = newWorker;
276
+ }
277
+ newWorker.on('exit', () => {
278
+ if (!this.shuttingDown) {
279
+ this.handleWorkerExit(newWorker);
280
+ }
281
+ });
282
+ newWorker
283
+ .start()
284
+ .then(() => {
285
+ this.releaseWorker(newWorker);
286
+ poolLog.debug(`Worker ${newWorker.id} restarted`);
287
+ })
288
+ .catch((err) => {
289
+ poolLog.debug(`Failed to restart worker ${newWorker.id}: ${err}`);
290
+ });
291
+ }
292
+ async acquireWorker() {
293
+ // Try to get an available worker
294
+ const worker = this.availableWorkers.shift();
295
+ if (worker) {
296
+ return worker;
297
+ }
298
+ // Wait for one to become available
299
+ return new Promise((resolve) => {
300
+ this.waitingTasks.push(resolve);
301
+ });
302
+ }
303
+ releaseWorker(worker) {
304
+ // If someone is waiting, give them the worker directly
305
+ const waiting = this.waitingTasks.shift();
306
+ if (waiting) {
307
+ waiting(worker);
308
+ return;
309
+ }
310
+ // Otherwise return to the pool
311
+ if (worker.isReady()) {
312
+ this.availableWorkers.push(worker);
313
+ }
314
+ }
315
+ async run(mutant, tests) {
316
+ if (!this.initialized) {
317
+ throw new Error('Pool not initialized. Call init() first.');
318
+ }
319
+ if (this.shuttingDown) {
320
+ throw new Error('Pool is shutting down');
321
+ }
322
+ const worker = await this.acquireWorker();
323
+ try {
324
+ const result = await worker.run(mutant, tests, this.options.timeoutMs);
325
+ return result;
326
+ }
327
+ finally {
328
+ this.releaseWorker(worker);
329
+ }
330
+ }
331
+ async shutdown() {
332
+ if (this.shuttingDown)
333
+ return;
334
+ this.shuttingDown = true;
335
+ poolLog.debug('Shutting down pool');
336
+ await Promise.all(this.workers.map((w) => w.shutdown()));
337
+ this.workers = [];
338
+ this.availableWorkers = [];
339
+ this.initialized = false;
340
+ poolLog.debug('Pool shut down');
341
+ }
342
+ }
343
+ /**
344
+ * Run a single mutation using the pool.
345
+ * Convenience function for integration with orchestrator.
346
+ */
347
+ export async function runWithPool(pool, mutant, tests) {
348
+ try {
349
+ const result = await pool.run(mutant, [...tests]);
350
+ if (result.error === 'timeout') {
351
+ return {
352
+ status: 'timeout',
353
+ durationMs: result.durationMs,
354
+ error: result.error,
355
+ };
356
+ }
357
+ if (result.error && !result.killed) {
358
+ return {
359
+ status: 'error',
360
+ durationMs: result.durationMs,
361
+ error: result.error,
362
+ };
363
+ }
364
+ return {
365
+ status: result.killed ? 'killed' : 'escaped',
366
+ durationMs: result.durationMs,
367
+ };
368
+ }
369
+ catch (err) {
370
+ return {
371
+ status: 'error',
372
+ durationMs: 0,
373
+ error: err instanceof Error ? err.message : String(err),
374
+ };
375
+ }
376
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Dynamic ESM loader for persistent Vitest workers.
3
+ *
4
+ * Reads redirect targets from globalThis.__mutineer_redirect__ on each resolution
5
+ * so workers can swap files without restarting.
6
+ *
7
+ * NOTE: This loader must be self-contained and cannot import from the shared module
8
+ * because it runs in a special Node.js context before module resolution happens.
9
+ */
10
+ declare global {
11
+ var __mutineer_redirect__: {
12
+ from: string | null;
13
+ to: string | null;
14
+ } | undefined;
15
+ }
16
+ export declare function initialize(data: {
17
+ debug?: boolean;
18
+ } | undefined): void;
19
+ export declare function resolve(specifier: string, context: {
20
+ parentURL?: string;
21
+ }, nextResolve: (specifier: string, context: {
22
+ parentURL?: string;
23
+ }) => Promise<{
24
+ url: string;
25
+ shortCircuit?: boolean;
26
+ } | null>): Promise<{
27
+ url: string;
28
+ shortCircuit?: boolean;
29
+ } | null>;
30
+ export declare function load(url: string, context: unknown, nextLoad: (u: string, c: unknown) => Promise<unknown>): Promise<unknown>;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Dynamic ESM loader for persistent Vitest workers.
3
+ *
4
+ * Reads redirect targets from globalThis.__mutineer_redirect__ on each resolution
5
+ * so workers can swap files without restarting.
6
+ *
7
+ * NOTE: This loader must be self-contained and cannot import from the shared module
8
+ * because it runs in a special Node.js context before module resolution happens.
9
+ */
10
+ import { register, builtinModules } from 'node:module';
11
+ import { pathToFileURL, fileURLToPath } from 'node:url';
12
+ import path from 'node:path';
13
+ import fs from 'node:fs';
14
+ // Register this file as the loader hooks module
15
+ register(import.meta.url, {
16
+ parentURL: import.meta.url,
17
+ data: { debug: process.env.MUTINEER_DEBUG === '1' },
18
+ });
19
+ let DEBUG = process.env.MUTINEER_DEBUG === '1';
20
+ export function initialize(data) {
21
+ if (data?.debug !== undefined) {
22
+ DEBUG = data.debug;
23
+ }
24
+ }
25
+ /**
26
+ * Try to resolve a .js import to a .ts or .tsx file (TypeScript ESM convention)
27
+ */
28
+ function tryResolveTsExtension(specifier, parentURL) {
29
+ if (!specifier.endsWith('.js') || !specifier.startsWith('.')) {
30
+ return null;
31
+ }
32
+ let parentPath;
33
+ try {
34
+ parentPath = fileURLToPath(parentURL ?? '');
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ const parentDir = path.dirname(parentPath);
40
+ // If the parent is in a __mutineer__ directory, also try the parent's parent
41
+ const dirsToTry = [parentDir];
42
+ if (path.basename(parentDir) === '__mutineer__') {
43
+ dirsToTry.push(path.dirname(parentDir));
44
+ }
45
+ const tsSpecifier = specifier.slice(0, -3) + '.ts';
46
+ const tsxSpecifier = specifier.slice(0, -3) + '.tsx';
47
+ for (const dir of dirsToTry) {
48
+ const tsPath = path.resolve(dir, tsSpecifier);
49
+ if (fs.existsSync(tsPath)) {
50
+ return pathToFileURL(tsPath).href;
51
+ }
52
+ const tsxPath = path.resolve(dir, tsxSpecifier);
53
+ if (fs.existsSync(tsxPath)) {
54
+ return pathToFileURL(tsxPath).href;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * Get redirect config with URL conversion for ESM loader.
61
+ * Self-contained implementation (cannot use shared module in loader context).
62
+ */
63
+ function getRedirect() {
64
+ const redirect = globalThis.__mutineer_redirect__;
65
+ if (!redirect?.from || !redirect?.to) {
66
+ return null;
67
+ }
68
+ return {
69
+ from: path.resolve(redirect.from),
70
+ fromUrl: pathToFileURL(path.resolve(redirect.from)).href,
71
+ to: redirect.to,
72
+ };
73
+ }
74
+ export async function resolve(specifier, context, nextResolve) {
75
+ const redirect = getRedirect();
76
+ const isBuiltin = specifier.startsWith('node:') || builtinModules.includes(specifier);
77
+ const isNodeModulesSpec = specifier.includes('node_modules');
78
+ const shouldLog = DEBUG && !isBuiltin && !isNodeModulesSpec;
79
+ if (shouldLog) {
80
+ console.error(`[pool-loader] resolve: ${specifier}`);
81
+ if (redirect) {
82
+ console.error(`[pool-loader] active redirect: ${redirect.from} -> ${redirect.to}`);
83
+ }
84
+ }
85
+ // Try to resolve .js -> .ts for TypeScript ESM imports
86
+ const tsResolved = tryResolveTsExtension(specifier, context.parentURL);
87
+ if (tsResolved) {
88
+ if (shouldLog)
89
+ console.error(`[pool-loader] .js -> .ts: ${specifier} -> ${tsResolved}`);
90
+ // Check if this is our redirect target
91
+ if (redirect && tsResolved === redirect.fromUrl) {
92
+ if (DEBUG)
93
+ console.error(`[pool-loader] REDIRECTING ${tsResolved} -> ${pathToFileURL(redirect.to).href}`);
94
+ return {
95
+ url: pathToFileURL(redirect.to).href,
96
+ shortCircuit: true,
97
+ };
98
+ }
99
+ return {
100
+ url: tsResolved,
101
+ shortCircuit: true,
102
+ };
103
+ }
104
+ const resolved = await nextResolve(specifier, context);
105
+ const resolvedInNodeModules = resolved?.url?.includes('/node_modules/');
106
+ const resolvedBuiltin = resolved?.url?.startsWith('node:');
107
+ if (shouldLog && resolved && !resolvedInNodeModules && !resolvedBuiltin)
108
+ console.error(`[pool-loader] resolved ${specifier} to ${resolved.url}`);
109
+ // Check if this resolves to our redirect target
110
+ if (redirect && resolved?.url === redirect.fromUrl) {
111
+ if (DEBUG)
112
+ console.error(`[pool-loader] REDIRECTING ${resolved.url} -> ${pathToFileURL(redirect.to).href}`);
113
+ return {
114
+ ...resolved,
115
+ url: pathToFileURL(redirect.to).href,
116
+ shortCircuit: true,
117
+ };
118
+ }
119
+ return resolved;
120
+ }
121
+ export async function load(url, context, nextLoad) {
122
+ return nextLoad(url, context);
123
+ }
@@ -0,0 +1,16 @@
1
+ import type { MutantPayload } from '../types.js';
2
+ import type { MutantRunSummary } from '../../types/mutant.js';
3
+ export interface VitestWorkerRuntimeOptions {
4
+ workerId: string;
5
+ cwd: string;
6
+ vitestConfigPath?: string;
7
+ }
8
+ export declare class VitestWorkerRuntime {
9
+ private readonly options;
10
+ private vitest;
11
+ constructor(options: VitestWorkerRuntimeOptions);
12
+ init(): Promise<void>;
13
+ shutdown(): Promise<void>;
14
+ run(mutant: MutantPayload, tests: string[]): Promise<MutantRunSummary>;
15
+ }
16
+ export declare function createVitestWorkerRuntime(options: VitestWorkerRuntimeOptions): VitestWorkerRuntime;