@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,105 @@
1
+ import { createVitest } from 'vitest/node';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { poolMutineerPlugin } from './plugin.js';
5
+ import { getMutantFilePath, setRedirect, clearRedirect, } from '../shared/index.js';
6
+ import { createLogger } from '../../utils/logger.js';
7
+ const log = createLogger('vitest-runtime');
8
+ export class VitestWorkerRuntime {
9
+ constructor(options) {
10
+ this.options = options;
11
+ this.vitest = null;
12
+ }
13
+ async init() {
14
+ try {
15
+ this.vitest = await createVitest('test', {
16
+ watch: true,
17
+ reporters: ['dot'],
18
+ silent: true,
19
+ pool: 'forks',
20
+ bail: 1,
21
+ ...(this.options.vitestConfigPath
22
+ ? { config: this.options.vitestConfigPath }
23
+ : {}),
24
+ }, {
25
+ plugins: [poolMutineerPlugin()],
26
+ });
27
+ await this.vitest.init();
28
+ log.debug(`Vitest initialized for worker ${this.options.workerId}`);
29
+ }
30
+ catch (err) {
31
+ log.error(`Failed to initialize Vitest: ${err}`);
32
+ throw err;
33
+ }
34
+ }
35
+ async shutdown() {
36
+ if (!this.vitest)
37
+ return;
38
+ await this.vitest.close();
39
+ this.vitest = null;
40
+ }
41
+ async run(mutant, tests) {
42
+ if (!this.vitest) {
43
+ throw new Error('Vitest runtime not initialized');
44
+ }
45
+ const start = Date.now();
46
+ try {
47
+ const mutantPath = getMutantFilePath(mutant.file, mutant.id);
48
+ fs.writeFileSync(mutantPath, mutant.code, 'utf8');
49
+ log.debug(`Wrote mutant to ${mutantPath}`);
50
+ setRedirect({
51
+ from: path.resolve(mutant.file),
52
+ to: mutantPath,
53
+ });
54
+ this.vitest.invalidateFile(mutant.file);
55
+ log.debug(`Invalidated ${mutant.file}`);
56
+ const specs = [];
57
+ for (const testFile of tests) {
58
+ const spec = this.vitest
59
+ .getProjectByName('')
60
+ ?.createSpecification(testFile);
61
+ if (spec)
62
+ specs.push(spec);
63
+ }
64
+ if (specs.length === 0) {
65
+ return {
66
+ killed: false,
67
+ durationMs: Date.now() - start,
68
+ };
69
+ }
70
+ log.debug(`Running ${specs.length} test specs`);
71
+ const results = await this.vitest.runTestSpecifications(specs);
72
+ const requestedModules = new Set(specs.map((s) => s.moduleId));
73
+ const relevantModules = results.testModules.filter((mod) => requestedModules.has(mod.moduleId));
74
+ const modulesForDecision = relevantModules.length
75
+ ? relevantModules
76
+ : results.testModules;
77
+ const killed = modulesForDecision.some((mod) => !mod.ok());
78
+ return {
79
+ killed,
80
+ durationMs: Date.now() - start,
81
+ };
82
+ }
83
+ catch (err) {
84
+ return {
85
+ killed: true,
86
+ durationMs: Date.now() - start,
87
+ error: err instanceof Error ? err.message : String(err),
88
+ };
89
+ }
90
+ finally {
91
+ // Clear redirect and clean up temp file
92
+ const mutantPath = getMutantFilePath(mutant.file, mutant.id);
93
+ clearRedirect();
94
+ try {
95
+ fs.rmSync(mutantPath, { force: true });
96
+ }
97
+ catch {
98
+ // ignore
99
+ }
100
+ }
101
+ }
102
+ }
103
+ export function createVitestWorkerRuntime(options) {
104
+ return new VitestWorkerRuntime(options);
105
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Persistent Vitest worker process.
3
+ *
4
+ * This worker stays alive and receives mutation tasks via stdin,
5
+ * using Vitest's programmatic API to rerun tests without process restart.
6
+ *
7
+ * Communication protocol (JSON-RPC over stdin/stdout):
8
+ *
9
+ * Request: { "type": "run", "mutant": { file, code, id, name }, "tests": string[] }
10
+ * Response: { "type": "result", "killed": boolean, "durationMs": number }
11
+ *
12
+ * Request: { "type": "shutdown" }
13
+ * Response: { "type": "shutdown", "ok": true }
14
+ */
15
+ export {};
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Persistent Vitest worker process.
3
+ *
4
+ * This worker stays alive and receives mutation tasks via stdin,
5
+ * using Vitest's programmatic API to rerun tests without process restart.
6
+ *
7
+ * Communication protocol (JSON-RPC over stdin/stdout):
8
+ *
9
+ * Request: { "type": "run", "mutant": { file, code, id, name }, "tests": string[] }
10
+ * Response: { "type": "result", "killed": boolean, "durationMs": number }
11
+ *
12
+ * Request: { "type": "shutdown" }
13
+ * Response: { "type": "shutdown", "ok": true }
14
+ */
15
+ import * as readline from 'node:readline';
16
+ import { createVitestWorkerRuntime } from './worker-runtime.js';
17
+ import { createLogger } from '../../utils/logger.js';
18
+ const log = createLogger('vitest-worker');
19
+ // Global state for redirect - shared with the plugin via globalThis
20
+ // Type is declared in pool-plugin.ts
21
+ globalThis.__mutineer_redirect__ = { from: null, to: null };
22
+ function send(response) {
23
+ console.log(JSON.stringify(response));
24
+ }
25
+ async function main() {
26
+ const workerId = process.env.MUTINEER_WORKER_ID ?? 'unknown';
27
+ const cwd = process.env.MUTINEER_CWD ?? process.cwd();
28
+ const vitestConfigPath = process.env.MUTINEER_VITEST_CONFIG;
29
+ log.debug(`Starting worker ${workerId} in ${cwd}`);
30
+ const runtime = createVitestWorkerRuntime({
31
+ workerId,
32
+ cwd,
33
+ vitestConfigPath,
34
+ });
35
+ try {
36
+ await runtime.init();
37
+ }
38
+ catch (err) {
39
+ log.error(`Failed to initialize Vitest: ${err}`);
40
+ process.exit(1);
41
+ }
42
+ // Signal ready
43
+ send({ type: 'ready', workerId });
44
+ // Process requests from stdin
45
+ const rl = readline.createInterface({
46
+ input: process.stdin,
47
+ terminal: false,
48
+ });
49
+ for await (const line of rl) {
50
+ if (!line.trim())
51
+ continue;
52
+ let request;
53
+ try {
54
+ request = JSON.parse(line);
55
+ }
56
+ catch (err) {
57
+ log.debug(`Invalid JSON: ${line}`);
58
+ continue;
59
+ }
60
+ if (request.type === 'shutdown') {
61
+ log.debug('Shutting down');
62
+ await runtime.shutdown();
63
+ send({ type: 'shutdown', ok: true });
64
+ process.exit(0);
65
+ }
66
+ if (request.type === 'run') {
67
+ try {
68
+ const { mutant, tests } = request;
69
+ const result = await runtime.run(mutant, tests);
70
+ send({
71
+ type: 'result',
72
+ killed: result.killed,
73
+ durationMs: result.durationMs,
74
+ error: result.error,
75
+ });
76
+ }
77
+ catch (err) {
78
+ // On error, treat as killed (conservative)
79
+ send({
80
+ type: 'result',
81
+ killed: true,
82
+ durationMs: 0,
83
+ error: String(err),
84
+ });
85
+ }
86
+ }
87
+ }
88
+ }
89
+ main().catch((err) => {
90
+ log.error(`Fatal error: ${err}`);
91
+ process.exit(1);
92
+ });
@@ -0,0 +1,20 @@
1
+ import type { MutineerConfig } from './config.js';
2
+ import type { MutantResult } from './mutant.js';
3
+ export type { MutantCacheEntry, MutantResult, MutantStatus } from './mutant.js';
4
+ export type DiffLineType = 'context' | 'add' | 'remove';
5
+ export interface DiffLine {
6
+ readonly type: DiffLineType;
7
+ readonly leftNumber?: number;
8
+ readonly rightNumber?: number;
9
+ readonly text: string;
10
+ }
11
+ export interface DiffResponse {
12
+ readonly lines: readonly DiffLine[];
13
+ }
14
+ export interface ApiResponse {
15
+ readonly mutants: readonly MutantResult[];
16
+ }
17
+ export interface ConfigResponse {
18
+ readonly config?: MutineerConfig;
19
+ readonly error?: string;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ export interface MutateTargetObject {
2
+ readonly file: string;
3
+ /** Auto-detected from file extension if omitted (.vue = 'vue:script-setup', else 'module') */
4
+ readonly kind?: 'vue:script-setup' | 'module';
5
+ }
6
+ /** Target can be a simple path string or an object with file and optional kind */
7
+ export type MutateTarget = string | MutateTargetObject;
8
+ export interface MutineerConfig {
9
+ readonly targets?: readonly MutateTarget[];
10
+ /** Mutator names to include (e.g., ['flipStrictEQ', 'andToOr']) */
11
+ readonly include?: readonly string[];
12
+ /** Mutator names to exclude (e.g., ['relaxGE']) */
13
+ readonly exclude?: readonly string[];
14
+ /** Glob patterns for paths to exclude from mutation (e.g., ['admin/**']) */
15
+ readonly excludePaths?: readonly string[];
16
+ readonly maxMutantsPerFile?: number;
17
+ readonly source?: string | readonly string[];
18
+ readonly baseRef?: string;
19
+ readonly testPatterns?: readonly string[];
20
+ readonly extensions?: readonly string[];
21
+ readonly autoDiscover?: boolean;
22
+ /**
23
+ * Control how Vitest output is handled for mutant runs:
24
+ * - 'mute' (default) suppresses all output
25
+ * - 'minimal' echoes only pass/fail summaries
26
+ * - 'inherit' streams full Vitest output to the CLI
27
+ */
28
+ readonly mutantOutput?: 'mute' | 'minimal' | 'inherit';
29
+ readonly minKillPercent?: number;
30
+ /** Preferred test runner (defaults to vitest) */
31
+ readonly runner?: 'vitest' | 'jest';
32
+ readonly vitestConfig?: string;
33
+ readonly jestConfig?: string;
34
+ /** Max depth for dependency resolution with --changed-with-deps (default: 1) */
35
+ readonly dependencyDepth?: number;
36
+ /** Path to coverage JSON file (Istanbul format, e.g., coverage/coverage-final.json) */
37
+ readonly coverageFile?: string;
38
+ /** Only mutate lines that are covered by tests (requires coverageFile) */
39
+ readonly onlyCoveredLines?: boolean;
40
+ /** Request a coverage-instrumented baseline run (implies per-test coverage if available) */
41
+ readonly coverage?: boolean;
42
+ /**
43
+ * Enable per-test coverage collection during the baseline run.
44
+ * When enabled, Mutineer will try to run only the tests that actually cover a mutated line.
45
+ * Requires Vitest coverage with perTest support.
46
+ */
47
+ readonly perTestCoverage?: boolean;
48
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Centralised type definitions.
3
+ *
4
+ * Re-exports all public types from a single entry point for easier imports
5
+ * and better tree-shaking.
6
+ *
7
+ * Usage:
8
+ * import type { MutineerConfig, MutantStatus } from '../types/index.js'
9
+ */
10
+ export type { MutineerConfig, MutateTarget } from './config.js';
11
+ export type { MutantStatus, MutantRunStatus, MutantCacheEntry, MutantResult, MutantRunSummary, MutantRunResult, MutantPayload, MutantDescriptor, MutantLocation, Variant, } from './mutant.js';
12
+ export type { MutationVariant } from '../core/types.js';
13
+ export { defineMutineerConfig } from '../index.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Centralised type definitions.
3
+ *
4
+ * Re-exports all public types from a single entry point for easier imports
5
+ * and better tree-shaking.
6
+ *
7
+ * Usage:
8
+ * import type { MutineerConfig, MutantStatus } from '../types/index.js'
9
+ */
10
+ // Re-export for convenience
11
+ export { defineMutineerConfig } from '../index.js';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared mutant-related type definitions.
3
+ *
4
+ * Centralises the shapes used across the runner and adapters so we
5
+ * don't duplicate unions or object shapes in multiple modules.
6
+ */
7
+ export type MutantStatus = 'killed' | 'escaped' | 'skipped' | 'error' | 'timeout';
8
+ export type MutantRunStatus = MutantStatus;
9
+ export interface MutantLocation {
10
+ readonly file: string;
11
+ readonly line: number;
12
+ readonly col: number;
13
+ }
14
+ export interface MutantDescriptor extends MutantLocation {
15
+ readonly id: string;
16
+ readonly name: string;
17
+ readonly code: string;
18
+ }
19
+ /** Payload passed to workers/pools for execution. */
20
+ export type MutantPayload = MutantDescriptor;
21
+ /** Variant with attached test files. */
22
+ export interface Variant extends MutantDescriptor {
23
+ readonly tests: readonly string[];
24
+ }
25
+ export interface MutantCacheEntry extends MutantLocation {
26
+ readonly status: MutantStatus;
27
+ readonly mutator: string;
28
+ }
29
+ export interface MutantResult extends MutantCacheEntry {
30
+ readonly id: string;
31
+ readonly relativePath: string;
32
+ }
33
+ /** Low-level execution result returned by a worker. */
34
+ export interface MutantRunSummary {
35
+ readonly killed: boolean;
36
+ readonly durationMs: number;
37
+ readonly error?: string;
38
+ }
39
+ /** Normalised result returned by adapters/orchestrator. */
40
+ export interface MutantRunResult {
41
+ readonly status: MutantRunStatus;
42
+ readonly durationMs: number;
43
+ readonly error?: string;
44
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared mutant-related type definitions.
3
+ *
4
+ * Centralises the shapes used across the runner and adapters so we
5
+ * don't duplicate unions or object shapes in multiple modules.
6
+ */
7
+ export {};
@@ -0,0 +1,5 @@
1
+ interface PoolSpinnerProps {
2
+ message: string;
3
+ }
4
+ export declare function PoolSpinner({ message }: PoolSpinnerProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ export function PoolSpinner({ message }) {
5
+ return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] }));
6
+ }
@@ -0,0 +1,11 @@
1
+ export interface ProgressBarProps {
2
+ total: number;
3
+ done: number;
4
+ killed: number;
5
+ escaped: number;
6
+ errors: number;
7
+ timeouts: number;
8
+ skipped: number;
9
+ width?: number;
10
+ }
11
+ export declare function ProgressBar({ total, done, killed, escaped, errors, timeouts, skipped, width, }: ProgressBarProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function ProgressBar({ total, done, killed, escaped, errors, timeouts, skipped, width = 40, }) {
4
+ const ratio = total === 0 ? 1 : Math.min(done / total, 1);
5
+ const filled = Math.round(ratio * width);
6
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
7
+ const pct = Math.round(ratio * 100);
8
+ return (_jsxs(Box, { children: [_jsxs(Text, { children: ["mutants ", done, "/", total, " [", bar, "] ", pct, "%", ' '] }), _jsxs(Text, { color: "green", children: ["killed=", killed] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "red", children: ["escaped=", escaped] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: ["errors=", errors] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: ["timeouts=", timeouts] }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: ["skipped=", skipped] })] }));
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { loadCoverageData, isLineCovered, getFileCoverageStats, loadPerTestCoverageData, } from '../coverage.js';
6
+ describe('coverage utilities', () => {
7
+ it('loads coverage data and reports covered lines', async () => {
8
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
9
+ const covPath = path.join(tmpDir, 'coverage-final.json');
10
+ const filePath = path.join(tmpDir, 'src', 'file.ts');
11
+ const data = {
12
+ [filePath]: {
13
+ path: filePath,
14
+ statementMap: {
15
+ '0': { start: { line: 1, column: 0 }, end: { line: 2, column: 0 } },
16
+ '1': { start: { line: 5, column: 0 }, end: { line: 5, column: 10 } },
17
+ },
18
+ s: { '0': 1, '1': 0 },
19
+ },
20
+ };
21
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
22
+ await fs.writeFile(covPath, JSON.stringify(data), 'utf8');
23
+ try {
24
+ const coverage = await loadCoverageData(covPath, tmpDir);
25
+ expect(isLineCovered(coverage, filePath, 1)).toBe(true);
26
+ expect(isLineCovered(coverage, filePath, 2)).toBe(true);
27
+ expect(isLineCovered(coverage, filePath, 3)).toBe(false);
28
+ expect(isLineCovered(coverage, filePath, 5)).toBe(false);
29
+ const stats = getFileCoverageStats(coverage, filePath);
30
+ expect(stats?.count).toBe(2);
31
+ expect(stats?.lines.has(1)).toBe(true);
32
+ }
33
+ finally {
34
+ await fs.rm(tmpDir, { recursive: true, force: true });
35
+ }
36
+ });
37
+ it('returns null stats for missing file', () => {
38
+ const coverage = { coveredLines: new Map() };
39
+ expect(getFileCoverageStats(coverage, '/nope.ts')).toBeNull();
40
+ });
41
+ it('loads per-test coverage data from various shapes', async () => {
42
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-coverage-'));
43
+ const reportsDir = path.join(tmpDir, 'coverage');
44
+ await fs.mkdir(reportsDir, { recursive: true });
45
+ const testFile = path.join(tmpDir, 'tests', 'foo.spec.ts');
46
+ const srcFile = path.join(tmpDir, 'src', 'foo.ts');
47
+ // Write per-test-coverage.json (format A)
48
+ const formatA = {
49
+ tests: {
50
+ [testFile]: {
51
+ files: {
52
+ [srcFile]: { lines: [1, 2, 3] },
53
+ },
54
+ },
55
+ },
56
+ };
57
+ await fs.writeFile(path.join(reportsDir, 'per-test-coverage.json'), JSON.stringify(formatA), 'utf8');
58
+ // Also drop a tmp file with format B to ensure fallback works when main file is absent
59
+ const tmpSub = path.join(reportsDir, 'tmp');
60
+ await fs.mkdir(tmpSub, { recursive: true });
61
+ const formatB = {
62
+ [testFile]: {
63
+ [srcFile]: [4, 5],
64
+ },
65
+ };
66
+ await fs.writeFile(path.join(tmpSub, 'extra.json'), JSON.stringify(formatB), 'utf8');
67
+ try {
68
+ const map = await loadPerTestCoverageData(reportsDir, tmpDir);
69
+ expect(map).not.toBeNull();
70
+ const perTest = map.get(testFile);
71
+ expect(perTest).toBeDefined();
72
+ const lines = perTest.get(srcFile);
73
+ expect(lines).toBeDefined();
74
+ // Format A lines present
75
+ expect(lines.has(1)).toBe(true);
76
+ expect(lines.has(3)).toBe(true);
77
+ // Fallback not used yet
78
+ expect(lines.has(4)).toBe(false);
79
+ // Remove primary file to force fallback loading from tmp/extra.json
80
+ await fs.rm(path.join(reportsDir, 'per-test-coverage.json'));
81
+ const fallback = await loadPerTestCoverageData(reportsDir, tmpDir);
82
+ expect(fallback).not.toBeNull();
83
+ const fallbackLines = fallback.get(testFile).get(srcFile);
84
+ expect(fallbackLines.has(4)).toBe(true);
85
+ expect(fallbackLines.has(5)).toBe(true);
86
+ }
87
+ finally {
88
+ await fs.rm(tmpDir, { recursive: true, force: true });
89
+ }
90
+ });
91
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { Progress } from '../progress.js';
3
+ describe('Progress', () => {
4
+ let originalConsole;
5
+ beforeEach(() => {
6
+ originalConsole = {
7
+ log: console.log,
8
+ info: console.info,
9
+ warn: console.warn,
10
+ error: console.error,
11
+ };
12
+ vi.restoreAllMocks();
13
+ });
14
+ afterEach(() => {
15
+ console.log = originalConsole.log;
16
+ console.info = originalConsole.info;
17
+ console.warn = originalConsole.warn;
18
+ console.error = originalConsole.error;
19
+ });
20
+ it('logs run/update/finish messages in list mode and tracks counts', () => {
21
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
22
+ const progress = new Progress(5, { mode: 'list' });
23
+ progress.start();
24
+ progress.update('killed');
25
+ progress.update('escaped');
26
+ progress.update('skipped');
27
+ progress.update('error');
28
+ progress.update('timeout');
29
+ progress.finish();
30
+ const logs = logSpy.mock.calls.map((args) => args.join(' '));
31
+ expect(logs[0]).toContain('running 5 mutants');
32
+ expect(logs.some((l) => l.includes('mutant 1/5 killed'))).toBe(true);
33
+ expect(logs.some((l) => l.includes('mutant 2/5 escaped'))).toBe(true);
34
+ expect(logs.some((l) => l.includes('mutant 3/5 skipped'))).toBe(true);
35
+ expect(logs.some((l) => l.includes('mutant 4/5 error'))).toBe(true);
36
+ expect(logs.some((l) => l.includes('mutant 5/5 timeout'))).toBe(true);
37
+ const lastLog = logs[logs.length - 1] ?? '';
38
+ expect(lastLog).toContain('killed=1');
39
+ expect(lastLog).toContain('errors=1');
40
+ expect(lastLog).toContain('timeouts=1');
41
+ });
42
+ it('is tolerant to finish before start and clamps negative totals', () => {
43
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
44
+ const progress = new Progress(-5, { mode: 'list' });
45
+ progress.finish(); // no-op
46
+ progress.start();
47
+ progress.finish();
48
+ expect(logSpy).toHaveBeenCalled();
49
+ });
50
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { computeSummary, printSummary, summarise } from '../summary.js';
3
+ /** Strip ANSI escape codes for clean text assertions */
4
+ const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
5
+ function makeEntry(overrides) {
6
+ return {
7
+ mutator: 'flip',
8
+ file: '/tmp/file.ts',
9
+ line: 1,
10
+ col: 1,
11
+ status: 'killed',
12
+ ...overrides,
13
+ };
14
+ }
15
+ describe('summary', () => {
16
+ it('computes totals and kill rate', () => {
17
+ const cache = {
18
+ a: makeEntry({ status: 'killed' }),
19
+ b: makeEntry({ status: 'escaped' }),
20
+ c: makeEntry({ status: 'skipped' }),
21
+ };
22
+ const s = computeSummary(cache);
23
+ expect(s).toEqual({
24
+ total: 3,
25
+ killed: 1,
26
+ escaped: 1,
27
+ skipped: 1,
28
+ evaluated: 2,
29
+ killRate: 50,
30
+ });
31
+ });
32
+ it('prints a friendly summary with sections', () => {
33
+ const cache = {
34
+ a: makeEntry({ status: 'killed', file: '/tmp/a.ts' }),
35
+ b: makeEntry({ status: 'escaped', file: '/tmp/b.ts', mutator: 'wrap' }),
36
+ };
37
+ const summary = computeSummary(cache);
38
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
39
+ printSummary(summary, cache, 1500);
40
+ const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
41
+ expect(lines.some((l) => l.includes('Killed Mutants'))).toBe(true);
42
+ expect(lines.some((l) => l.includes('Escaped Mutants'))).toBe(true);
43
+ expect(lines.some((l) => l.includes('Duration: 1.50s'))).toBe(true);
44
+ logSpy.mockRestore();
45
+ });
46
+ it('summarise returns summary and prints', () => {
47
+ const cache = { a: makeEntry({ status: 'killed' }) };
48
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
49
+ const s = summarise(cache);
50
+ expect(s.total).toBe(1);
51
+ expect(logSpy).toHaveBeenCalled();
52
+ logSpy.mockRestore();
53
+ });
54
+ });