@mutineerjs/mutineer 0.2.3 → 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.
Files changed (49) hide show
  1. package/dist/core/__tests__/module.spec.js +66 -3
  2. package/dist/core/__tests__/sfc.spec.d.ts +1 -0
  3. package/dist/core/__tests__/sfc.spec.js +76 -0
  4. package/dist/core/__tests__/variant-utils.spec.d.ts +1 -0
  5. package/dist/core/__tests__/variant-utils.spec.js +93 -0
  6. package/dist/runner/__tests__/args.spec.d.ts +1 -0
  7. package/dist/runner/__tests__/args.spec.js +225 -0
  8. package/dist/runner/__tests__/cache.spec.d.ts +1 -0
  9. package/dist/runner/__tests__/cache.spec.js +180 -0
  10. package/dist/runner/__tests__/changed.spec.d.ts +1 -0
  11. package/dist/runner/__tests__/changed.spec.js +227 -0
  12. package/dist/runner/__tests__/cleanup.spec.d.ts +1 -0
  13. package/dist/runner/__tests__/cleanup.spec.js +41 -0
  14. package/dist/runner/__tests__/config.spec.d.ts +1 -0
  15. package/dist/runner/__tests__/config.spec.js +71 -0
  16. package/dist/runner/__tests__/coverage-resolver.spec.d.ts +1 -0
  17. package/dist/runner/__tests__/coverage-resolver.spec.js +171 -0
  18. package/dist/runner/__tests__/pool-executor.spec.d.ts +1 -0
  19. package/dist/runner/__tests__/pool-executor.spec.js +213 -0
  20. package/dist/runner/__tests__/tasks.spec.d.ts +1 -0
  21. package/dist/runner/__tests__/tasks.spec.js +95 -0
  22. package/dist/runner/__tests__/variants.spec.d.ts +1 -0
  23. package/dist/runner/__tests__/variants.spec.js +259 -0
  24. package/dist/runner/args.d.ts +5 -0
  25. package/dist/runner/args.js +7 -0
  26. package/dist/runner/config.js +2 -2
  27. package/dist/runner/coverage-resolver.d.ts +21 -0
  28. package/dist/runner/coverage-resolver.js +96 -0
  29. package/dist/runner/jest/__tests__/pool.spec.d.ts +1 -0
  30. package/dist/runner/jest/__tests__/pool.spec.js +212 -0
  31. package/dist/runner/jest/__tests__/worker-runtime.spec.d.ts +1 -0
  32. package/dist/runner/jest/__tests__/worker-runtime.spec.js +148 -0
  33. package/dist/runner/orchestrator.js +43 -295
  34. package/dist/runner/pool-executor.d.ts +17 -0
  35. package/dist/runner/pool-executor.js +143 -0
  36. package/dist/runner/shared/__tests__/mutant-paths.spec.d.ts +1 -0
  37. package/dist/runner/shared/__tests__/mutant-paths.spec.js +66 -0
  38. package/dist/runner/shared/__tests__/redirect-state.spec.d.ts +1 -0
  39. package/dist/runner/shared/__tests__/redirect-state.spec.js +56 -0
  40. package/dist/runner/tasks.d.ts +12 -0
  41. package/dist/runner/tasks.js +25 -0
  42. package/dist/runner/variants.d.ts +17 -2
  43. package/dist/runner/variants.js +33 -0
  44. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +4 -0
  45. package/dist/utils/__tests__/logger.spec.d.ts +1 -0
  46. package/dist/utils/__tests__/logger.spec.js +61 -0
  47. package/dist/utils/__tests__/normalizePath.spec.d.ts +1 -0
  48. package/dist/utils/__tests__/normalizePath.spec.js +22 -0
  49. package/package.json +1 -1
@@ -0,0 +1,227 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { listChangedFiles } from '../changed.js';
3
+ // Mock child_process.spawnSync
4
+ const spawnSyncMock = vi.fn();
5
+ vi.mock('node:child_process', () => ({
6
+ spawnSync: (...args) => spawnSyncMock(...args),
7
+ }));
8
+ // Mock fs functions
9
+ const existsSyncMock = vi.fn();
10
+ const readFileSyncMock = vi.fn();
11
+ vi.mock('node:fs', () => ({
12
+ default: {
13
+ existsSync: (...args) => existsSyncMock(...args),
14
+ readFileSync: (...args) => readFileSyncMock(...args),
15
+ },
16
+ existsSync: (...args) => existsSyncMock(...args),
17
+ readFileSync: (...args) => readFileSyncMock(...args),
18
+ }));
19
+ describe('listChangedFiles', () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ // Default: files exist
23
+ existsSyncMock.mockReturnValue(true);
24
+ });
25
+ it('returns empty array when no git repo is found', () => {
26
+ spawnSyncMock.mockReturnValue({ status: 1, stdout: '' });
27
+ const result = listChangedFiles('/not-a-repo', { quiet: true });
28
+ expect(result).toEqual([]);
29
+ });
30
+ it('returns changed files from git diff', () => {
31
+ // First call: rev-parse --show-toplevel (find repo root)
32
+ spawnSyncMock.mockImplementation((_cmd, args) => {
33
+ if (args.includes('--show-toplevel')) {
34
+ return { status: 0, stdout: '/repo\n' };
35
+ }
36
+ if (args.includes('--name-only') && args.includes('main...HEAD')) {
37
+ return { status: 0, stdout: 'src/foo.ts\0src/bar.ts\0' };
38
+ }
39
+ if (args.includes('--name-only') && args.includes('HEAD')) {
40
+ return { status: 0, stdout: '' };
41
+ }
42
+ if (args.includes('--others')) {
43
+ return { status: 0, stdout: '' };
44
+ }
45
+ return { status: 1, stdout: '' };
46
+ });
47
+ const result = listChangedFiles('/repo');
48
+ expect(result).toHaveLength(2);
49
+ expect(result).toContain('/repo/src/foo.ts');
50
+ expect(result).toContain('/repo/src/bar.ts');
51
+ });
52
+ it('deduplicates files from multiple git sources', () => {
53
+ spawnSyncMock.mockImplementation((_cmd, args) => {
54
+ if (args.includes('--show-toplevel')) {
55
+ return { status: 0, stdout: '/repo\n' };
56
+ }
57
+ if (args.includes('main...HEAD')) {
58
+ return { status: 0, stdout: 'src/foo.ts\0' };
59
+ }
60
+ if (args.includes('HEAD') && !args.includes('main...HEAD')) {
61
+ return { status: 0, stdout: 'src/foo.ts\0' }; // same file
62
+ }
63
+ if (args.includes('--others')) {
64
+ return { status: 0, stdout: '' };
65
+ }
66
+ return { status: 1, stdout: '' };
67
+ });
68
+ const result = listChangedFiles('/repo');
69
+ expect(result).toHaveLength(1);
70
+ });
71
+ it('skips deleted/missing files', () => {
72
+ spawnSyncMock.mockImplementation((_cmd, args) => {
73
+ if (args.includes('--show-toplevel')) {
74
+ return { status: 0, stdout: '/repo\n' };
75
+ }
76
+ if (args.includes('main...HEAD')) {
77
+ return { status: 0, stdout: 'deleted.ts\0' };
78
+ }
79
+ return { status: 0, stdout: '' };
80
+ });
81
+ existsSyncMock.mockReturnValue(false);
82
+ const result = listChangedFiles('/repo');
83
+ expect(result).toEqual([]);
84
+ });
85
+ it('includes untracked files', () => {
86
+ spawnSyncMock.mockImplementation((_cmd, args) => {
87
+ if (args.includes('--show-toplevel')) {
88
+ return { status: 0, stdout: '/repo\n' };
89
+ }
90
+ if (args.includes('--others')) {
91
+ return { status: 0, stdout: 'new-file.ts\0' };
92
+ }
93
+ return { status: 0, stdout: '' };
94
+ });
95
+ const result = listChangedFiles('/repo');
96
+ expect(result).toContain('/repo/new-file.ts');
97
+ });
98
+ it('returns empty when all git commands fail', () => {
99
+ spawnSyncMock.mockImplementation((_cmd, args) => {
100
+ if (args.includes('--show-toplevel')) {
101
+ return { status: 0, stdout: '/repo\n' };
102
+ }
103
+ return { status: 1, stdout: '' };
104
+ });
105
+ const result = listChangedFiles('/repo');
106
+ expect(result).toEqual([]);
107
+ });
108
+ it('uses custom baseRef', () => {
109
+ const gitArgs = [];
110
+ spawnSyncMock.mockImplementation((_cmd, args) => {
111
+ gitArgs.push(args);
112
+ if (args.includes('--show-toplevel')) {
113
+ return { status: 0, stdout: '/repo\n' };
114
+ }
115
+ return { status: 0, stdout: '' };
116
+ });
117
+ listChangedFiles('/repo', { baseRef: 'develop' });
118
+ const diffCall = gitArgs.find((a) => a.some((x) => x.includes('...')));
119
+ expect(diffCall).toBeDefined();
120
+ expect(diffCall.some((a) => a.includes('develop...HEAD'))).toBe(true);
121
+ });
122
+ it('resolves dependencies when includeDeps is true', () => {
123
+ spawnSyncMock.mockImplementation((_cmd, args) => {
124
+ if (args.includes('--show-toplevel')) {
125
+ return { status: 0, stdout: '/repo\n' };
126
+ }
127
+ if (args.includes('main...HEAD')) {
128
+ return { status: 0, stdout: 'src/foo.ts\0' };
129
+ }
130
+ return { status: 0, stdout: '' };
131
+ });
132
+ // When reading the changed file to resolve deps
133
+ readFileSyncMock.mockReturnValue('// no imports');
134
+ const result = listChangedFiles('/repo', { includeDeps: true });
135
+ // Should at least contain the original file
136
+ expect(result).toContain('/repo/src/foo.ts');
137
+ });
138
+ it('resolves dependencies with import statements', () => {
139
+ spawnSyncMock.mockImplementation((_cmd, args) => {
140
+ if (args.includes('--show-toplevel')) {
141
+ return { status: 0, stdout: '/repo\n' };
142
+ }
143
+ if (args.includes('main...HEAD')) {
144
+ return { status: 0, stdout: 'src/foo.ts\0' };
145
+ }
146
+ return { status: 0, stdout: '' };
147
+ });
148
+ // File with imports - the import resolution will fail (no real files)
149
+ // but it exercises the parsing code paths
150
+ readFileSyncMock.mockReturnValue('import { bar } from "./bar"\nexport { baz } from "./baz"\nconst x = require("./qux")');
151
+ const result = listChangedFiles('/repo', { includeDeps: true });
152
+ expect(result).toContain('/repo/src/foo.ts');
153
+ });
154
+ it('skips non-local imports in dependency resolution', () => {
155
+ spawnSyncMock.mockImplementation((_cmd, args) => {
156
+ if (args.includes('--show-toplevel')) {
157
+ return { status: 0, stdout: '/repo\n' };
158
+ }
159
+ if (args.includes('main...HEAD')) {
160
+ return { status: 0, stdout: 'src/foo.ts\0' };
161
+ }
162
+ return { status: 0, stdout: '' };
163
+ });
164
+ // Non-local imports (no './' prefix) should be skipped
165
+ readFileSyncMock.mockReturnValue('import lodash from "lodash"');
166
+ const result = listChangedFiles('/repo', { includeDeps: true });
167
+ expect(result).toHaveLength(1);
168
+ expect(result).toContain('/repo/src/foo.ts');
169
+ });
170
+ it('handles file that no longer exists during dep resolution', () => {
171
+ spawnSyncMock.mockImplementation((_cmd, args) => {
172
+ if (args.includes('--show-toplevel')) {
173
+ return { status: 0, stdout: '/repo\n' };
174
+ }
175
+ if (args.includes('main...HEAD')) {
176
+ return { status: 0, stdout: 'src/foo.ts\0' };
177
+ }
178
+ return { status: 0, stdout: '' };
179
+ });
180
+ // First existsSync for changed file check = true
181
+ // Then existsSync for dep resolution: file exists check, then readFile fails
182
+ let callCount = 0;
183
+ existsSyncMock.mockImplementation(() => {
184
+ callCount++;
185
+ // First call is for the changed file in the main loop
186
+ return callCount <= 1;
187
+ });
188
+ readFileSyncMock.mockImplementation(() => {
189
+ throw new Error('ENOENT');
190
+ });
191
+ const result = listChangedFiles('/repo', { includeDeps: true });
192
+ expect(result).toContain('/repo/src/foo.ts');
193
+ });
194
+ it('respects maxDepth option', () => {
195
+ spawnSyncMock.mockImplementation((_cmd, args) => {
196
+ if (args.includes('--show-toplevel')) {
197
+ return { status: 0, stdout: '/repo\n' };
198
+ }
199
+ if (args.includes('main...HEAD')) {
200
+ return { status: 0, stdout: 'src/foo.ts\0' };
201
+ }
202
+ return { status: 0, stdout: '' };
203
+ });
204
+ readFileSyncMock.mockReturnValue('import { x } from "./bar"');
205
+ const result = listChangedFiles('/repo', {
206
+ includeDeps: true,
207
+ maxDepth: 0,
208
+ });
209
+ // maxDepth=0 means no recursion into deps
210
+ expect(result).toContain('/repo/src/foo.ts');
211
+ });
212
+ it('only processes source files for dependency resolution', () => {
213
+ spawnSyncMock.mockImplementation((_cmd, args) => {
214
+ if (args.includes('--show-toplevel')) {
215
+ return { status: 0, stdout: '/repo\n' };
216
+ }
217
+ if (args.includes('main...HEAD')) {
218
+ return { status: 0, stdout: 'README.md\0' }; // non-source file
219
+ }
220
+ return { status: 0, stdout: '' };
221
+ });
222
+ const result = listChangedFiles('/repo', { includeDeps: true });
223
+ // README.md doesn't match /\.(js|ts|vue|mjs|cjs)$/, so no deps resolved
224
+ expect(result).toContain('/repo/README.md');
225
+ expect(readFileSyncMock).not.toHaveBeenCalled();
226
+ });
227
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { cleanupMutineerDirs } from '../cleanup.js';
6
+ let tmpDir;
7
+ beforeEach(async () => {
8
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-cleanup-'));
9
+ });
10
+ afterEach(async () => {
11
+ await fs.rm(tmpDir, { recursive: true, force: true });
12
+ });
13
+ describe('cleanupMutineerDirs', () => {
14
+ it('removes __mutineer__ directories', async () => {
15
+ const mutDir = path.join(tmpDir, 'src', '__mutineer__');
16
+ await fs.mkdir(mutDir, { recursive: true });
17
+ await fs.writeFile(path.join(mutDir, 'mutant.ts'), 'code');
18
+ await cleanupMutineerDirs(tmpDir);
19
+ await expect(fs.access(mutDir)).rejects.toThrow();
20
+ });
21
+ it('removes nested __mutineer__ directories', async () => {
22
+ const dir1 = path.join(tmpDir, 'src', 'a', '__mutineer__');
23
+ const dir2 = path.join(tmpDir, 'src', 'b', '__mutineer__');
24
+ await fs.mkdir(dir1, { recursive: true });
25
+ await fs.mkdir(dir2, { recursive: true });
26
+ await cleanupMutineerDirs(tmpDir);
27
+ await expect(fs.access(dir1)).rejects.toThrow();
28
+ await expect(fs.access(dir2)).rejects.toThrow();
29
+ });
30
+ it('does not throw when no __mutineer__ dirs exist', async () => {
31
+ await expect(cleanupMutineerDirs(tmpDir)).resolves.toBeUndefined();
32
+ });
33
+ it('preserves non-mutineer directories', async () => {
34
+ const srcDir = path.join(tmpDir, 'src');
35
+ await fs.mkdir(srcDir, { recursive: true });
36
+ await fs.writeFile(path.join(srcDir, 'file.ts'), 'code');
37
+ await cleanupMutineerDirs(tmpDir);
38
+ const stat = await fs.stat(srcDir);
39
+ expect(stat.isDirectory()).toBe(true);
40
+ });
41
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { loadMutineerConfig } from '../config.js';
6
+ let tmpDir;
7
+ beforeEach(async () => {
8
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-config-'));
9
+ });
10
+ afterEach(async () => {
11
+ await fs.rm(tmpDir, { recursive: true, force: true });
12
+ });
13
+ describe('loadMutineerConfig', () => {
14
+ it('throws when no config file is found', async () => {
15
+ await expect(loadMutineerConfig(tmpDir)).rejects.toThrow('No config found in');
16
+ });
17
+ it('throws when explicit config path does not exist', async () => {
18
+ await expect(loadMutineerConfig(tmpDir, 'nonexistent.js')).rejects.toThrow('No config found at nonexistent.js');
19
+ });
20
+ it('loads a .js config file', async () => {
21
+ const configFile = path.join(tmpDir, 'mutineer.config.js');
22
+ await fs.writeFile(configFile, 'export default { runner: "vitest" }');
23
+ const config = await loadMutineerConfig(tmpDir);
24
+ expect(config).toEqual({ runner: 'vitest' });
25
+ });
26
+ it('loads a .mjs config file', async () => {
27
+ const configFile = path.join(tmpDir, 'mutineer.config.mjs');
28
+ await fs.writeFile(configFile, 'export default { runner: "jest" }');
29
+ const config = await loadMutineerConfig(tmpDir);
30
+ expect(config).toEqual({ runner: 'jest' });
31
+ });
32
+ it('loads config from explicit path', async () => {
33
+ const configFile = path.join(tmpDir, 'custom.config.mjs');
34
+ await fs.writeFile(configFile, 'export default { maxMutantsPerFile: 10 }');
35
+ const config = await loadMutineerConfig(tmpDir, 'custom.config.mjs');
36
+ expect(config).toEqual({ maxMutantsPerFile: 10 });
37
+ });
38
+ it('prefers mutineer.config.ts over .js and .mjs', async () => {
39
+ // When a .ts config exists but we can't load it with Vite, it will fail.
40
+ // We just test the .js fallback works when no .ts exists.
41
+ const jsConfig = path.join(tmpDir, 'mutineer.config.js');
42
+ const mjsConfig = path.join(tmpDir, 'mutineer.config.mjs');
43
+ await fs.writeFile(jsConfig, 'export default { source: "js" }');
44
+ await fs.writeFile(mjsConfig, 'export default { source: "mjs" }');
45
+ const config = await loadMutineerConfig(tmpDir);
46
+ // .js comes before .mjs in the candidate order
47
+ expect(config).toEqual({ source: 'js' });
48
+ });
49
+ it('wraps load errors with config path info', async () => {
50
+ const configFile = path.join(tmpDir, 'mutineer.config.js');
51
+ // Write invalid JS that will fail to import
52
+ await fs.writeFile(configFile, '??? not valid javascript ???');
53
+ await expect(loadMutineerConfig(tmpDir)).rejects.toThrow(/Failed to load config from/);
54
+ });
55
+ // BUG: Two bugs compound here:
56
+ // 1. validateConfig uses `&&` instead of `||`: `typeof config !== 'object' && config === null`
57
+ // Since typeof null === 'object', this condition is always false, so null passes validation.
58
+ // 2. loadModule uses `||` instead of `??`: `mod.default || mod`
59
+ // When default export is null (falsy), it falls back to the module namespace object.
60
+ // Together: null configs pass validation AND get returned as the module namespace.
61
+ it('BUG: null config passes validation due to && vs || logic error', async () => {
62
+ const configFile = path.join(tmpDir, 'mutineer.config.mjs');
63
+ await fs.writeFile(configFile, 'export default null');
64
+ // This SHOULD throw but doesn't because of the validateConfig bug.
65
+ // Additionally, loadModule returns { default: null } instead of null
66
+ // because it uses || (which treats null as falsy) instead of ??
67
+ const config = await loadMutineerConfig(tmpDir);
68
+ // Bug: returns the module namespace object instead of throwing
69
+ expect(config).toHaveProperty('default', null);
70
+ });
71
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { resolveCoverageConfig, loadCoverageAfterBaseline, } from '../coverage-resolver.js';
6
+ function makeOpts(overrides = {}) {
7
+ return {
8
+ configPath: undefined,
9
+ wantsChanged: false,
10
+ wantsChangedWithDeps: false,
11
+ wantsOnlyCoveredLines: false,
12
+ wantsPerTestCoverage: false,
13
+ coverageFilePath: undefined,
14
+ concurrency: 1,
15
+ progressMode: 'bar',
16
+ minKillPercent: undefined,
17
+ runner: 'vitest',
18
+ ...overrides,
19
+ };
20
+ }
21
+ function makeAdapter(overrides = {}) {
22
+ return {
23
+ name: 'vitest',
24
+ init: vi.fn().mockResolvedValue(undefined),
25
+ runBaseline: vi.fn().mockResolvedValue(true),
26
+ runMutant: vi.fn().mockResolvedValue({ status: 'killed', durationMs: 0 }),
27
+ shutdown: vi.fn().mockResolvedValue(undefined),
28
+ hasCoverageProvider: vi.fn().mockReturnValue(false),
29
+ detectCoverageConfig: vi
30
+ .fn()
31
+ .mockResolvedValue({ perTestEnabled: false, coverageEnabled: false }),
32
+ ...overrides,
33
+ };
34
+ }
35
+ describe('resolveCoverageConfig', () => {
36
+ beforeEach(() => {
37
+ process.exitCode = undefined;
38
+ });
39
+ afterEach(() => {
40
+ process.exitCode = undefined;
41
+ });
42
+ it('returns baseline defaults when no coverage is requested', async () => {
43
+ const result = await resolveCoverageConfig(makeOpts(), {}, makeAdapter(), []);
44
+ expect(result.coverageData).toBeNull();
45
+ expect(result.perTestCoverage).toBeNull();
46
+ expect(result.enableCoverageForBaseline).toBe(false);
47
+ expect(result.wantsPerTestCoverage).toBe(false);
48
+ expect(result.needsCoverageFromBaseline).toBe(false);
49
+ });
50
+ it('enables coverage for baseline when config coverage is true', async () => {
51
+ const result = await resolveCoverageConfig(makeOpts(), { coverage: true }, makeAdapter(), []);
52
+ expect(result.enableCoverageForBaseline).toBe(true);
53
+ });
54
+ it('disables coverage for baseline when config coverage is false', async () => {
55
+ const adapter = makeAdapter({
56
+ detectCoverageConfig: vi
57
+ .fn()
58
+ .mockResolvedValue({ perTestEnabled: false, coverageEnabled: true }),
59
+ });
60
+ const result = await resolveCoverageConfig(makeOpts(), { coverage: false }, adapter, []);
61
+ expect(result.enableCoverageForBaseline).toBe(false);
62
+ });
63
+ it('sets exitCode when onlyCoveredLines is set but no coverage provider', async () => {
64
+ const opts = makeOpts({ wantsOnlyCoveredLines: true });
65
+ const adapter = makeAdapter({ hasCoverageProvider: vi.fn().mockReturnValue(false) });
66
+ await resolveCoverageConfig(opts, {}, adapter, []);
67
+ expect(process.exitCode).toBe(1);
68
+ });
69
+ it('does not set exitCode when onlyCoveredLines is set with coverage provider', async () => {
70
+ const opts = makeOpts({ wantsOnlyCoveredLines: true });
71
+ const adapter = makeAdapter({
72
+ hasCoverageProvider: vi.fn().mockReturnValue(true),
73
+ });
74
+ await resolveCoverageConfig(opts, {}, adapter, []);
75
+ expect(process.exitCode).toBeUndefined();
76
+ });
77
+ it('disables per-test coverage for jest runner', async () => {
78
+ const opts = makeOpts({ runner: 'jest', wantsPerTestCoverage: true });
79
+ const result = await resolveCoverageConfig(opts, {}, makeAdapter(), []);
80
+ expect(result.wantsPerTestCoverage).toBe(false);
81
+ });
82
+ it('enables per-test coverage for vitest runner', async () => {
83
+ const opts = makeOpts({ runner: 'vitest', wantsPerTestCoverage: true });
84
+ const result = await resolveCoverageConfig(opts, {}, makeAdapter(), []);
85
+ expect(result.wantsPerTestCoverage).toBe(true);
86
+ expect(result.enableCoverageForBaseline).toBe(true);
87
+ });
88
+ it('enables per-test coverage when adapter reports it enabled', async () => {
89
+ const adapter = makeAdapter({
90
+ detectCoverageConfig: vi
91
+ .fn()
92
+ .mockResolvedValue({ perTestEnabled: true, coverageEnabled: false }),
93
+ });
94
+ const result = await resolveCoverageConfig(makeOpts(), {}, adapter, []);
95
+ expect(result.wantsPerTestCoverage).toBe(true);
96
+ });
97
+ it('sets needsCoverageFromBaseline when onlyCoveredLines without coverageFile', async () => {
98
+ const opts = makeOpts({ wantsOnlyCoveredLines: true });
99
+ const adapter = makeAdapter({
100
+ hasCoverageProvider: vi.fn().mockReturnValue(true),
101
+ });
102
+ const result = await resolveCoverageConfig(opts, {}, adapter, []);
103
+ expect(result.needsCoverageFromBaseline).toBe(true);
104
+ expect(result.enableCoverageForBaseline).toBe(true);
105
+ });
106
+ });
107
+ describe('loadCoverageAfterBaseline', () => {
108
+ let tmpDir;
109
+ beforeEach(async () => {
110
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-cov-resolver-'));
111
+ });
112
+ afterEach(async () => {
113
+ await fs.rm(tmpDir, { recursive: true, force: true });
114
+ });
115
+ it('returns resolution unchanged when no coverage needed from baseline', async () => {
116
+ const resolution = {
117
+ coverageData: null,
118
+ perTestCoverage: null,
119
+ enableCoverageForBaseline: false,
120
+ wantsPerTestCoverage: false,
121
+ needsCoverageFromBaseline: false,
122
+ };
123
+ const result = await loadCoverageAfterBaseline(resolution, tmpDir);
124
+ expect(result).toEqual(resolution);
125
+ });
126
+ it('loads coverage data from default path when needsCoverageFromBaseline', async () => {
127
+ const coverageDir = path.join(tmpDir, 'coverage');
128
+ await fs.mkdir(coverageDir, { recursive: true });
129
+ const coverageJson = {
130
+ '/src/foo.ts': {
131
+ path: '/src/foo.ts',
132
+ statementMap: { '0': { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } } },
133
+ s: { '0': 1 },
134
+ },
135
+ };
136
+ await fs.writeFile(path.join(coverageDir, 'coverage-final.json'), JSON.stringify(coverageJson));
137
+ const resolution = {
138
+ coverageData: null,
139
+ perTestCoverage: null,
140
+ enableCoverageForBaseline: true,
141
+ wantsPerTestCoverage: false,
142
+ needsCoverageFromBaseline: true,
143
+ };
144
+ const result = await loadCoverageAfterBaseline(resolution, tmpDir);
145
+ expect(result.coverageData).not.toBeNull();
146
+ expect(result.coverageData.coveredLines.size).toBeGreaterThan(0);
147
+ });
148
+ it('continues gracefully when coverage file is missing', async () => {
149
+ const resolution = {
150
+ coverageData: null,
151
+ perTestCoverage: null,
152
+ enableCoverageForBaseline: true,
153
+ wantsPerTestCoverage: false,
154
+ needsCoverageFromBaseline: true,
155
+ };
156
+ const result = await loadCoverageAfterBaseline(resolution, tmpDir);
157
+ // Should not throw, coverageData stays null
158
+ expect(result.coverageData).toBeNull();
159
+ });
160
+ it('does not modify resolution when wantsPerTestCoverage is false', async () => {
161
+ const resolution = {
162
+ coverageData: null,
163
+ perTestCoverage: null,
164
+ enableCoverageForBaseline: false,
165
+ wantsPerTestCoverage: false,
166
+ needsCoverageFromBaseline: false,
167
+ };
168
+ const result = await loadCoverageAfterBaseline(resolution, tmpDir);
169
+ expect(result.perTestCoverage).toBeNull();
170
+ });
171
+ });
@@ -0,0 +1 @@
1
+ export {};