@mutineerjs/mutineer 0.2.3 → 0.3.2

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 (162) hide show
  1. package/README.md +48 -42
  2. package/dist/bin/mutineer.js +0 -0
  3. package/dist/core/__tests__/module.spec.js +66 -3
  4. package/dist/core/__tests__/sfc.spec.js +76 -0
  5. package/dist/core/__tests__/variant-utils.spec.js +93 -0
  6. package/dist/mutators/__tests__/operator.spec.js +169 -0
  7. package/dist/mutators/__tests__/registry.spec.js +6 -0
  8. package/dist/mutators/__tests__/return-value.spec.js +239 -0
  9. package/dist/mutators/__tests__/utils.spec.js +68 -1
  10. package/dist/mutators/operator.d.ts +25 -0
  11. package/dist/mutators/operator.js +50 -0
  12. package/dist/mutators/registry.d.ts +6 -28
  13. package/dist/mutators/registry.js +14 -66
  14. package/dist/mutators/return-value.d.ts +39 -0
  15. package/dist/mutators/return-value.js +104 -0
  16. package/dist/mutators/utils.d.ts +21 -0
  17. package/dist/mutators/utils.js +44 -27
  18. package/dist/runner/__tests__/args.spec.js +225 -0
  19. package/dist/runner/__tests__/cache.spec.js +180 -0
  20. package/dist/runner/__tests__/changed.spec.js +227 -0
  21. package/dist/runner/__tests__/cleanup.spec.js +41 -0
  22. package/dist/runner/__tests__/config.spec.js +71 -0
  23. package/dist/runner/__tests__/coverage-resolver.spec.js +171 -0
  24. package/dist/runner/__tests__/pool-executor.spec.js +211 -0
  25. package/dist/runner/__tests__/tasks.spec.js +95 -0
  26. package/dist/runner/__tests__/variants.spec.js +261 -0
  27. package/dist/runner/args.d.ts +5 -0
  28. package/dist/runner/args.js +7 -0
  29. package/dist/runner/config.js +2 -2
  30. package/dist/runner/coverage-resolver.d.ts +21 -0
  31. package/dist/runner/coverage-resolver.js +96 -0
  32. package/dist/runner/discover.js +2 -1
  33. package/dist/runner/jest/__tests__/adapter.spec.js +1 -1
  34. package/dist/runner/jest/__tests__/pool.spec.d.ts +1 -0
  35. package/dist/runner/jest/__tests__/pool.spec.js +211 -0
  36. package/dist/runner/jest/__tests__/worker-runtime.spec.d.ts +1 -0
  37. package/dist/runner/jest/__tests__/worker-runtime.spec.js +148 -0
  38. package/dist/runner/jest/adapter.js +1 -1
  39. package/dist/runner/jest/pool.d.ts +1 -1
  40. package/dist/runner/jest/pool.js +6 -6
  41. package/dist/runner/jest/worker.mjs +1 -1
  42. package/dist/runner/orchestrator.js +43 -295
  43. package/dist/runner/pool-executor.d.ts +17 -0
  44. package/dist/runner/pool-executor.js +143 -0
  45. package/dist/runner/shared/__tests__/mutant-paths.spec.d.ts +1 -0
  46. package/dist/runner/shared/__tests__/mutant-paths.spec.js +66 -0
  47. package/dist/runner/shared/__tests__/redirect-state.spec.d.ts +1 -0
  48. package/dist/runner/shared/__tests__/redirect-state.spec.js +56 -0
  49. package/dist/runner/shared/index.d.ts +1 -1
  50. package/dist/runner/shared/index.js +1 -1
  51. package/dist/runner/shared/redirect-state.d.ts +2 -2
  52. package/dist/runner/shared/redirect-state.js +4 -4
  53. package/dist/runner/tasks.d.ts +12 -0
  54. package/dist/runner/tasks.js +25 -0
  55. package/dist/runner/types.d.ts +1 -1
  56. package/dist/runner/variants.d.ts +17 -2
  57. package/dist/runner/variants.js +33 -0
  58. package/dist/runner/vitest/__tests__/adapter.spec.js +1 -1
  59. package/dist/runner/vitest/__tests__/pool.spec.js +1 -1
  60. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -1
  61. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +84 -0
  62. package/dist/runner/vitest/adapter.js +1 -1
  63. package/dist/runner/vitest/index.d.ts +0 -1
  64. package/dist/runner/vitest/index.js +0 -1
  65. package/dist/runner/vitest/pool.d.ts +1 -1
  66. package/dist/runner/vitest/pool.js +7 -7
  67. package/dist/runner/vitest/redirect-loader.d.ts +1 -1
  68. package/dist/runner/vitest/redirect-loader.js +1 -1
  69. package/dist/runner/vitest/worker-runtime.js +3 -3
  70. package/dist/runner/vitest/worker.mjs +1 -1
  71. package/dist/utils/__tests__/coverage.spec.js +167 -0
  72. package/dist/utils/__tests__/logger.spec.d.ts +1 -0
  73. package/dist/utils/__tests__/logger.spec.js +61 -0
  74. package/dist/utils/__tests__/normalizePath.spec.d.ts +1 -0
  75. package/dist/utils/__tests__/normalizePath.spec.js +22 -0
  76. package/dist/utils/__tests__/progress.spec.js +96 -0
  77. package/package.json +71 -22
  78. package/dist/admin/assets/index-B7nXq-e7.js +0 -32
  79. package/dist/admin/assets/index-B7nXq-e7.js.map +0 -1
  80. package/dist/admin/assets/index-BDQLkBUE.js +0 -32
  81. package/dist/admin/assets/index-BDQLkBUE.js.map +0 -1
  82. package/dist/admin/assets/index-DVkP-Tc7.css +0 -1
  83. package/dist/admin/index.html +0 -13
  84. package/dist/admin/server/admin.d.ts +0 -6
  85. package/dist/admin/server/admin.js +0 -234
  86. package/dist/bin/mutate-vitest.d.ts +0 -2
  87. package/dist/bin/mutate-vitest.js +0 -90
  88. package/dist/plugin/viteMutate.d.ts +0 -15
  89. package/dist/plugin/viteMutate.js +0 -52
  90. package/dist/plugin/vitest.setup.d.ts +0 -47
  91. package/dist/plugin/vitest.setup.js +0 -118
  92. package/dist/plugin/withVitest.d.ts +0 -13
  93. package/dist/plugin/withVitest.js +0 -30
  94. package/dist/runner/__tests__/orchestrator.spec.js +0 -55
  95. package/dist/runner/adapters/__tests__/jest.spec.js +0 -88
  96. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +0 -59
  97. package/dist/runner/adapters/__tests__/vitest.spec.js +0 -118
  98. package/dist/runner/adapters/index.d.ts +0 -10
  99. package/dist/runner/adapters/index.js +0 -9
  100. package/dist/runner/adapters/jest/__tests__/index.spec.js +0 -88
  101. package/dist/runner/adapters/jest/index.d.ts +0 -24
  102. package/dist/runner/adapters/jest/index.js +0 -216
  103. package/dist/runner/adapters/jest/worker-runtime.d.ts +0 -37
  104. package/dist/runner/adapters/jest/worker-runtime.js +0 -171
  105. package/dist/runner/adapters/jest-worker-runtime.d.ts +0 -37
  106. package/dist/runner/adapters/jest-worker-runtime.js +0 -171
  107. package/dist/runner/adapters/jest.d.ts +0 -24
  108. package/dist/runner/adapters/jest.js +0 -216
  109. package/dist/runner/adapters/types.d.ts +0 -89
  110. package/dist/runner/adapters/types.js +0 -8
  111. package/dist/runner/adapters/vitest/__tests__/index.spec.js +0 -118
  112. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +0 -59
  113. package/dist/runner/adapters/vitest/index.d.ts +0 -33
  114. package/dist/runner/adapters/vitest/index.js +0 -267
  115. package/dist/runner/adapters/vitest/worker-runtime.d.ts +0 -25
  116. package/dist/runner/adapters/vitest/worker-runtime.js +0 -118
  117. package/dist/runner/adapters/vitest-worker-runtime.d.ts +0 -25
  118. package/dist/runner/adapters/vitest-worker-runtime.js +0 -118
  119. package/dist/runner/adapters/vitest.d.ts +0 -33
  120. package/dist/runner/adapters/vitest.js +0 -267
  121. package/dist/runner/pool/__tests__/index.spec.js +0 -83
  122. package/dist/runner/pool/__tests__/pool-plugin.spec.js +0 -59
  123. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +0 -78
  124. package/dist/runner/pool/index.d.ts +0 -8
  125. package/dist/runner/pool/index.js +0 -9
  126. package/dist/runner/pool/jest/pool.d.ts +0 -52
  127. package/dist/runner/pool/jest/pool.js +0 -309
  128. package/dist/runner/pool/jest/worker.mjs +0 -60
  129. package/dist/runner/pool/jest-pool.d.ts +0 -52
  130. package/dist/runner/pool/jest-pool.js +0 -309
  131. package/dist/runner/pool/jest-worker.mjs +0 -60
  132. package/dist/runner/pool/plugin.d.ts +0 -18
  133. package/dist/runner/pool/plugin.js +0 -60
  134. package/dist/runner/pool/pool-plugin.d.ts +0 -18
  135. package/dist/runner/pool/pool-plugin.js +0 -60
  136. package/dist/runner/pool/pool-redirect-loader.d.ts +0 -19
  137. package/dist/runner/pool/pool-redirect-loader.js +0 -116
  138. package/dist/runner/pool/pool-redirect-loader.mjs +0 -146
  139. package/dist/runner/pool/redirect-loader.d.ts +0 -19
  140. package/dist/runner/pool/redirect-loader.js +0 -116
  141. package/dist/runner/pool/vitest/pool.d.ts +0 -70
  142. package/dist/runner/pool/vitest/pool.js +0 -376
  143. package/dist/runner/pool/vitest/worker.d.mts +0 -15
  144. package/dist/runner/pool/vitest/worker.mjs +0 -96
  145. package/dist/runner/pool/vitest-worker.d.mts +0 -15
  146. package/dist/runner/pool/vitest-worker.mjs +0 -96
  147. package/dist/runner/shared-module-redirect.d.ts +0 -56
  148. package/dist/runner/shared-module-redirect.js +0 -84
  149. package/dist/types/api.d.ts +0 -20
  150. /package/dist/{runner/__tests__/orchestrator.spec.d.ts → core/__tests__/sfc.spec.d.ts} +0 -0
  151. /package/dist/{runner/adapters/__tests__/jest.spec.d.ts → core/__tests__/variant-utils.spec.d.ts} +0 -0
  152. /package/dist/{runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts → mutators/__tests__/operator.spec.d.ts} +0 -0
  153. /package/dist/{runner/adapters/__tests__/vitest.spec.d.ts → mutators/__tests__/return-value.spec.d.ts} +0 -0
  154. /package/dist/runner/{adapters/jest/__tests__/index.spec.d.ts → __tests__/args.spec.d.ts} +0 -0
  155. /package/dist/runner/{adapters/vitest/__tests__/index.spec.d.ts → __tests__/cache.spec.d.ts} +0 -0
  156. /package/dist/runner/{adapters/vitest/__tests__/worker-runtime.spec.d.ts → __tests__/changed.spec.d.ts} +0 -0
  157. /package/dist/runner/{pool/__tests__/index.spec.d.ts → __tests__/cleanup.spec.d.ts} +0 -0
  158. /package/dist/runner/{pool/__tests__/pool-plugin.spec.d.ts → __tests__/config.spec.d.ts} +0 -0
  159. /package/dist/runner/{pool/__tests__/pool-redirect-loader.spec.d.ts → __tests__/coverage-resolver.spec.d.ts} +0 -0
  160. /package/dist/runner/{pool/jest-worker.d.mts → __tests__/pool-executor.spec.d.ts} +0 -0
  161. /package/dist/runner/{pool/jest/worker.d.mts → __tests__/tasks.spec.d.ts} +0 -0
  162. /package/dist/{types/api.js → runner/__tests__/variants.spec.d.ts} +0 -0
package/README.md CHANGED
@@ -26,25 +26,31 @@ Mutations are applied using Babel AST analysis, so operators inside strings and
26
26
 
27
27
  ## Supported Mutations (WIP)
28
28
 
29
- | Category | Mutator | Transformation |
30
- | ---------- | --------------- | ------------------ |
31
- | Equality | `flipStrictEQ` | `===` → `!==` |
32
- | | `flipStrictNEQ` | `!==` → `===` |
33
- | | `flipEQ` | `==` → `!=` |
34
- | | `flipNEQ` | `!=` → `==` |
35
- | Boundary | `relaxLE` | `<=` &rarr; `<` |
36
- | | `relaxGE` | `>=` &rarr; `>` |
37
- | | `tightenLT` | `<` &rarr; `<=` |
38
- | | `tightenGT` | `>` &rarr; `>=` |
39
- | Logical | `andToOr` | `&&` &rarr; `\|\|` |
40
- | | `orToAnd` | `\|\|` &rarr; `&&` |
41
- | | `nullishToOr` | `??` &rarr; `\|\|` |
42
- | Arithmetic | `addToSub` | `+` &rarr; `-` |
43
- | | `subToAdd` | `-` &rarr; `+` |
44
- | | `mulToDiv` | `*` &rarr; `/` |
45
- | | `divToMul` | `/` &rarr; `*` |
46
- | | `modToMul` | `%` &rarr; `*` |
47
- | | `powerToMul` | `**` &rarr; `*` |
29
+ | Category | Mutator | Transformation |
30
+ | ------------ | ------------------- | ------------------------------------ |
31
+ | Equality | `flipStrictEQ` | `===` &rarr; `!==` |
32
+ | | `flipStrictNEQ` | `!==` &rarr; `===` |
33
+ | | `flipEQ` | `==` &rarr; `!=` |
34
+ | | `flipNEQ` | `!=` &rarr; `==` |
35
+ | Boundary | `relaxLE` | `<=` &rarr; `<` |
36
+ | | `relaxGE` | `>=` &rarr; `>` |
37
+ | | `tightenLT` | `<` &rarr; `<=` |
38
+ | | `tightenGT` | `>` &rarr; `>=` |
39
+ | Logical | `andToOr` | `&&` &rarr; `\|\|` |
40
+ | | `orToAnd` | `\|\|` &rarr; `&&` |
41
+ | | `nullishToOr` | `??` &rarr; `\|\|` |
42
+ | Arithmetic | `addToSub` | `+` &rarr; `-` |
43
+ | | `subToAdd` | `-` &rarr; `+` |
44
+ | | `mulToDiv` | `*` &rarr; `/` |
45
+ | | `divToMul` | `/` &rarr; `*` |
46
+ | | `modToMul` | `%` &rarr; `*` |
47
+ | | `powerToMul` | `**` &rarr; `*` |
48
+ | Return value | `returnToNull` | `return x` &rarr; `return null` |
49
+ | | `returnToUndefined` | `return x` &rarr; `return undefined` |
50
+ | | `returnFlipBool` | `return true` &harr; `return false` |
51
+ | | `returnZero` | `return n` &rarr; `return 0` |
52
+ | | `returnEmptyStr` | `return s` &rarr; `return ''` |
53
+ | | `returnEmptyArr` | `return [...]` &rarr; `return []` |
48
54
 
49
55
  ## Installation
50
56
 
@@ -56,11 +62,11 @@ npm i @mutineerjs/mutineer
56
62
 
57
63
  ### Commands
58
64
 
59
- | Command | Description |
60
- | ---------------- | ---------------------------------------------------- |
61
- | `mutineer init` | Create a `mutineer.config.ts` with minimal defaults |
62
- | `mutineer run` | Run mutation testing |
63
- | `mutineer clean` | Remove leftover `__mutineer__` temp directories |
65
+ | Command | Description |
66
+ | ---------------- | --------------------------------------------------- |
67
+ | `mutineer init` | Create a `mutineer.config.ts` with minimal defaults |
68
+ | `mutineer run` | Run mutation testing |
69
+ | `mutineer clean` | Remove leftover `__mutineer__` temp directories |
64
70
 
65
71
  ### Quick Start
66
72
 
@@ -93,7 +99,7 @@ npm run mutineer
93
99
  | ------------------------ | ------------------------------------------ | ------------- |
94
100
  | `--runner <type>` | Test runner: `vitest` or `jest` | `vitest` |
95
101
  | `--config`, `-c` | Path to config file | auto-detected |
96
- | `--concurrency <n>` | Parallel workers (1-4) | CPUs - 1 |
102
+ | `--concurrency <n>` | Parallel workers (min 1) | CPUs - 1 |
97
103
  | `--changed` | Only mutate files changed vs base branch | -- |
98
104
  | `--changed-with-deps` | Include dependents of changed files | -- |
99
105
  | `--only-covered-lines` | Skip mutations on uncovered lines | -- |
@@ -140,23 +146,23 @@ export default defineMutineerConfig({
140
146
 
141
147
  ### Config Options
142
148
 
143
- | Option | Type | Description |
144
- | ------------------- | ---------------------------------- | ------------------------------------------------ |
145
- | `source` | `string \| string[]` | Glob patterns for source files to mutate |
146
- | `targets` | `MutateTarget[]` | Explicit list of files to mutate |
147
- | `runner` | `'vitest' \| 'jest'` | Test runner to use |
148
- | `vitestConfig` | `string` | Path to vitest config |
149
- | `jestConfig` | `string` | Path to jest config |
150
- | `include` | `string[]` | Only run these mutators |
151
- | `exclude` | `string[]` | Skip these mutators |
152
- | `excludePaths` | `string[]` | Glob patterns for paths to skip |
153
- | `maxMutantsPerFile` | `number` | Cap mutations per file |
154
- | `minKillPercent` | `number` | Fail if kill rate is below this |
155
- | `onlyCoveredLines` | `boolean` | Only mutate lines covered by tests |
156
- | `perTestCoverage` | `boolean` | Use per-test coverage to select tests |
157
- | `baseRef` | `string` | Git ref for `--changed` (default: `origin/main`) |
158
- | `testPatterns` | `string[]` | Globs for test file discovery |
159
- | `extensions` | `string[]` | File extensions to consider |
149
+ | Option | Type | Description |
150
+ | ------------------- | -------------------- | ------------------------------------------------ |
151
+ | `source` | `string \| string[]` | Glob patterns for source files to mutate |
152
+ | `targets` | `MutateTarget[]` | Explicit list of files to mutate |
153
+ | `runner` | `'vitest' \| 'jest'` | Test runner to use |
154
+ | `vitestConfig` | `string` | Path to vitest config |
155
+ | `jestConfig` | `string` | Path to jest config |
156
+ | `include` | `string[]` | Only run these mutators |
157
+ | `exclude` | `string[]` | Skip these mutators |
158
+ | `excludePaths` | `string[]` | Glob patterns for paths to skip |
159
+ | `maxMutantsPerFile` | `number` | Cap mutations per file |
160
+ | `minKillPercent` | `number` | Fail if kill rate is below this |
161
+ | `onlyCoveredLines` | `boolean` | Only mutate lines covered by tests |
162
+ | `perTestCoverage` | `boolean` | Use per-test coverage to select tests |
163
+ | `baseRef` | `string` | Git ref for `--changed` (default: `origin/main`) |
164
+ | `testPatterns` | `string[]` | Globs for test file discovery |
165
+ | `extensions` | `string[]` | File extensions to consider |
160
166
 
161
167
  ## File Support
162
168
 
File without changes
@@ -1,6 +1,69 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- describe('Module', () => {
3
- it('should pass a basic test', () => {
4
- expect(true).toBe(true);
2
+ import { mutateModuleSource } from '../module.js';
3
+ describe('mutateModuleSource', () => {
4
+ it('returns empty array for code with no mutable patterns', () => {
5
+ const result = mutateModuleSource('const x = 1');
6
+ expect(result).toEqual([]);
7
+ });
8
+ it('generates mutations for equality operators', () => {
9
+ const code = 'if (a === b) {}';
10
+ const result = mutateModuleSource(code);
11
+ expect(result.length).toBeGreaterThan(0);
12
+ expect(result.some((v) => v.name === 'flipStrictEQ')).toBe(true);
13
+ });
14
+ it('generates mutations for logical operators', () => {
15
+ const code = 'const x = a && b';
16
+ const result = mutateModuleSource(code);
17
+ expect(result.some((v) => v.name === 'andToOr')).toBe(true);
18
+ });
19
+ it('generates mutations for arithmetic operators', () => {
20
+ const code = 'const x = a + b';
21
+ const result = mutateModuleSource(code);
22
+ expect(result.some((v) => v.name === 'addToSub')).toBe(true);
23
+ });
24
+ it('deduplicates identical mutations', () => {
25
+ const code = 'if (a === b) {}';
26
+ const result = mutateModuleSource(code);
27
+ const codes = result.map((v) => v.code);
28
+ expect(new Set(codes).size).toBe(codes.length);
29
+ });
30
+ it('respects include filter', () => {
31
+ const code = 'if (a === b && c || d) {}';
32
+ const result = mutateModuleSource(code, ['andToOr']);
33
+ expect(result.every((v) => v.name === 'andToOr')).toBe(true);
34
+ });
35
+ it('respects exclude filter', () => {
36
+ const code = 'if (a === b && c || d) {}';
37
+ const result = mutateModuleSource(code, undefined, ['flipStrictEQ']);
38
+ expect(result.every((v) => v.name !== 'flipStrictEQ')).toBe(true);
39
+ });
40
+ it('respects max limit', () => {
41
+ const code = 'if (a === b && c || d) {}';
42
+ const all = mutateModuleSource(code);
43
+ expect(all.length).toBeGreaterThan(1);
44
+ const limited = mutateModuleSource(code, undefined, undefined, 1);
45
+ expect(limited).toHaveLength(1);
46
+ });
47
+ it('throws when max is 0', () => {
48
+ expect(() => mutateModuleSource('code', undefined, undefined, 0)).toThrow('max must be a positive number');
49
+ });
50
+ it('throws when max is negative', () => {
51
+ expect(() => mutateModuleSource('code', undefined, undefined, -1)).toThrow('max must be a positive number');
52
+ });
53
+ it('includes line and col in variants', () => {
54
+ const code = 'const x = a + b';
55
+ const result = mutateModuleSource(code);
56
+ for (const v of result) {
57
+ expect(typeof v.line).toBe('number');
58
+ expect(typeof v.col).toBe('number');
59
+ }
60
+ });
61
+ it('produces valid mutated code strings', () => {
62
+ const code = 'const x = a <= b';
63
+ const result = mutateModuleSource(code);
64
+ for (const v of result) {
65
+ expect(typeof v.code).toBe('string');
66
+ expect(v.code).not.toBe(code);
67
+ }
5
68
  });
6
69
  });
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { mutateVueSfcScriptSetup } from '../sfc.js';
3
+ // Mock @vue/compiler-sfc
4
+ vi.mock('@vue/compiler-sfc', () => ({
5
+ parse: (code, _opts) => {
6
+ // Simple mock that extracts content between <script setup> tags
7
+ const startTag = '<script setup>';
8
+ const endTag = '</script>';
9
+ const startIdx = code.indexOf(startTag);
10
+ if (startIdx === -1) {
11
+ return { descriptor: { scriptSetup: null } };
12
+ }
13
+ const contentStart = startIdx + startTag.length;
14
+ const contentEnd = code.indexOf(endTag, contentStart);
15
+ return {
16
+ descriptor: {
17
+ scriptSetup: {
18
+ loc: {
19
+ start: { offset: contentStart },
20
+ end: { offset: contentEnd },
21
+ },
22
+ },
23
+ },
24
+ };
25
+ },
26
+ }));
27
+ describe('mutateVueSfcScriptSetup', () => {
28
+ it('returns empty array when there is no script setup block', async () => {
29
+ const code = '<template><div>hello</div></template>';
30
+ const result = await mutateVueSfcScriptSetup('test.vue', code);
31
+ expect(result).toEqual([]);
32
+ });
33
+ it('throws when max is 0', async () => {
34
+ await expect(mutateVueSfcScriptSetup('test.vue', '<script setup></script>', [], [], 0)).rejects.toThrow('max must be a positive number, got: 0');
35
+ });
36
+ it('throws when max is negative', async () => {
37
+ await expect(mutateVueSfcScriptSetup('test.vue', '<script setup></script>', [], [], -1)).rejects.toThrow('max must be a positive number, got: -1');
38
+ });
39
+ it('generates mutations for script setup content', async () => {
40
+ const code = '<script setup>\nconst x = a && b\n</script>';
41
+ const result = await mutateVueSfcScriptSetup('test.vue', code);
42
+ // Should find at least the andToOr mutation
43
+ expect(result.length).toBeGreaterThan(0);
44
+ // Every result should have the full SFC code (containing template tags)
45
+ for (const v of result) {
46
+ expect(v.code).toContain('<script setup>');
47
+ expect(v.code).toContain('</script>');
48
+ }
49
+ });
50
+ it('deduplicates identical mutations', async () => {
51
+ const code = '<script setup>\nconst x = a && b\n</script>';
52
+ const result = await mutateVueSfcScriptSetup('test.vue', code);
53
+ const outputs = result.map((v) => v.code);
54
+ const unique = new Set(outputs);
55
+ expect(outputs.length).toBe(unique.size);
56
+ });
57
+ it('respects max limit', async () => {
58
+ const code = '<script setup>\nconst x = a && b\nconst y = c || d\n</script>';
59
+ const result = await mutateVueSfcScriptSetup('test.vue', code, undefined, undefined, 1);
60
+ expect(result.length).toBeLessThanOrEqual(1);
61
+ });
62
+ it('filters mutators with include', async () => {
63
+ const code = '<script setup>\nconst x = a && b\n</script>';
64
+ const result = await mutateVueSfcScriptSetup('test.vue', code, ['andToOr']);
65
+ for (const v of result) {
66
+ expect(v.name).toBe('andToOr');
67
+ }
68
+ });
69
+ it('filters mutators with exclude', async () => {
70
+ const code = '<script setup>\nconst x = a && b\n</script>';
71
+ const result = await mutateVueSfcScriptSetup('test.vue', code, undefined, ['andToOr']);
72
+ for (const v of result) {
73
+ expect(v.name).not.toBe('andToOr');
74
+ }
75
+ });
76
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateMutationVariants, getFilteredRegistry } from '../variant-utils.js';
3
+ function makeMutator(name, mutations) {
4
+ return {
5
+ name,
6
+ description: name,
7
+ apply: () => mutations,
8
+ };
9
+ }
10
+ describe('generateMutationVariants', () => {
11
+ it('returns empty array for empty registry', () => {
12
+ expect(generateMutationVariants([], 'const x = 1')).toEqual([]);
13
+ });
14
+ it('generates variants from a single mutator', () => {
15
+ const mutator = makeMutator('flip', [
16
+ { code: 'const x = 2', line: 1, col: 0 },
17
+ ]);
18
+ const result = generateMutationVariants([mutator], 'const x = 1');
19
+ expect(result).toEqual([
20
+ { name: 'flip', code: 'const x = 2', line: 1, col: 0 },
21
+ ]);
22
+ });
23
+ it('generates variants from multiple mutators', () => {
24
+ const m1 = makeMutator('a', [{ code: 'code_a', line: 1, col: 0 }]);
25
+ const m2 = makeMutator('b', [{ code: 'code_b', line: 2, col: 5 }]);
26
+ const result = generateMutationVariants([m1, m2], 'original');
27
+ expect(result).toHaveLength(2);
28
+ expect(result[0].name).toBe('a');
29
+ expect(result[1].name).toBe('b');
30
+ });
31
+ it('deduplicates identical mutation outputs', () => {
32
+ const m1 = makeMutator('a', [{ code: 'same', line: 1, col: 0 }]);
33
+ const m2 = makeMutator('b', [{ code: 'same', line: 2, col: 0 }]);
34
+ const result = generateMutationVariants([m1, m2], 'original');
35
+ expect(result).toHaveLength(1);
36
+ expect(result[0].name).toBe('a');
37
+ });
38
+ it('throws when max is 0', () => {
39
+ expect(() => generateMutationVariants([], 'code', { max: 0 })).toThrow('max must be a positive number, got: 0');
40
+ });
41
+ it('throws when max is negative', () => {
42
+ expect(() => generateMutationVariants([], 'code', { max: -5 })).toThrow('max must be a positive number, got: -5');
43
+ });
44
+ it('respects max limit', () => {
45
+ const mutator = makeMutator('m', [
46
+ { code: 'v1', line: 1, col: 0 },
47
+ { code: 'v2', line: 2, col: 0 },
48
+ { code: 'v3', line: 3, col: 0 },
49
+ ]);
50
+ const result = generateMutationVariants([mutator], 'code', { max: 2 });
51
+ expect(result).toHaveLength(2);
52
+ });
53
+ it('stops iterating mutators once max is reached via inner return', () => {
54
+ const m1 = makeMutator('a', [
55
+ { code: 'v1', line: 1, col: 0 },
56
+ { code: 'v2', line: 2, col: 0 },
57
+ ]);
58
+ const m2 = makeMutator('b', [{ code: 'v3', line: 3, col: 0 }]);
59
+ const result = generateMutationVariants([m1, m2], 'code', { max: 2 });
60
+ expect(result).toHaveLength(2);
61
+ expect(result.every((v) => v.name === 'a')).toBe(true);
62
+ });
63
+ // NOTE: The outer loop `break` (variant-utils.ts line 42) is dead code.
64
+ // The inner loop always `return`s immediately when max is reached after adding
65
+ // a new unique variant, so the outer loop check can never fire.
66
+ // Same applies to sfc.ts line 43.
67
+ it('works with undefined max', () => {
68
+ const mutator = makeMutator('m', [
69
+ { code: 'v1', line: 1, col: 0 },
70
+ { code: 'v2', line: 2, col: 0 },
71
+ ]);
72
+ const result = generateMutationVariants([mutator], 'code', {});
73
+ expect(result).toHaveLength(2);
74
+ });
75
+ });
76
+ describe('getFilteredRegistry', () => {
77
+ it('returns the full registry when no filters', () => {
78
+ const result = getFilteredRegistry();
79
+ expect(result.length).toBeGreaterThan(0);
80
+ expect(result[0]).toHaveProperty('name');
81
+ expect(result[0]).toHaveProperty('apply');
82
+ });
83
+ it('filters by include', () => {
84
+ const result = getFilteredRegistry(['flipEQ', 'andToOr']);
85
+ expect(result.map((r) => r.name)).toEqual(['andToOr', 'flipEQ']);
86
+ });
87
+ it('filters by exclude', () => {
88
+ const all = getFilteredRegistry();
89
+ const result = getFilteredRegistry(undefined, ['flipEQ']);
90
+ expect(result.length).toBe(all.length - 1);
91
+ expect(result.find((r) => r.name === 'flipEQ')).toBeUndefined();
92
+ });
93
+ });
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { relaxLE, relaxGE, tightenLT, tightenGT, andToOr, orToAnd, nullishToOr, flipEQ, flipNEQ, flipStrictEQ, flipStrictNEQ, addToSub, subToAdd, mulToDiv, divToMul, modToMul, powerToMul, } from '../operator.js';
3
+ // ---------------------------------------------------------------------------
4
+ // Shared behaviour (tested once; all mutators use the same factory)
5
+ // ---------------------------------------------------------------------------
6
+ describe('operator mutator shared behaviour', () => {
7
+ it('produces one mutation per matching operator occurrence', () => {
8
+ const src = `const ok = a && b && c`;
9
+ const results = andToOr.apply(src);
10
+ expect(results).toHaveLength(2);
11
+ });
12
+ it('reports the correct 1-based line number', () => {
13
+ const src = `const x = 1\nconst ok = a && b`;
14
+ const [result] = andToOr.apply(src);
15
+ expect(result.line).toBe(2);
16
+ });
17
+ it('reports the correct visual column', () => {
18
+ const src = `const ok = a && b`;
19
+ // ^ col 14 ('a' is at col 12, ' ' 13, '&' 14)
20
+ const [result] = andToOr.apply(src);
21
+ expect(result.col).toBe(14);
22
+ });
23
+ it('does not mutate operators inside string literals (AST-safe)', () => {
24
+ const src = `const s = 'a && b'`;
25
+ const results = andToOr.apply(src);
26
+ expect(results).toHaveLength(0);
27
+ });
28
+ it('does not mutate operators inside comments (AST-safe)', () => {
29
+ const src = `// a && b\nconst x = 1`;
30
+ const results = andToOr.apply(src);
31
+ expect(results).toHaveLength(0);
32
+ });
33
+ it('returns no results when the operator is absent', () => {
34
+ const src = `const x = a || b`;
35
+ const results = andToOr.apply(src);
36
+ expect(results).toHaveLength(0);
37
+ });
38
+ });
39
+ // ---------------------------------------------------------------------------
40
+ // Boundary mutators
41
+ // ---------------------------------------------------------------------------
42
+ describe('relaxLE', () => {
43
+ it("replaces '<=' with '<'", () => {
44
+ const src = `if (x <= 10) {}`;
45
+ const [result] = relaxLE.apply(src);
46
+ expect(result.code).toBe(`if (x < 10) {}`);
47
+ });
48
+ });
49
+ describe('relaxGE', () => {
50
+ it("replaces '>=' with '>'", () => {
51
+ const src = `if (x >= 0) {}`;
52
+ const [result] = relaxGE.apply(src);
53
+ expect(result.code).toBe(`if (x > 0) {}`);
54
+ });
55
+ });
56
+ describe('tightenLT', () => {
57
+ it("replaces '<' with '<='", () => {
58
+ const src = `if (x < 10) {}`;
59
+ const [result] = tightenLT.apply(src);
60
+ expect(result.code).toBe(`if (x <= 10) {}`);
61
+ });
62
+ });
63
+ describe('tightenGT', () => {
64
+ it("replaces '>' with '>='", () => {
65
+ const src = `if (x > 0) {}`;
66
+ const [result] = tightenGT.apply(src);
67
+ expect(result.code).toBe(`if (x >= 0) {}`);
68
+ });
69
+ });
70
+ // ---------------------------------------------------------------------------
71
+ // Logical mutators
72
+ // ---------------------------------------------------------------------------
73
+ describe('andToOr', () => {
74
+ it("replaces '&&' with '||'", () => {
75
+ const src = `const ok = a && b`;
76
+ const [result] = andToOr.apply(src);
77
+ expect(result.code).toBe(`const ok = a || b`);
78
+ });
79
+ });
80
+ describe('orToAnd', () => {
81
+ it("replaces '||' with '&&'", () => {
82
+ const src = `const ok = a || b`;
83
+ const [result] = orToAnd.apply(src);
84
+ expect(result.code).toBe(`const ok = a && b`);
85
+ });
86
+ });
87
+ describe('nullishToOr', () => {
88
+ it("replaces '??' with '||'", () => {
89
+ const src = `const v = x ?? defaultValue`;
90
+ const [result] = nullishToOr.apply(src);
91
+ expect(result.code).toBe(`const v = x || defaultValue`);
92
+ });
93
+ });
94
+ // ---------------------------------------------------------------------------
95
+ // Equality mutators
96
+ // ---------------------------------------------------------------------------
97
+ describe('flipEQ', () => {
98
+ it("replaces '==' with '!='", () => {
99
+ const src = `if (a == b) {}`;
100
+ const [result] = flipEQ.apply(src);
101
+ expect(result.code).toBe(`if (a != b) {}`);
102
+ });
103
+ });
104
+ describe('flipNEQ', () => {
105
+ it("replaces '!=' with '=='", () => {
106
+ const src = `if (a != b) {}`;
107
+ const [result] = flipNEQ.apply(src);
108
+ expect(result.code).toBe(`if (a == b) {}`);
109
+ });
110
+ });
111
+ describe('flipStrictEQ', () => {
112
+ it("replaces '===' with '!=='", () => {
113
+ const src = `if (a === b) {}`;
114
+ const [result] = flipStrictEQ.apply(src);
115
+ expect(result.code).toBe(`if (a !== b) {}`);
116
+ });
117
+ });
118
+ describe('flipStrictNEQ', () => {
119
+ it("replaces '!==' with '==='", () => {
120
+ const src = `if (a !== b) {}`;
121
+ const [result] = flipStrictNEQ.apply(src);
122
+ expect(result.code).toBe(`if (a === b) {}`);
123
+ });
124
+ });
125
+ // ---------------------------------------------------------------------------
126
+ // Arithmetic mutators
127
+ // ---------------------------------------------------------------------------
128
+ describe('addToSub', () => {
129
+ it("replaces '+' with '-'", () => {
130
+ const src = `const n = a + b`;
131
+ const [result] = addToSub.apply(src);
132
+ expect(result.code).toBe(`const n = a - b`);
133
+ });
134
+ });
135
+ describe('subToAdd', () => {
136
+ it("replaces '-' with '+'", () => {
137
+ const src = `const n = a - b`;
138
+ const [result] = subToAdd.apply(src);
139
+ expect(result.code).toBe(`const n = a + b`);
140
+ });
141
+ });
142
+ describe('mulToDiv', () => {
143
+ it("replaces '*' with '/'", () => {
144
+ const src = `const n = a * b`;
145
+ const [result] = mulToDiv.apply(src);
146
+ expect(result.code).toBe(`const n = a / b`);
147
+ });
148
+ });
149
+ describe('divToMul', () => {
150
+ it("replaces '/' with '*'", () => {
151
+ const src = `const n = a / b`;
152
+ const [result] = divToMul.apply(src);
153
+ expect(result.code).toBe(`const n = a * b`);
154
+ });
155
+ });
156
+ describe('modToMul', () => {
157
+ it("replaces '%' with '*'", () => {
158
+ const src = `const n = a % b`;
159
+ const [result] = modToMul.apply(src);
160
+ expect(result.code).toBe(`const n = a * b`);
161
+ });
162
+ });
163
+ describe('powerToMul', () => {
164
+ it("replaces '**' with '*'", () => {
165
+ const src = `const n = a ** b`;
166
+ const [result] = powerToMul.apply(src);
167
+ expect(result.code).toBe(`const n = a * b`);
168
+ });
169
+ });
@@ -18,6 +18,12 @@ const ALL_NAMES = [
18
18
  'divToMul',
19
19
  'modToMul',
20
20
  'powerToMul',
21
+ 'returnToNull',
22
+ 'returnToUndefined',
23
+ 'returnFlipBool',
24
+ 'returnZero',
25
+ 'returnEmptyStr',
26
+ 'returnEmptyArr',
21
27
  ];
22
28
  describe('mutator registry', () => {
23
29
  it('returns all mutators by default in declared order', () => {