@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,216 @@
1
+ /**
2
+ * Jest Test Runner Adapter
3
+ *
4
+ * Implements the TestRunnerAdapter interface for Jest using runCLI.
5
+ * Baseline runs are executed directly via runCLI; mutant runs are delegated
6
+ * to a pool of long-lived worker processes that also use runCLI with a
7
+ * redirect resolver to swap in mutated code.
8
+ */
9
+ import fs from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { createRequire } from 'node:module';
12
+ // import os from 'node:os'
13
+ import { JestPool } from '../../pool/jest/pool.js';
14
+ const require = createRequire(import.meta.url);
15
+ /**
16
+ * Strip mutineer-specific CLI args that shouldn't be passed to Jest.
17
+ */
18
+ function stripMutineerArgs(args) {
19
+ const out = [];
20
+ const consumeNext = new Set([
21
+ '--concurrency',
22
+ '--progress',
23
+ '--min-kill-percent',
24
+ '--config',
25
+ '-c',
26
+ '--coverage-file',
27
+ '--runner',
28
+ ]);
29
+ const dropExact = new Set([
30
+ '-m',
31
+ '--mutate',
32
+ '--changed',
33
+ '--changed-with-deps',
34
+ '--only-covered-lines',
35
+ '--per-test-coverage',
36
+ '--perTestCoverage',
37
+ ]);
38
+ for (let i = 0; i < args.length; i++) {
39
+ const a = args[i];
40
+ if (dropExact.has(a))
41
+ continue;
42
+ if (consumeNext.has(a)) {
43
+ i++;
44
+ continue;
45
+ }
46
+ if (a.startsWith('--min-kill-percent='))
47
+ continue;
48
+ if (a.startsWith('--config=') || a.startsWith('-c='))
49
+ continue;
50
+ out.push(a);
51
+ }
52
+ return out;
53
+ }
54
+ // function getMutantFilePath(originalFile: string, mutantId: string): string {
55
+ // const dir = path.dirname(originalFile)
56
+ // const ext = path.extname(originalFile)
57
+ // const basename = path.basename(originalFile, ext)
58
+ // const mutineerDir = path.join(dir, '__mutineer__')
59
+ // return path.join(mutineerDir, `${basename}_${mutantId.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 20)}${ext}`)
60
+ // }
61
+ // function ensureMutineerDir(file: string): void {
62
+ // const dir = path.dirname(file)
63
+ // if (!fs) return
64
+ // }
65
+ // function createResolverModule(workerId: string): string {
66
+ // const resolverPath = path.join(os.tmpdir(), `mutineer-jest-resolver-${workerId}.cjs`)
67
+ // const resolverCode = `
68
+ // const path = require('path');
69
+ // const { defaultResolver } = require('jest-resolve');
70
+ // module.exports = (request, options) => {
71
+ // const from = process.env.MUTINEER_REDIRECT_FROM;
72
+ // const to = process.env.MUTINEER_REDIRECT_TO;
73
+ // if (!from || !to) {
74
+ // return defaultResolver(request, options);
75
+ // }
76
+ // const normalizedFrom = path.resolve(from);
77
+ // const baseDir = options?.basedir ?? process.cwd();
78
+ // const candidate = path.resolve(baseDir, request);
79
+ // if (candidate === normalizedFrom || request === normalizedFrom) {
80
+ // return defaultResolver(to, options);
81
+ // }
82
+ // return defaultResolver(request, options);
83
+ // };
84
+ // `
85
+ // fs.writeFile(resolverPath, resolverCode, 'utf8')
86
+ // .catch(() => {
87
+ // /* ignore */
88
+ // })
89
+ // return resolverPath
90
+ // }
91
+ async function loadRunCLI(requireFromCwd) {
92
+ try {
93
+ return requireFromCwd('@jest/core');
94
+ }
95
+ catch {
96
+ return import('@jest/core');
97
+ }
98
+ }
99
+ /**
100
+ * Build Jest runCLI options for the given mode.
101
+ */
102
+ function buildJestCliOptions(tests, mode, jestConfigPath) {
103
+ const opts = {
104
+ _: [...tests],
105
+ $0: 'mutineer',
106
+ runInBand: true,
107
+ runTestsByPath: true,
108
+ watch: false,
109
+ passWithNoTests: true,
110
+ testPathPattern: [...tests],
111
+ };
112
+ if (jestConfigPath) {
113
+ opts.config = jestConfigPath;
114
+ }
115
+ if (mode === 'baseline-with-coverage') {
116
+ opts.coverage = true;
117
+ opts.collectCoverage = true;
118
+ opts.coverageProvider = 'v8';
119
+ }
120
+ return opts;
121
+ }
122
+ export class JestAdapter {
123
+ constructor(options) {
124
+ this.name = 'jest';
125
+ this.pool = null;
126
+ this.options = options;
127
+ this.jestConfigPath = options.config.jestConfig;
128
+ stripMutineerArgs(options.cliArgs);
129
+ this.requireFromCwd = createRequire(path.join(options.cwd, 'package.json'));
130
+ }
131
+ async init(concurrencyOverride) {
132
+ const workerCount = Math.max(1, concurrencyOverride ?? this.options.concurrency);
133
+ this.pool = new JestPool({
134
+ cwd: this.options.cwd,
135
+ concurrency: workerCount,
136
+ jestConfig: this.options.config.jestConfig,
137
+ timeoutMs: this.options.timeoutMs,
138
+ debug: this.options.debug,
139
+ });
140
+ await this.pool.init();
141
+ }
142
+ async runBaseline(tests, options) {
143
+ const mode = options.collectCoverage ? 'baseline-with-coverage' : 'baseline';
144
+ const cliOptions = buildJestCliOptions(tests, mode, this.jestConfigPath);
145
+ try {
146
+ const { runCLI } = await loadRunCLI(this.requireFromCwd);
147
+ const { results } = await runCLI(cliOptions, [this.options.cwd]);
148
+ return results.success;
149
+ }
150
+ catch (err) {
151
+ if (this.options.debug) {
152
+ console.error('Failed to run Jest baseline', err);
153
+ }
154
+ return false;
155
+ }
156
+ }
157
+ async runMutant(mutant, tests) {
158
+ if (!this.pool) {
159
+ throw new Error('JestAdapter not initialized. Call init() first.');
160
+ }
161
+ try {
162
+ const result = await this.pool.run(mutant, [...tests]);
163
+ if (result.error === 'timeout') {
164
+ return { status: 'timeout', durationMs: result.durationMs, error: result.error };
165
+ }
166
+ if (result.error) {
167
+ return { status: 'error', durationMs: result.durationMs, error: result.error };
168
+ }
169
+ return {
170
+ status: result.killed ? 'killed' : 'escaped',
171
+ durationMs: result.durationMs,
172
+ error: result.error,
173
+ };
174
+ }
175
+ catch (err) {
176
+ return {
177
+ status: 'error',
178
+ durationMs: 0,
179
+ error: err instanceof Error ? err.message : String(err),
180
+ };
181
+ }
182
+ }
183
+ async shutdown() {
184
+ if (this.pool) {
185
+ await this.pool.shutdown();
186
+ this.pool = null;
187
+ }
188
+ }
189
+ hasCoverageProvider() {
190
+ try {
191
+ require.resolve('jest/package.json', { paths: [this.options.cwd] });
192
+ return true;
193
+ }
194
+ catch {
195
+ return false;
196
+ }
197
+ }
198
+ async detectCoverageConfig() {
199
+ const configPath = this.options.config.jestConfig;
200
+ if (!configPath) {
201
+ return { perTestEnabled: false, coverageEnabled: false };
202
+ }
203
+ try {
204
+ const abs = path.isAbsolute(configPath) ? configPath : path.join(this.options.cwd, configPath);
205
+ const content = await fs.readFile(abs, 'utf8');
206
+ const coverageEnabled = /collectCoverage\s*:\s*true/.test(content) || /coverageProvider\s*:/.test(content);
207
+ return { perTestEnabled: false, coverageEnabled };
208
+ }
209
+ catch {
210
+ return { perTestEnabled: false, coverageEnabled: false };
211
+ }
212
+ }
213
+ }
214
+ export function createJestAdapter(options) {
215
+ return new JestAdapter(options);
216
+ }
@@ -0,0 +1,37 @@
1
+ import type { MutantPayload } from '../types.js';
2
+ import type { MutantRunSummary } from '../../../types/mutant.js';
3
+ declare global {
4
+ var __mutineer_redirect__: {
5
+ from: string | null;
6
+ to: string | null;
7
+ } | undefined;
8
+ }
9
+ interface JestTestResult {
10
+ failureMessage?: string;
11
+ }
12
+ interface JestAggregatedResult {
13
+ success: boolean;
14
+ numTotalTests?: number;
15
+ testResults?: JestTestResult[];
16
+ }
17
+ export type JestRunCLI = (argv: Record<string, unknown>, projects: string[]) => Promise<{
18
+ results: JestAggregatedResult;
19
+ globalConfig: unknown;
20
+ }>;
21
+ export interface JestWorkerRuntimeOptions {
22
+ workerId: string;
23
+ cwd: string;
24
+ jestConfigPath?: string;
25
+ debug?: boolean;
26
+ }
27
+ export declare class JestWorkerRuntime {
28
+ private readonly options;
29
+ private resolverPath;
30
+ private readonly requireFromCwd;
31
+ constructor(options: JestWorkerRuntimeOptions);
32
+ init(): Promise<void>;
33
+ shutdown(): Promise<void>;
34
+ run(mutant: MutantPayload, tests: string[]): Promise<MutantRunSummary>;
35
+ }
36
+ export declare function createJestWorkerRuntime(options: JestWorkerRuntimeOptions): JestWorkerRuntime;
37
+ export {};
@@ -0,0 +1,171 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import { createRequire } from 'node:module';
5
+ globalThis.__mutineer_redirect__ = { from: null, to: null };
6
+ function getMutantFilePath(originalFile, mutantId) {
7
+ const dir = path.dirname(originalFile);
8
+ const ext = path.extname(originalFile);
9
+ const basename = path.basename(originalFile, ext);
10
+ const mutineerDir = path.join(dir, '__mutineer__');
11
+ if (!fs.existsSync(mutineerDir)) {
12
+ fs.mkdirSync(mutineerDir, { recursive: true });
13
+ }
14
+ const idMatch = mutantId.match(/#(\d+)$/);
15
+ const suffix = idMatch ? idMatch[1] : mutantId.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 20);
16
+ return path.join(mutineerDir, `${basename}_${suffix}${ext}`);
17
+ }
18
+ function resolveDefaultResolver(cwdRequire) {
19
+ try {
20
+ cwdRequire('jest-resolve');
21
+ return 'jest-resolve';
22
+ }
23
+ catch {
24
+ return 'jest-resolve';
25
+ }
26
+ }
27
+ function createResolverModule(workerId, requireFromCwd, projectCwd) {
28
+ const resolverPath = path.join(os.tmpdir(), `mutineer-jest-resolver-${workerId}.cjs`);
29
+ // Resolve defaultResolver relative to the project so we don't require jest-resolve as a global dep
30
+ const defaultResolverPath = resolveDefaultResolver(requireFromCwd);
31
+ const projectPkgPath = path.join(projectCwd, 'package.json');
32
+ const resolverCode = `
33
+ const path = require('path');
34
+ const { createRequire } = require('module');
35
+ const projectRequire = createRequire(${JSON.stringify(projectPkgPath)});
36
+ const defaultResolverModule = projectRequire(${JSON.stringify(defaultResolverPath)});
37
+ const defaultResolver = defaultResolverModule.default || defaultResolverModule;
38
+
39
+ module.exports = (request, options) => {
40
+ const resolveWith = (req, opts) => {
41
+ const baseOpts = { ...opts, paths: [${JSON.stringify(projectCwd + '/node_modules')}] };
42
+ if (options?.defaultResolver) return options.defaultResolver(req, baseOpts);
43
+ return defaultResolver(req, baseOpts);
44
+ };
45
+
46
+ const from = process.env.MUTINEER_REDIRECT_FROM;
47
+ const to = process.env.MUTINEER_REDIRECT_TO;
48
+ const normalizedFrom = from ? path.resolve(from) : null;
49
+
50
+ const resolved = (() => {
51
+ try { return resolveWith(request, options); } catch { return null; }
52
+ })();
53
+ const baseDir = options?.basedir ?? process.cwd();
54
+ const candidate = path.resolve(baseDir, request);
55
+ const matchesFrom = (p) => {
56
+ if (!normalizedFrom) return false;
57
+ const abs = path.resolve(p);
58
+ if (abs === normalizedFrom) return true;
59
+ const withExt = path.extname(abs) ? abs : abs + path.extname(normalizedFrom);
60
+ return path.resolve(withExt) === normalizedFrom;
61
+ };
62
+
63
+ if (normalizedFrom && (matchesFrom(candidate) || (resolved && matchesFrom(resolved)))) {
64
+ return to;
65
+ }
66
+
67
+ return resolved ?? resolveWith(request, options);
68
+ };
69
+ `;
70
+ fs.writeFileSync(resolverPath, resolverCode, 'utf8');
71
+ return resolverPath;
72
+ }
73
+ async function loadRunCLI(requireFromCwd) {
74
+ try {
75
+ return requireFromCwd('@jest/core');
76
+ }
77
+ catch {
78
+ return import('@jest/core');
79
+ }
80
+ }
81
+ export class JestWorkerRuntime {
82
+ constructor(options) {
83
+ this.options = options;
84
+ this.resolverPath = null;
85
+ this.requireFromCwd = createRequire(path.join(options.cwd, 'package.json'));
86
+ }
87
+ async init() {
88
+ this.resolverPath = createResolverModule(this.options.workerId, this.requireFromCwd, this.options.cwd);
89
+ }
90
+ async shutdown() {
91
+ if (this.resolverPath) {
92
+ try {
93
+ fs.rmSync(this.resolverPath, { force: true });
94
+ }
95
+ catch {
96
+ // ignore cleanup errors
97
+ }
98
+ this.resolverPath = null;
99
+ }
100
+ }
101
+ async run(mutant, tests) {
102
+ const start = Date.now();
103
+ if (!this.resolverPath) {
104
+ throw new Error('Resolver not initialized');
105
+ }
106
+ try {
107
+ const mutantPath = getMutantFilePath(mutant.file, mutant.id);
108
+ fs.writeFileSync(mutantPath, mutant.code, 'utf8');
109
+ const redirectFrom = path.resolve(mutant.file);
110
+ globalThis.__mutineer_redirect__ = { from: redirectFrom, to: mutantPath };
111
+ process.env.MUTINEER_REDIRECT_FROM = redirectFrom;
112
+ process.env.MUTINEER_REDIRECT_TO = mutantPath;
113
+ const cliOptions = {
114
+ _: [...tests],
115
+ $0: 'mutineer',
116
+ runInBand: true,
117
+ runTestsByPath: true,
118
+ testPathPattern: [...tests],
119
+ watch: false,
120
+ passWithNoTests: true,
121
+ resolver: this.resolverPath,
122
+ silent: true,
123
+ };
124
+ if (this.options.jestConfigPath) {
125
+ cliOptions.config = this.options.jestConfigPath;
126
+ }
127
+ const { runCLI } = await loadRunCLI(this.requireFromCwd);
128
+ const { results } = await runCLI(cliOptions, [this.options.cwd]);
129
+ const killed = !results.success;
130
+ const failureMessages = results.testResults
131
+ ?.map((r) => r.failureMessage)
132
+ .filter(Boolean)
133
+ .join('\n');
134
+ if (this.options.debug) {
135
+ console.error(`[jest-worker:${this.options.workerId}] runCLI success=${results.success} tests=${results.numTotalTests ?? 'n/a'}`);
136
+ }
137
+ return {
138
+ killed,
139
+ durationMs: Date.now() - start,
140
+ error: failureMessages || undefined,
141
+ };
142
+ }
143
+ catch (err) {
144
+ if (this.options.debug) {
145
+ console.error(`[jest-worker:${this.options.workerId}] runCLI error`, err);
146
+ }
147
+ return {
148
+ killed: true,
149
+ durationMs: Date.now() - start,
150
+ error: err instanceof Error ? err.message : String(err),
151
+ };
152
+ }
153
+ finally {
154
+ const redirect = globalThis.__mutineer_redirect__;
155
+ globalThis.__mutineer_redirect__ = { from: null, to: null };
156
+ delete process.env.MUTINEER_REDIRECT_FROM;
157
+ delete process.env.MUTINEER_REDIRECT_TO;
158
+ try {
159
+ if (redirect?.to) {
160
+ fs.rmSync(redirect.to, { force: true });
161
+ }
162
+ }
163
+ catch {
164
+ // ignore
165
+ }
166
+ }
167
+ }
168
+ }
169
+ export function createJestWorkerRuntime(options) {
170
+ return new JestWorkerRuntime(options);
171
+ }
@@ -0,0 +1,37 @@
1
+ import type { MutantPayload } from './types.js';
2
+ import type { MutantRunSummary } from '../../types/mutant.js';
3
+ declare global {
4
+ var __mutineer_redirect__: {
5
+ from: string | null;
6
+ to: string | null;
7
+ } | undefined;
8
+ }
9
+ interface JestTestResult {
10
+ failureMessage?: string;
11
+ }
12
+ interface JestAggregatedResult {
13
+ success: boolean;
14
+ numTotalTests?: number;
15
+ testResults?: JestTestResult[];
16
+ }
17
+ export type JestRunCLI = (argv: Record<string, unknown>, projects: string[]) => Promise<{
18
+ results: JestAggregatedResult;
19
+ globalConfig: unknown;
20
+ }>;
21
+ export interface JestWorkerRuntimeOptions {
22
+ workerId: string;
23
+ cwd: string;
24
+ jestConfigPath?: string;
25
+ debug?: boolean;
26
+ }
27
+ export declare class JestWorkerRuntime {
28
+ private readonly options;
29
+ private resolverPath;
30
+ private readonly requireFromCwd;
31
+ constructor(options: JestWorkerRuntimeOptions);
32
+ init(): Promise<void>;
33
+ shutdown(): Promise<void>;
34
+ run(mutant: MutantPayload, tests: string[]): Promise<MutantRunSummary>;
35
+ }
36
+ export declare function createJestWorkerRuntime(options: JestWorkerRuntimeOptions): JestWorkerRuntime;
37
+ export {};
@@ -0,0 +1,171 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import { createRequire } from 'node:module';
5
+ globalThis.__mutineer_redirect__ = { from: null, to: null };
6
+ function getMutantFilePath(originalFile, mutantId) {
7
+ const dir = path.dirname(originalFile);
8
+ const ext = path.extname(originalFile);
9
+ const basename = path.basename(originalFile, ext);
10
+ const mutineerDir = path.join(dir, '__mutineer__');
11
+ if (!fs.existsSync(mutineerDir)) {
12
+ fs.mkdirSync(mutineerDir, { recursive: true });
13
+ }
14
+ const idMatch = mutantId.match(/#(\d+)$/);
15
+ const suffix = idMatch ? idMatch[1] : mutantId.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 20);
16
+ return path.join(mutineerDir, `${basename}_${suffix}${ext}`);
17
+ }
18
+ function resolveDefaultResolver(cwdRequire) {
19
+ try {
20
+ cwdRequire('jest-resolve');
21
+ return 'jest-resolve';
22
+ }
23
+ catch {
24
+ return 'jest-resolve';
25
+ }
26
+ }
27
+ function createResolverModule(workerId, requireFromCwd, projectCwd) {
28
+ const resolverPath = path.join(os.tmpdir(), `mutineer-jest-resolver-${workerId}.cjs`);
29
+ // Resolve defaultResolver relative to the project so we don't require jest-resolve as a global dep
30
+ const defaultResolverPath = resolveDefaultResolver(requireFromCwd);
31
+ const projectPkgPath = path.join(projectCwd, 'package.json');
32
+ const resolverCode = `
33
+ const path = require('path');
34
+ const { createRequire } = require('module');
35
+ const projectRequire = createRequire(${JSON.stringify(projectPkgPath)});
36
+ const defaultResolverModule = projectRequire(${JSON.stringify(defaultResolverPath)});
37
+ const defaultResolver = defaultResolverModule.default || defaultResolverModule;
38
+
39
+ module.exports = (request, options) => {
40
+ const resolveWith = (req, opts) => {
41
+ const baseOpts = { ...opts, paths: [${JSON.stringify(projectCwd + '/node_modules')}] };
42
+ if (options?.defaultResolver) return options.defaultResolver(req, baseOpts);
43
+ return defaultResolver(req, baseOpts);
44
+ };
45
+
46
+ const from = process.env.MUTINEER_REDIRECT_FROM;
47
+ const to = process.env.MUTINEER_REDIRECT_TO;
48
+ const normalizedFrom = from ? path.resolve(from) : null;
49
+
50
+ const resolved = (() => {
51
+ try { return resolveWith(request, options); } catch { return null; }
52
+ })();
53
+ const baseDir = options?.basedir ?? process.cwd();
54
+ const candidate = path.resolve(baseDir, request);
55
+ const matchesFrom = (p) => {
56
+ if (!normalizedFrom) return false;
57
+ const abs = path.resolve(p);
58
+ if (abs === normalizedFrom) return true;
59
+ const withExt = path.extname(abs) ? abs : abs + path.extname(normalizedFrom);
60
+ return path.resolve(withExt) === normalizedFrom;
61
+ };
62
+
63
+ if (normalizedFrom && (matchesFrom(candidate) || (resolved && matchesFrom(resolved)))) {
64
+ return to;
65
+ }
66
+
67
+ return resolved ?? resolveWith(request, options);
68
+ };
69
+ `;
70
+ fs.writeFileSync(resolverPath, resolverCode, 'utf8');
71
+ return resolverPath;
72
+ }
73
+ async function loadRunCLI(requireFromCwd) {
74
+ try {
75
+ return requireFromCwd('@jest/core');
76
+ }
77
+ catch {
78
+ return import('@jest/core');
79
+ }
80
+ }
81
+ export class JestWorkerRuntime {
82
+ constructor(options) {
83
+ this.options = options;
84
+ this.resolverPath = null;
85
+ this.requireFromCwd = createRequire(path.join(options.cwd, 'package.json'));
86
+ }
87
+ async init() {
88
+ this.resolverPath = createResolverModule(this.options.workerId, this.requireFromCwd, this.options.cwd);
89
+ }
90
+ async shutdown() {
91
+ if (this.resolverPath) {
92
+ try {
93
+ fs.rmSync(this.resolverPath, { force: true });
94
+ }
95
+ catch {
96
+ // ignore cleanup errors
97
+ }
98
+ this.resolverPath = null;
99
+ }
100
+ }
101
+ async run(mutant, tests) {
102
+ const start = Date.now();
103
+ if (!this.resolverPath) {
104
+ throw new Error('Resolver not initialized');
105
+ }
106
+ try {
107
+ const mutantPath = getMutantFilePath(mutant.file, mutant.id);
108
+ fs.writeFileSync(mutantPath, mutant.code, 'utf8');
109
+ const redirectFrom = path.resolve(mutant.file);
110
+ globalThis.__mutineer_redirect__ = { from: redirectFrom, to: mutantPath };
111
+ process.env.MUTINEER_REDIRECT_FROM = redirectFrom;
112
+ process.env.MUTINEER_REDIRECT_TO = mutantPath;
113
+ const cliOptions = {
114
+ _: [...tests],
115
+ $0: 'mutineer',
116
+ runInBand: true,
117
+ runTestsByPath: true,
118
+ testPathPattern: [...tests],
119
+ watch: false,
120
+ passWithNoTests: true,
121
+ resolver: this.resolverPath,
122
+ silent: true,
123
+ };
124
+ if (this.options.jestConfigPath) {
125
+ cliOptions.config = this.options.jestConfigPath;
126
+ }
127
+ const { runCLI } = await loadRunCLI(this.requireFromCwd);
128
+ const { results } = await runCLI(cliOptions, [this.options.cwd]);
129
+ const killed = !results.success;
130
+ const failureMessages = results.testResults
131
+ ?.map((r) => r.failureMessage)
132
+ .filter(Boolean)
133
+ .join('\n');
134
+ if (this.options.debug) {
135
+ console.error(`[jest-worker:${this.options.workerId}] runCLI success=${results.success} tests=${results.numTotalTests ?? 'n/a'}`);
136
+ }
137
+ return {
138
+ killed,
139
+ durationMs: Date.now() - start,
140
+ error: failureMessages || undefined,
141
+ };
142
+ }
143
+ catch (err) {
144
+ if (this.options.debug) {
145
+ console.error(`[jest-worker:${this.options.workerId}] runCLI error`, err);
146
+ }
147
+ return {
148
+ killed: true,
149
+ durationMs: Date.now() - start,
150
+ error: err instanceof Error ? err.message : String(err),
151
+ };
152
+ }
153
+ finally {
154
+ const redirect = globalThis.__mutineer_redirect__;
155
+ globalThis.__mutineer_redirect__ = { from: null, to: null };
156
+ delete process.env.MUTINEER_REDIRECT_FROM;
157
+ delete process.env.MUTINEER_REDIRECT_TO;
158
+ try {
159
+ if (redirect?.to) {
160
+ fs.rmSync(redirect.to, { force: true });
161
+ }
162
+ }
163
+ catch {
164
+ // ignore
165
+ }
166
+ }
167
+ }
168
+ }
169
+ export function createJestWorkerRuntime(options) {
170
+ return new JestWorkerRuntime(options);
171
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Jest Test Runner Adapter
3
+ *
4
+ * Implements the TestRunnerAdapter interface for Jest using runCLI.
5
+ * Baseline runs are executed directly via runCLI; mutant runs are delegated
6
+ * to a pool of long-lived worker processes that also use runCLI with a
7
+ * redirect resolver to swap in mutated code.
8
+ */
9
+ import type { TestRunnerAdapter, TestRunnerAdapterOptions, MutantPayload, MutantRunResult, BaselineOptions, CoverageConfig } from './types.js';
10
+ export declare class JestAdapter implements TestRunnerAdapter {
11
+ readonly name = "jest";
12
+ private readonly options;
13
+ private jestConfigPath?;
14
+ private pool;
15
+ private readonly requireFromCwd;
16
+ constructor(options: TestRunnerAdapterOptions);
17
+ init(concurrencyOverride?: number): Promise<void>;
18
+ runBaseline(tests: readonly string[], options: BaselineOptions): Promise<boolean>;
19
+ runMutant(mutant: MutantPayload, tests: readonly string[]): Promise<MutantRunResult>;
20
+ shutdown(): Promise<void>;
21
+ hasCoverageProvider(): boolean;
22
+ detectCoverageConfig(): Promise<CoverageConfig>;
23
+ }
24
+ export declare function createJestAdapter(options: TestRunnerAdapterOptions): JestAdapter;