@mutineerjs/mutineer 0.2.2 → 0.2.4
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.
- package/README.md +22 -7
- package/dist/core/__tests__/module.spec.js +66 -3
- package/dist/core/__tests__/sfc.spec.d.ts +1 -0
- package/dist/core/__tests__/sfc.spec.js +76 -0
- package/dist/core/__tests__/variant-utils.spec.d.ts +1 -0
- package/dist/core/__tests__/variant-utils.spec.js +93 -0
- package/dist/runner/__tests__/args.spec.d.ts +1 -0
- package/dist/runner/__tests__/args.spec.js +225 -0
- package/dist/runner/__tests__/cache.spec.d.ts +1 -0
- package/dist/runner/__tests__/cache.spec.js +180 -0
- package/dist/runner/__tests__/changed.spec.d.ts +1 -0
- package/dist/runner/__tests__/changed.spec.js +227 -0
- package/dist/runner/__tests__/cleanup.spec.d.ts +1 -0
- package/dist/runner/__tests__/cleanup.spec.js +41 -0
- package/dist/runner/__tests__/config.spec.d.ts +1 -0
- package/dist/runner/__tests__/config.spec.js +71 -0
- package/dist/runner/__tests__/coverage-resolver.spec.d.ts +1 -0
- package/dist/runner/__tests__/coverage-resolver.spec.js +171 -0
- package/dist/runner/__tests__/pool-executor.spec.d.ts +1 -0
- package/dist/runner/__tests__/pool-executor.spec.js +213 -0
- package/dist/runner/__tests__/tasks.spec.d.ts +1 -0
- package/dist/runner/__tests__/tasks.spec.js +95 -0
- package/dist/runner/__tests__/variants.spec.d.ts +1 -0
- package/dist/runner/__tests__/variants.spec.js +259 -0
- package/dist/runner/args.d.ts +5 -0
- package/dist/runner/args.js +7 -0
- package/dist/runner/config.js +2 -2
- package/dist/runner/coverage-resolver.d.ts +21 -0
- package/dist/runner/coverage-resolver.js +96 -0
- package/dist/runner/jest/__tests__/pool.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/pool.spec.js +212 -0
- package/dist/runner/jest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/worker-runtime.spec.js +148 -0
- package/dist/runner/orchestrator.js +43 -295
- package/dist/runner/pool-executor.d.ts +17 -0
- package/dist/runner/pool-executor.js +143 -0
- package/dist/runner/shared/__tests__/mutant-paths.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/mutant-paths.spec.js +66 -0
- package/dist/runner/shared/__tests__/redirect-state.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/redirect-state.spec.js +56 -0
- package/dist/runner/tasks.d.ts +12 -0
- package/dist/runner/tasks.js +25 -0
- package/dist/runner/variants.d.ts +17 -2
- package/dist/runner/variants.js +33 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +4 -0
- package/dist/utils/__tests__/logger.spec.d.ts +1 -0
- package/dist/utils/__tests__/logger.spec.js +61 -0
- package/dist/utils/__tests__/normalizePath.spec.d.ts +1 -0
- package/dist/utils/__tests__/normalizePath.spec.js +22 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -64,12 +64,27 @@ npm i @mutineerjs/mutineer
|
|
|
64
64
|
|
|
65
65
|
### Quick Start
|
|
66
66
|
|
|
67
|
+
Try it immediately with `npx`:
|
|
68
|
+
|
|
67
69
|
```bash
|
|
68
|
-
|
|
69
|
-
mutineer
|
|
70
|
+
npx @mutineerjs/mutineer init
|
|
71
|
+
npx @mutineerjs/mutineer run
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or add scripts to your `package.json` (recommended for team projects):
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"scripts": {
|
|
79
|
+
"mutineer": "mutineer run",
|
|
80
|
+
"mutineer:init": "mutineer init"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
70
84
|
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
```bash
|
|
86
|
+
npm run mutineer:init
|
|
87
|
+
npm run mutineer
|
|
73
88
|
```
|
|
74
89
|
|
|
75
90
|
### CLI Options (for `mutineer run`)
|
|
@@ -92,19 +107,19 @@ mutineer run
|
|
|
92
107
|
Run mutations on only the files you changed:
|
|
93
108
|
|
|
94
109
|
```bash
|
|
95
|
-
|
|
110
|
+
npm run mutineer -- --changed
|
|
96
111
|
```
|
|
97
112
|
|
|
98
113
|
Run with Jest and a minimum kill rate:
|
|
99
114
|
|
|
100
115
|
```bash
|
|
101
|
-
|
|
116
|
+
npm run mutineer -- --runner jest --min-kill-percent 80
|
|
102
117
|
```
|
|
103
118
|
|
|
104
119
|
Focus on covered code with 2 parallel workers:
|
|
105
120
|
|
|
106
121
|
```bash
|
|
107
|
-
|
|
122
|
+
npm run mutineer -- --only-covered-lines --concurrency 2
|
|
108
123
|
```
|
|
109
124
|
|
|
110
125
|
## Configuration
|
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseFlagNumber, readNumberFlag, readStringFlag, validatePercent, parseConcurrency, parseProgressMode, parseCliOptions, extractConfigPath, } from '../args.js';
|
|
3
|
+
describe('parseFlagNumber', () => {
|
|
4
|
+
it('parses valid integers', () => {
|
|
5
|
+
expect(parseFlagNumber('42', '--flag')).toBe(42);
|
|
6
|
+
});
|
|
7
|
+
it('parses valid floats', () => {
|
|
8
|
+
expect(parseFlagNumber('3.14', '--flag')).toBe(3.14);
|
|
9
|
+
});
|
|
10
|
+
it('parses zero', () => {
|
|
11
|
+
expect(parseFlagNumber('0', '--flag')).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
it('parses negative numbers', () => {
|
|
14
|
+
expect(parseFlagNumber('-5', '--flag')).toBe(-5);
|
|
15
|
+
});
|
|
16
|
+
it('throws on non-numeric strings', () => {
|
|
17
|
+
expect(() => parseFlagNumber('abc', '--flag')).toThrow('Invalid value for --flag: abc');
|
|
18
|
+
});
|
|
19
|
+
it('parses empty string as 0', () => {
|
|
20
|
+
// Number('') === 0, which is finite
|
|
21
|
+
expect(parseFlagNumber('', '--flag')).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
it('throws on NaN-producing values', () => {
|
|
24
|
+
expect(() => parseFlagNumber('not-a-number', '--flag')).toThrow('Invalid value for --flag: not-a-number');
|
|
25
|
+
});
|
|
26
|
+
it('throws on Infinity', () => {
|
|
27
|
+
expect(() => parseFlagNumber('Infinity', '--flag')).toThrow('Invalid value for --flag: Infinity');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('readNumberFlag', () => {
|
|
31
|
+
it('reads flag with separate value', () => {
|
|
32
|
+
expect(readNumberFlag(['--count', '5'], '--count')).toBe(5);
|
|
33
|
+
});
|
|
34
|
+
it('reads flag with = syntax', () => {
|
|
35
|
+
expect(readNumberFlag(['--count=10'], '--count')).toBe(10);
|
|
36
|
+
});
|
|
37
|
+
it('returns undefined when flag is not present', () => {
|
|
38
|
+
expect(readNumberFlag(['--other', '5'], '--count')).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
it('throws when no value follows flag', () => {
|
|
41
|
+
expect(() => readNumberFlag(['--count'], '--count')).toThrow('Expected a numeric value after --count');
|
|
42
|
+
});
|
|
43
|
+
it('throws when value is not a number', () => {
|
|
44
|
+
expect(() => readNumberFlag(['--count', 'abc'], '--count')).toThrow('Invalid value for --count: abc');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('readStringFlag', () => {
|
|
48
|
+
it('reads flag with separate value', () => {
|
|
49
|
+
expect(readStringFlag(['--config', 'path.ts'], '--config')).toBe('path.ts');
|
|
50
|
+
});
|
|
51
|
+
it('reads flag with = syntax', () => {
|
|
52
|
+
expect(readStringFlag(['--config=path.ts'], '--config')).toBe('path.ts');
|
|
53
|
+
});
|
|
54
|
+
it('reads alias', () => {
|
|
55
|
+
expect(readStringFlag(['-c', 'path.ts'], '--config', '-c')).toBe('path.ts');
|
|
56
|
+
});
|
|
57
|
+
it('reads alias with = syntax', () => {
|
|
58
|
+
expect(readStringFlag(['-c=path.ts'], '--config', '-c')).toBe('path.ts');
|
|
59
|
+
});
|
|
60
|
+
it('returns undefined when flag is not present', () => {
|
|
61
|
+
expect(readStringFlag(['--other', 'val'], '--config')).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
it('throws when no value follows flag', () => {
|
|
64
|
+
expect(() => readStringFlag(['--config'], '--config')).toThrow('Expected a value after --config');
|
|
65
|
+
});
|
|
66
|
+
it('throws when no value follows alias', () => {
|
|
67
|
+
expect(() => readStringFlag(['-c'], '--config', '-c')).toThrow('Expected a value after -c');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('validatePercent', () => {
|
|
71
|
+
it('returns undefined for undefined', () => {
|
|
72
|
+
expect(validatePercent(undefined, 'test')).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
it('returns the value for valid percentages', () => {
|
|
75
|
+
expect(validatePercent(0, 'test')).toBe(0);
|
|
76
|
+
expect(validatePercent(50, 'test')).toBe(50);
|
|
77
|
+
expect(validatePercent(100, 'test')).toBe(100);
|
|
78
|
+
});
|
|
79
|
+
it('throws for negative values', () => {
|
|
80
|
+
expect(() => validatePercent(-1, 'test')).toThrow('Invalid test: expected value between 0 and 100 (received -1)');
|
|
81
|
+
});
|
|
82
|
+
it('throws for values over 100', () => {
|
|
83
|
+
expect(() => validatePercent(101, 'test')).toThrow('Invalid test: expected value between 0 and 100 (received 101)');
|
|
84
|
+
});
|
|
85
|
+
it('throws for non-finite values', () => {
|
|
86
|
+
expect(() => validatePercent(NaN, 'test')).toThrow('Invalid test: expected a number between 0 and 100');
|
|
87
|
+
expect(() => validatePercent(Infinity, 'test')).toThrow('Invalid test: expected a number between 0 and 100');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('parseConcurrency', () => {
|
|
91
|
+
it('returns default when flag not present', () => {
|
|
92
|
+
const result = parseConcurrency([]);
|
|
93
|
+
expect(result).toBeGreaterThanOrEqual(1);
|
|
94
|
+
});
|
|
95
|
+
it('parses explicit concurrency', () => {
|
|
96
|
+
expect(parseConcurrency(['--concurrency', '4'])).toBe(4);
|
|
97
|
+
});
|
|
98
|
+
it('clamps to minimum of 1', () => {
|
|
99
|
+
expect(parseConcurrency(['--concurrency', '0'])).toBeGreaterThanOrEqual(1);
|
|
100
|
+
});
|
|
101
|
+
it('handles invalid concurrency value gracefully', () => {
|
|
102
|
+
const result = parseConcurrency(['--concurrency', 'abc']);
|
|
103
|
+
expect(result).toBeGreaterThanOrEqual(1);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('parseProgressMode', () => {
|
|
107
|
+
it('returns bar by default', () => {
|
|
108
|
+
expect(parseProgressMode([])).toBe('bar');
|
|
109
|
+
});
|
|
110
|
+
it('returns list when specified', () => {
|
|
111
|
+
expect(parseProgressMode(['--progress', 'list'])).toBe('list');
|
|
112
|
+
});
|
|
113
|
+
it('returns quiet when specified', () => {
|
|
114
|
+
expect(parseProgressMode(['--progress', 'quiet'])).toBe('quiet');
|
|
115
|
+
});
|
|
116
|
+
it('returns bar for unknown values', () => {
|
|
117
|
+
expect(parseProgressMode(['--progress', 'unknown'])).toBe('bar');
|
|
118
|
+
});
|
|
119
|
+
it('defaults to bar when --progress has no following value', () => {
|
|
120
|
+
expect(parseProgressMode(['--progress'])).toBe('bar');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('parseCliOptions', () => {
|
|
124
|
+
const emptyCfg = {};
|
|
125
|
+
it('parses --changed flag', () => {
|
|
126
|
+
const opts = parseCliOptions(['--changed'], emptyCfg);
|
|
127
|
+
expect(opts.wantsChanged).toBe(true);
|
|
128
|
+
expect(opts.wantsChangedWithDeps).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
it('parses --changed-with-deps flag', () => {
|
|
131
|
+
const opts = parseCliOptions(['--changed-with-deps'], emptyCfg);
|
|
132
|
+
expect(opts.wantsChangedWithDeps).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
it('parses --only-covered-lines flag', () => {
|
|
135
|
+
const opts = parseCliOptions(['--only-covered-lines'], emptyCfg);
|
|
136
|
+
expect(opts.wantsOnlyCoveredLines).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('reads onlyCoveredLines from config', () => {
|
|
139
|
+
const opts = parseCliOptions([], { onlyCoveredLines: true });
|
|
140
|
+
expect(opts.wantsOnlyCoveredLines).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it('parses --per-test-coverage flag', () => {
|
|
143
|
+
const opts = parseCliOptions(['--per-test-coverage'], emptyCfg);
|
|
144
|
+
expect(opts.wantsPerTestCoverage).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
it('reads perTestCoverage from config', () => {
|
|
147
|
+
const opts = parseCliOptions([], { perTestCoverage: true });
|
|
148
|
+
expect(opts.wantsPerTestCoverage).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
it('parses --coverage-file flag', () => {
|
|
151
|
+
const opts = parseCliOptions(['--coverage-file', 'coverage.json'], emptyCfg);
|
|
152
|
+
expect(opts.coverageFilePath).toBe('coverage.json');
|
|
153
|
+
});
|
|
154
|
+
it('reads coverageFile from config', () => {
|
|
155
|
+
const opts = parseCliOptions([], { coverageFile: 'cov.json' });
|
|
156
|
+
expect(opts.coverageFilePath).toBe('cov.json');
|
|
157
|
+
});
|
|
158
|
+
it('CLI coverage-file takes precedence over config', () => {
|
|
159
|
+
const opts = parseCliOptions(['--coverage-file', 'cli.json'], {
|
|
160
|
+
coverageFile: 'config.json',
|
|
161
|
+
});
|
|
162
|
+
expect(opts.coverageFilePath).toBe('cli.json');
|
|
163
|
+
});
|
|
164
|
+
it('parses --runner vitest', () => {
|
|
165
|
+
const opts = parseCliOptions(['--runner', 'vitest'], emptyCfg);
|
|
166
|
+
expect(opts.runner).toBe('vitest');
|
|
167
|
+
});
|
|
168
|
+
it('parses --runner jest', () => {
|
|
169
|
+
const opts = parseCliOptions(['--runner', 'jest'], emptyCfg);
|
|
170
|
+
expect(opts.runner).toBe('jest');
|
|
171
|
+
});
|
|
172
|
+
it('falls back to config runner', () => {
|
|
173
|
+
const opts = parseCliOptions([], { runner: 'jest' });
|
|
174
|
+
expect(opts.runner).toBe('jest');
|
|
175
|
+
});
|
|
176
|
+
it('defaults to vitest', () => {
|
|
177
|
+
const opts = parseCliOptions([], emptyCfg);
|
|
178
|
+
expect(opts.runner).toBe('vitest');
|
|
179
|
+
});
|
|
180
|
+
it('parses --config flag', () => {
|
|
181
|
+
const opts = parseCliOptions(['--config', 'my.config.ts'], emptyCfg);
|
|
182
|
+
expect(opts.configPath).toBe('my.config.ts');
|
|
183
|
+
});
|
|
184
|
+
it('parses -c alias', () => {
|
|
185
|
+
const opts = parseCliOptions(['-c', 'my.config.ts'], emptyCfg);
|
|
186
|
+
expect(opts.configPath).toBe('my.config.ts');
|
|
187
|
+
});
|
|
188
|
+
it('parses --min-kill-percent from CLI', () => {
|
|
189
|
+
const opts = parseCliOptions(['--min-kill-percent', '80'], emptyCfg);
|
|
190
|
+
expect(opts.minKillPercent).toBe(80);
|
|
191
|
+
});
|
|
192
|
+
it('reads minKillPercent from config', () => {
|
|
193
|
+
const opts = parseCliOptions([], { minKillPercent: 75 });
|
|
194
|
+
expect(opts.minKillPercent).toBe(75);
|
|
195
|
+
});
|
|
196
|
+
it('CLI min-kill-percent takes precedence over config', () => {
|
|
197
|
+
const opts = parseCliOptions(['--min-kill-percent', '90'], {
|
|
198
|
+
minKillPercent: 75,
|
|
199
|
+
});
|
|
200
|
+
expect(opts.minKillPercent).toBe(90);
|
|
201
|
+
});
|
|
202
|
+
it('rejects invalid --min-kill-percent', () => {
|
|
203
|
+
expect(() => parseCliOptions(['--min-kill-percent', '150'], emptyCfg)).toThrow('expected value between 0 and 100');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('extractConfigPath', () => {
|
|
207
|
+
it('extracts --config with separate value', () => {
|
|
208
|
+
expect(extractConfigPath(['--config', 'my.config.ts'])).toBe('my.config.ts');
|
|
209
|
+
});
|
|
210
|
+
it('extracts --config with = syntax', () => {
|
|
211
|
+
expect(extractConfigPath(['--config=my.config.ts'])).toBe('my.config.ts');
|
|
212
|
+
});
|
|
213
|
+
it('extracts -c alias with separate value', () => {
|
|
214
|
+
expect(extractConfigPath(['-c', 'my.config.ts'])).toBe('my.config.ts');
|
|
215
|
+
});
|
|
216
|
+
it('extracts -c alias with = syntax', () => {
|
|
217
|
+
expect(extractConfigPath(['-c=my.config.ts'])).toBe('my.config.ts');
|
|
218
|
+
});
|
|
219
|
+
it('returns undefined when no config flag is present', () => {
|
|
220
|
+
expect(extractConfigPath(['--changed', '--runner', 'vitest'])).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
it('returns undefined for empty args', () => {
|
|
223
|
+
expect(extractConfigPath([])).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|