@mutineerjs/mutineer 0.9.0 → 0.11.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.
- package/README.md +52 -47
- package/dist/__tests__/index.spec.js +8 -0
- package/dist/bin/__tests__/mutineer.spec.js +7 -7
- package/dist/bin/mutineer.d.ts +1 -1
- package/dist/bin/mutineer.js +7 -4
- package/dist/core/__tests__/schemata.spec.js +62 -0
- package/dist/core/__tests__/sfc.spec.js +41 -1
- package/dist/core/schemata.js +15 -21
- package/dist/core/sfc.js +0 -4
- package/dist/core/variant-utils.js +0 -4
- package/dist/mutators/__tests__/utils.spec.js +65 -1
- package/dist/mutators/operator.js +13 -27
- package/dist/mutators/return-value.js +3 -7
- package/dist/mutators/utils.d.ts +2 -2
- package/dist/mutators/utils.js +59 -96
- package/dist/runner/__tests__/args.spec.js +8 -4
- package/dist/runner/__tests__/cache.spec.js +24 -0
- package/dist/runner/__tests__/changed.spec.js +75 -0
- package/dist/runner/__tests__/config.spec.js +50 -1
- package/dist/runner/__tests__/coverage-resolver.spec.js +88 -1
- package/dist/runner/__tests__/discover.spec.js +179 -0
- package/dist/runner/__tests__/orchestrator.spec.js +336 -11
- package/dist/runner/__tests__/pool-executor.spec.js +77 -0
- package/dist/runner/__tests__/ts-checker-worker.spec.d.ts +1 -0
- package/dist/runner/__tests__/ts-checker-worker.spec.js +66 -0
- package/dist/runner/__tests__/ts-checker.spec.js +89 -2
- package/dist/runner/args.d.ts +1 -1
- package/dist/runner/args.js +2 -2
- package/dist/runner/config.js +3 -4
- package/dist/runner/coverage-resolver.js +2 -1
- package/dist/runner/discover.js +2 -2
- package/dist/runner/jest/__tests__/adapter.spec.js +169 -0
- package/dist/runner/jest/__tests__/pool.spec.js +223 -1
- package/dist/runner/jest/adapter.js +3 -45
- package/dist/runner/jest/pool.js +4 -10
- package/dist/runner/jest/worker-runtime.js +2 -1
- package/dist/runner/orchestrator.js +8 -7
- package/dist/runner/pool-executor.js +7 -12
- package/dist/runner/shared/__tests__/strip-mutineer-args.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/strip-mutineer-args.spec.js +104 -0
- package/dist/runner/shared/__tests__/worker-script.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/worker-script.spec.js +32 -0
- package/dist/runner/shared/index.d.ts +4 -0
- package/dist/runner/shared/index.js +2 -0
- package/dist/runner/shared/pending-task.d.ts +9 -0
- package/dist/runner/shared/pending-task.js +1 -0
- package/dist/runner/shared/strip-mutineer-args.d.ts +11 -0
- package/dist/runner/shared/strip-mutineer-args.js +47 -0
- package/dist/runner/shared/worker-script.d.ts +5 -0
- package/dist/runner/shared/worker-script.js +12 -0
- package/dist/runner/ts-checker-worker.d.ts +10 -1
- package/dist/runner/ts-checker-worker.js +27 -25
- package/dist/runner/ts-checker.d.ts +6 -0
- package/dist/runner/ts-checker.js +1 -1
- package/dist/runner/vitest/__tests__/adapter.spec.js +294 -0
- package/dist/runner/vitest/__tests__/plugin.spec.js +28 -1
- package/dist/runner/vitest/__tests__/pool.spec.js +711 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +116 -1
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +81 -0
- package/dist/runner/vitest/adapter.js +14 -46
- package/dist/runner/vitest/plugin.js +1 -7
- package/dist/runner/vitest/pool.js +6 -19
- package/dist/runner/vitest/redirect-loader.js +3 -1
- package/dist/runner/vitest/worker-runtime.js +16 -1
- package/dist/runner/vitest/worker.mjs +1 -0
- package/dist/types/config.d.ts +2 -2
- package/dist/types/mutant.d.ts +3 -0
- package/dist/utils/__tests__/PoolSpinner.spec.d.ts +1 -0
- package/dist/utils/__tests__/PoolSpinner.spec.js +15 -0
- package/dist/utils/__tests__/coverage.spec.js +89 -0
- package/dist/utils/__tests__/logger.spec.js +9 -0
- package/dist/utils/__tests__/progress.spec.js +38 -0
- package/dist/utils/__tests__/summary.spec.js +70 -31
- package/dist/utils/coverage.js +3 -4
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +6 -0
- package/dist/utils/summary.d.ts +2 -3
- package/dist/utils/summary.js +5 -6
- package/package.json +1 -1
- package/dist/utils/CompileErrors.d.ts +0 -7
- package/dist/utils/CompileErrors.js +0 -24
- package/dist/utils/__tests__/CompileErrors.spec.js +0 -96
- /package/dist/{utils/__tests__/CompileErrors.spec.d.ts → __tests__/index.spec.d.ts} +0 -0
|
@@ -60,6 +60,52 @@ describe('createViteResolver Vue plugin gating', () => {
|
|
|
60
60
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
|
+
it('successfully loads @vitejs/plugin-vue when available and .vue files exist', async () => {
|
|
64
|
+
vi.doMock('@vitejs/plugin-vue', () => ({
|
|
65
|
+
default: () => ({}),
|
|
66
|
+
}));
|
|
67
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-vueok-'));
|
|
68
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
69
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
70
|
+
await fs.writeFile(path.join(srcDir, 'Comp.vue'), '<template><div/></template>\n', 'utf8');
|
|
71
|
+
await fs.writeFile(path.join(srcDir, 'a.ts'), 'export const a = 1\n', 'utf8');
|
|
72
|
+
const importLine = ['im', 'port { a } from "./a"'].join('');
|
|
73
|
+
await fs.writeFile(path.join(srcDir, 'a.test.ts'), `${importLine}\n`, 'utf8');
|
|
74
|
+
try {
|
|
75
|
+
await autoDiscoverTargetsAndTests(tmpDir, {
|
|
76
|
+
testPatterns: ['**/*.test.ts'],
|
|
77
|
+
extensions: ['.vue', '.ts'],
|
|
78
|
+
});
|
|
79
|
+
// Should not warn about plugin-vue
|
|
80
|
+
const pluginVueWarnings = warnSpy.mock.calls.filter((args) => String(args[0]).includes('plugin-vue'));
|
|
81
|
+
expect(pluginVueWarnings).toHaveLength(0);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
vi.doUnmock('@vitejs/plugin-vue');
|
|
85
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
it('does not load @vitejs/plugin-vue when extensions excludes .vue', async () => {
|
|
89
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-novueext-'));
|
|
90
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
91
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
92
|
+
await fs.writeFile(path.join(srcDir, 'a.ts'), 'export const a = 1\n', 'utf8');
|
|
93
|
+
const importLine = ['im', 'port { a } from "./a"'].join('');
|
|
94
|
+
await fs.writeFile(path.join(srcDir, 'a.test.ts'), `${importLine}\n`, 'utf8');
|
|
95
|
+
try {
|
|
96
|
+
const result = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
97
|
+
testPatterns: ['**/*.test.ts'],
|
|
98
|
+
extensions: ['.ts'],
|
|
99
|
+
});
|
|
100
|
+
// Plugin-vue code is skipped entirely; no warnings
|
|
101
|
+
const pluginVueWarnings = warnSpy.mock.calls.filter((args) => String(args[0]).includes('plugin-vue'));
|
|
102
|
+
expect(pluginVueWarnings).toHaveLength(0);
|
|
103
|
+
expect(result.targets.length).toBeGreaterThan(0);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
63
109
|
it('warns when .vue files exist but @vitejs/plugin-vue fails to load', async () => {
|
|
64
110
|
vi.doMock('@vitejs/plugin-vue', () => {
|
|
65
111
|
throw new Error('Cannot find module @vitejs/plugin-vue');
|
|
@@ -86,6 +132,20 @@ describe('createViteResolver Vue plugin gating', () => {
|
|
|
86
132
|
});
|
|
87
133
|
});
|
|
88
134
|
describe('autoDiscoverTargetsAndTests', () => {
|
|
135
|
+
it('returns empty result when no test files found', async () => {
|
|
136
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-notests-'));
|
|
137
|
+
try {
|
|
138
|
+
const result = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
139
|
+
testPatterns: ['**/*.test.ts'],
|
|
140
|
+
});
|
|
141
|
+
expect(result.targets).toHaveLength(0);
|
|
142
|
+
expect(result.testMap.size).toBe(0);
|
|
143
|
+
expect(result.directTestMap.size).toBe(0);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
89
149
|
it('directTestMap only includes direct importers', async () => {
|
|
90
150
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-direct-'));
|
|
91
151
|
const srcDir = path.join(tmpDir, 'src');
|
|
@@ -242,6 +302,125 @@ describe('autoDiscoverTargetsAndTests', () => {
|
|
|
242
302
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
243
303
|
}
|
|
244
304
|
});
|
|
305
|
+
it('excludes files matching excludePaths prefix', async () => {
|
|
306
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-exclude-'));
|
|
307
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
308
|
+
const adminDir = path.join(srcDir, 'admin');
|
|
309
|
+
const adminFile = path.join(adminDir, 'restricted.ts');
|
|
310
|
+
const publicFile = path.join(srcDir, 'public.ts');
|
|
311
|
+
const testFile = path.join(srcDir, 'a.test.ts');
|
|
312
|
+
await fs.mkdir(adminDir, { recursive: true });
|
|
313
|
+
await fs.writeFile(adminFile, 'export const restricted = 1\n', 'utf8');
|
|
314
|
+
await fs.writeFile(publicFile, 'export const pub = 2\n', 'utf8');
|
|
315
|
+
const importLine = [
|
|
316
|
+
['im', 'port { pub } from "./public"'].join(''),
|
|
317
|
+
['im', 'port { restricted } from "./admin/restricted"'].join(''),
|
|
318
|
+
].join('\n');
|
|
319
|
+
await fs.writeFile(testFile, `${importLine}\n`, 'utf8');
|
|
320
|
+
try {
|
|
321
|
+
const { targets } = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
322
|
+
testPatterns: ['**/*.test.ts'],
|
|
323
|
+
excludePaths: ['src/admin'],
|
|
324
|
+
});
|
|
325
|
+
const targetFiles = targets.map((t) => normalizePath(typeof t === 'string' ? t : t.file));
|
|
326
|
+
expect(targetFiles.some((f) => f.includes('restricted.ts'))).toBe(false);
|
|
327
|
+
expect(targetFiles.some((f) => f.includes('public.ts'))).toBe(true);
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
it('excludes files matching excludePaths glob pattern', async () => {
|
|
334
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-excludeglob-'));
|
|
335
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
336
|
+
const adminDir = path.join(srcDir, 'admin');
|
|
337
|
+
const adminFile = path.join(adminDir, 'restricted.ts');
|
|
338
|
+
const publicFile = path.join(srcDir, 'public.ts');
|
|
339
|
+
const testFile = path.join(srcDir, 'a.test.ts');
|
|
340
|
+
await fs.mkdir(adminDir, { recursive: true });
|
|
341
|
+
await fs.writeFile(adminFile, 'export const restricted = 1\n', 'utf8');
|
|
342
|
+
await fs.writeFile(publicFile, 'export const pub = 2\n', 'utf8');
|
|
343
|
+
const importLine = [
|
|
344
|
+
['im', 'port { pub } from "./public"'].join(''),
|
|
345
|
+
['im', 'port { restricted } from "./admin/restricted"'].join(''),
|
|
346
|
+
].join('\n');
|
|
347
|
+
await fs.writeFile(testFile, `${importLine}\n`, 'utf8');
|
|
348
|
+
try {
|
|
349
|
+
const { targets } = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
350
|
+
testPatterns: ['**/*.test.ts'],
|
|
351
|
+
excludePaths: ['src/admin/**'],
|
|
352
|
+
});
|
|
353
|
+
const targetFiles = targets.map((t) => normalizePath(typeof t === 'string' ? t : t.file));
|
|
354
|
+
expect(targetFiles.some((f) => f.includes('restricted.ts'))).toBe(false);
|
|
355
|
+
expect(targetFiles.some((f) => f.includes('public.ts'))).toBe(true);
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
it('handles unreadable dependency gracefully', async () => {
|
|
362
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-unreadable-'));
|
|
363
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
364
|
+
const sourceFile = path.join(srcDir, 'source.ts');
|
|
365
|
+
const testFile = path.join(srcDir, 'source.test.ts');
|
|
366
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
367
|
+
// source.ts imports ./ghost which doesn't exist on disk
|
|
368
|
+
const ghostImport = ['im', 'port { x } from "./ghost"'].join('');
|
|
369
|
+
await fs.writeFile(sourceFile, `${ghostImport}\nexport const y = 1\n`, 'utf8');
|
|
370
|
+
const importLine = ['im', 'port { y } from "./source"'].join('');
|
|
371
|
+
await fs.writeFile(testFile, `${importLine}\n`, 'utf8');
|
|
372
|
+
try {
|
|
373
|
+
const { targets } = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
374
|
+
testPatterns: ['**/*.test.ts'],
|
|
375
|
+
});
|
|
376
|
+
const targetFiles = targets.map((t) => normalizePath(typeof t === 'string' ? t : t.file));
|
|
377
|
+
expect(targetFiles.some((f) => f.includes('source.ts'))).toBe(true);
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
it('uses default testPatterns when cfg.testPatterns is not set', async () => {
|
|
384
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-defaultpat-'));
|
|
385
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
386
|
+
const moduleFile = path.join(srcDir, 'foo.ts');
|
|
387
|
+
const testFile = path.join(srcDir, 'foo.spec.ts');
|
|
388
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
389
|
+
await fs.writeFile(moduleFile, 'export const foo = 1\n', 'utf8');
|
|
390
|
+
const importLine = ['im', 'port { foo } from "./foo"'].join('');
|
|
391
|
+
await fs.writeFile(testFile, `${importLine}\n`, 'utf8');
|
|
392
|
+
try {
|
|
393
|
+
// No testPatterns → uses TEST_PATTERNS_DEFAULT which includes **/*.spec.[jt]s?(x)
|
|
394
|
+
const { targets } = await autoDiscoverTargetsAndTests(tmpDir, {});
|
|
395
|
+
const targetFiles = targets.map((t) => normalizePath(typeof t === 'string' ? t : t.file));
|
|
396
|
+
expect(targetFiles).toContain(normalizePath(moduleFile));
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
it('assigns vue:script-setup kind to .vue source files', async () => {
|
|
403
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-vuekind-'));
|
|
404
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
405
|
+
const vueFile = path.join(srcDir, 'Comp.vue');
|
|
406
|
+
const testFile = path.join(srcDir, 'Comp.test.ts');
|
|
407
|
+
await fs.mkdir(srcDir, { recursive: true });
|
|
408
|
+
await fs.writeFile(vueFile, '<script setup>\nconst x = 1\n</script>\n', 'utf8');
|
|
409
|
+
const importLine = ['im', `port Comp from "./Comp.vue"`].join('');
|
|
410
|
+
await fs.writeFile(testFile, `${importLine}\n`, 'utf8');
|
|
411
|
+
try {
|
|
412
|
+
const { targets } = await autoDiscoverTargetsAndTests(tmpDir, {
|
|
413
|
+
testPatterns: ['**/*.test.ts'],
|
|
414
|
+
extensions: ['.vue', '.ts'],
|
|
415
|
+
});
|
|
416
|
+
const vueTarget = targets.find((t) => typeof t !== 'string' && normalizePath(t.file).endsWith('Comp.vue'));
|
|
417
|
+
expect(vueTarget).toBeDefined();
|
|
418
|
+
expect(typeof vueTarget !== 'string' && vueTarget?.kind).toBe('vue:script-setup');
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
245
424
|
it('ignores test files when collecting mutate targets', async () => {
|
|
246
425
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-discover-'));
|
|
247
426
|
const srcDir = path.join(tmpDir, 'src');
|
|
@@ -62,9 +62,7 @@ vi.mock('../ts-checker.js', () => ({
|
|
|
62
62
|
resolveTsconfigPath: vi.fn().mockReturnValue(undefined),
|
|
63
63
|
}));
|
|
64
64
|
vi.mock('../../core/schemata.js', () => ({
|
|
65
|
-
generateSchema: vi
|
|
66
|
-
.fn()
|
|
67
|
-
.mockReturnValue({
|
|
65
|
+
generateSchema: vi.fn().mockReturnValue({
|
|
68
66
|
schemaCode: '// @ts-nocheck\n',
|
|
69
67
|
fallbackIds: new Set(),
|
|
70
68
|
}),
|
|
@@ -72,11 +70,14 @@ vi.mock('../../core/schemata.js', () => ({
|
|
|
72
70
|
import { runOrchestrator, parseMutantTimeoutMs } from '../orchestrator.js';
|
|
73
71
|
import { loadMutineerConfig } from '../config.js';
|
|
74
72
|
import { createVitestAdapter } from '../vitest/index.js';
|
|
73
|
+
import { createJestAdapter } from '../jest/index.js';
|
|
75
74
|
import { autoDiscoverTargetsAndTests } from '../discover.js';
|
|
76
75
|
import { listChangedFiles } from '../changed.js';
|
|
77
76
|
import { executePool } from '../pool-executor.js';
|
|
78
77
|
import { prepareTasks } from '../tasks.js';
|
|
79
|
-
import { enumerateAllVariants } from '../variants.js';
|
|
78
|
+
import { enumerateAllVariants, getTargetFile } from '../variants.js';
|
|
79
|
+
import { resolveTypescriptEnabled, checkTypes } from '../ts-checker.js';
|
|
80
|
+
import { resolveCoverageConfig, loadCoverageAfterBaseline, } from '../coverage-resolver.js';
|
|
80
81
|
import { generateSchema } from '../../core/schemata.js';
|
|
81
82
|
import os from 'node:os';
|
|
82
83
|
import fssync from 'node:fs';
|
|
@@ -185,8 +186,8 @@ describe('runOrchestrator discovery logging', () => {
|
|
|
185
186
|
expect(consoleSpy).toHaveBeenCalledWith('Discovery complete: 3 source file(s), 2 test file(s)');
|
|
186
187
|
});
|
|
187
188
|
});
|
|
188
|
-
describe('runOrchestrator --changed-with-
|
|
189
|
-
it('logs uncovered targets when
|
|
189
|
+
describe('runOrchestrator --changed-with-imports diagnostic', () => {
|
|
190
|
+
it('logs uncovered targets when wantsChangedWithImports is true', async () => {
|
|
190
191
|
const cfg = {};
|
|
191
192
|
vi.mocked(loadMutineerConfig).mockResolvedValue(cfg);
|
|
192
193
|
const depFile = '/cwd/src/dep.ts';
|
|
@@ -197,10 +198,10 @@ describe('runOrchestrator --changed-with-deps diagnostic', () => {
|
|
|
197
198
|
directTestMap: new Map(),
|
|
198
199
|
});
|
|
199
200
|
const consoleSpy = vi.spyOn(console, 'log');
|
|
200
|
-
await runOrchestrator(['--changed-with-
|
|
201
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1 target(s) from --changed-with-
|
|
201
|
+
await runOrchestrator(['--changed-with-imports'], '/cwd');
|
|
202
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1 target(s) from --changed-with-imports have no covering tests and will be skipped'));
|
|
202
203
|
});
|
|
203
|
-
it('does not log when all changed-with-
|
|
204
|
+
it('does not log when all changed-with-imports targets have covering tests', async () => {
|
|
204
205
|
const cfg = {};
|
|
205
206
|
vi.mocked(loadMutineerConfig).mockResolvedValue(cfg);
|
|
206
207
|
const depFile = '/cwd/src/dep.ts';
|
|
@@ -213,11 +214,11 @@ describe('runOrchestrator --changed-with-deps diagnostic', () => {
|
|
|
213
214
|
});
|
|
214
215
|
vi.mocked(mockAdapter.runBaseline).mockResolvedValue(true);
|
|
215
216
|
const consoleSpy = vi.spyOn(console, 'log');
|
|
216
|
-
await runOrchestrator(['--changed-with-
|
|
217
|
+
await runOrchestrator(['--changed-with-imports'], '/cwd');
|
|
217
218
|
const diagnosticCalls = consoleSpy.mock.calls.filter(([msg]) => typeof msg === 'string' && msg.includes('have no covering tests'));
|
|
218
219
|
expect(diagnosticCalls).toHaveLength(0);
|
|
219
220
|
});
|
|
220
|
-
it('does not log diagnostic when
|
|
221
|
+
it('does not log diagnostic when wantsChangedWithImports is false', async () => {
|
|
221
222
|
const cfg = {};
|
|
222
223
|
vi.mocked(loadMutineerConfig).mockResolvedValue(cfg);
|
|
223
224
|
const depFile = '/cwd/src/dep.ts';
|
|
@@ -334,6 +335,40 @@ describe('runOrchestrator shard filtering', () => {
|
|
|
334
335
|
expect(call.shard).toEqual({ index: 1, total: 4 });
|
|
335
336
|
});
|
|
336
337
|
});
|
|
338
|
+
describe('runOrchestrator target ordering', () => {
|
|
339
|
+
const testFile = '/cwd/src/__tests__/foo.spec.ts';
|
|
340
|
+
beforeEach(() => {
|
|
341
|
+
vi.clearAllMocks();
|
|
342
|
+
process.exitCode = undefined;
|
|
343
|
+
vi.mocked(createVitestAdapter).mockReturnValue(mockAdapter);
|
|
344
|
+
// No explicit targets -- use auto-discovery
|
|
345
|
+
vi.mocked(loadMutineerConfig).mockResolvedValue({});
|
|
346
|
+
vi.mocked(mockAdapter.runBaseline).mockResolvedValue(true);
|
|
347
|
+
vi.mocked(enumerateAllVariants).mockResolvedValue([]);
|
|
348
|
+
vi.mocked(prepareTasks).mockReturnValue([]);
|
|
349
|
+
});
|
|
350
|
+
afterEach(() => {
|
|
351
|
+
process.exitCode = undefined;
|
|
352
|
+
});
|
|
353
|
+
it('sorts auto-discovered targets alphabetically before enumeration', async () => {
|
|
354
|
+
const fileA = '/cwd/src/aaa.ts';
|
|
355
|
+
const fileB = '/cwd/src/bbb.ts';
|
|
356
|
+
const fileC = '/cwd/src/ccc.ts';
|
|
357
|
+
// Return targets in reverse-alphabetical order (simulating non-deterministic fs)
|
|
358
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
359
|
+
targets: [fileC, fileA, fileB],
|
|
360
|
+
testMap: new Map([
|
|
361
|
+
[fileA, new Set([testFile])],
|
|
362
|
+
[fileB, new Set([testFile])],
|
|
363
|
+
[fileC, new Set([testFile])],
|
|
364
|
+
]),
|
|
365
|
+
directTestMap: new Map(),
|
|
366
|
+
});
|
|
367
|
+
await runOrchestrator([], '/cwd');
|
|
368
|
+
const call = vi.mocked(enumerateAllVariants).mock.calls[0][0];
|
|
369
|
+
expect(call.targets).toEqual([fileA, fileB, fileC]);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
337
372
|
describe('runOrchestrator --skip-baseline', () => {
|
|
338
373
|
const targetFile = '/cwd/src/foo.ts';
|
|
339
374
|
const testFile = '/cwd/src/__tests__/foo.spec.ts';
|
|
@@ -469,3 +504,293 @@ describe('runOrchestrator schema generation', () => {
|
|
|
469
504
|
expect(mockLogDebug).toHaveBeenCalledWith(expect.stringMatching(/Schema: 1 embedded, 0 fallback/));
|
|
470
505
|
});
|
|
471
506
|
});
|
|
507
|
+
describe('runOrchestrator jest runner', () => {
|
|
508
|
+
const targetFile = '/cwd/src/foo.ts';
|
|
509
|
+
const testFile = '/cwd/src/__tests__/foo.spec.ts';
|
|
510
|
+
beforeEach(() => {
|
|
511
|
+
vi.clearAllMocks();
|
|
512
|
+
process.exitCode = undefined;
|
|
513
|
+
vi.mocked(loadMutineerConfig).mockResolvedValue({});
|
|
514
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
515
|
+
targets: [targetFile],
|
|
516
|
+
testMap: new Map([[targetFile, new Set([testFile])]]),
|
|
517
|
+
directTestMap: new Map(),
|
|
518
|
+
});
|
|
519
|
+
vi.mocked(createJestAdapter).mockReturnValue(mockAdapter);
|
|
520
|
+
vi.mocked(mockAdapter.runBaseline).mockResolvedValue(true);
|
|
521
|
+
});
|
|
522
|
+
afterEach(() => {
|
|
523
|
+
process.exitCode = undefined;
|
|
524
|
+
});
|
|
525
|
+
it('uses jest adapter when --runner jest is passed', async () => {
|
|
526
|
+
await runOrchestrator(['--runner', 'jest'], '/cwd');
|
|
527
|
+
expect(createJestAdapter).toHaveBeenCalled();
|
|
528
|
+
expect(createVitestAdapter).not.toHaveBeenCalled();
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
describe('runOrchestrator vitestProject', () => {
|
|
532
|
+
const targetFile = '/cwd/src/foo.ts';
|
|
533
|
+
const testFile = '/cwd/src/__tests__/foo.spec.ts';
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
vi.clearAllMocks();
|
|
536
|
+
process.exitCode = undefined;
|
|
537
|
+
vi.mocked(createVitestAdapter).mockReturnValue(mockAdapter);
|
|
538
|
+
vi.mocked(mockAdapter.runBaseline).mockResolvedValue(true);
|
|
539
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
540
|
+
targets: [targetFile],
|
|
541
|
+
testMap: new Map([[targetFile, new Set([testFile])]]),
|
|
542
|
+
directTestMap: new Map(),
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
afterEach(() => {
|
|
546
|
+
process.exitCode = undefined;
|
|
547
|
+
});
|
|
548
|
+
it('passes vitestProject from config to adapter', async () => {
|
|
549
|
+
vi.mocked(loadMutineerConfig).mockResolvedValue({
|
|
550
|
+
vitestProject: 'my-project',
|
|
551
|
+
});
|
|
552
|
+
await runOrchestrator([], '/cwd');
|
|
553
|
+
expect(createVitestAdapter).toHaveBeenCalledWith(expect.objectContaining({ vitestProject: 'my-project' }));
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
describe('runOrchestrator --changed filter', () => {
|
|
557
|
+
afterEach(() => {
|
|
558
|
+
process.exitCode = undefined;
|
|
559
|
+
});
|
|
560
|
+
it('skips target not in changedAbs and exits with no tests error', async () => {
|
|
561
|
+
vi.mocked(loadMutineerConfig).mockResolvedValue({});
|
|
562
|
+
const targetFile = '/cwd/src/foo.ts';
|
|
563
|
+
const unchangedFile = '/cwd/src/bar.ts';
|
|
564
|
+
vi.mocked(listChangedFiles).mockReturnValue([targetFile]); // only targetFile is changed
|
|
565
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
566
|
+
targets: [unchangedFile], // bar.ts is not changed -> filtered out
|
|
567
|
+
testMap: new Map([
|
|
568
|
+
[unchangedFile, new Set(['/cwd/src/__tests__/bar.spec.ts'])],
|
|
569
|
+
]),
|
|
570
|
+
directTestMap: new Map(),
|
|
571
|
+
});
|
|
572
|
+
await runOrchestrator(['--changed'], '/cwd');
|
|
573
|
+
// With no tests because bar.ts was filtered, should hit the no-tests error
|
|
574
|
+
expect(process.exitCode).toBe(1);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
describe('runOrchestrator baseline failure', () => {
|
|
578
|
+
const targetFile = '/cwd/src/foo.ts';
|
|
579
|
+
const testFile = '/cwd/src/__tests__/foo.spec.ts';
|
|
580
|
+
beforeEach(() => {
|
|
581
|
+
vi.clearAllMocks();
|
|
582
|
+
process.exitCode = undefined;
|
|
583
|
+
vi.mocked(createVitestAdapter).mockReturnValue(mockAdapter);
|
|
584
|
+
vi.mocked(loadMutineerConfig).mockResolvedValue({});
|
|
585
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
586
|
+
targets: [targetFile],
|
|
587
|
+
testMap: new Map([[targetFile, new Set([testFile])]]),
|
|
588
|
+
directTestMap: new Map(),
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
afterEach(() => {
|
|
592
|
+
process.exitCode = undefined;
|
|
593
|
+
});
|
|
594
|
+
it('sets exitCode=1 and returns when baseline fails', async () => {
|
|
595
|
+
vi.mocked(mockAdapter.runBaseline).mockResolvedValue(false);
|
|
596
|
+
await runOrchestrator([], '/cwd');
|
|
597
|
+
expect(process.exitCode).toBe(1);
|
|
598
|
+
expect(enumerateAllVariants).not.toHaveBeenCalled();
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
describe('runOrchestrator TypeScript checking', () => {
|
|
602
|
+
let tmpDir;
|
|
603
|
+
const testFile = '/cwd/src/__tests__/foo.spec.ts';
|
|
604
|
+
function makeVariant(id, file) {
|
|
605
|
+
return {
|
|
606
|
+
id,
|
|
607
|
+
name: 'returnNull',
|
|
608
|
+
file,
|
|
609
|
+
code: 'return null',
|
|
610
|
+
line: 1,
|
|
611
|
+
col: 0,
|
|
612
|
+
tests: [testFile],
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
beforeEach(() => {
|
|
616
|
+
tmpDir = fssync.mkdtempSync(path.join(os.tmpdir(), 'mutineer-orch-ts-'));
|
|
617
|
+
vi.clearAllMocks();
|
|
618
|
+
process.exitCode = undefined;
|
|
619
|
+
vi.mocked(createVitestAdapter).mockReturnValue(mockAdapter);
|
|
620
|
+
vi.mocked(loadMutineerConfig).mockResolvedValue({});
|
|
621
|
+
vi.mocked(mockAdapter.runBaseline).mockResolvedValue(true);
|
|
622
|
+
});
|
|
623
|
+
afterEach(() => {
|
|
624
|
+
process.exitCode = undefined;
|
|
625
|
+
fssync.rmSync(tmpDir, { recursive: true, force: true });
|
|
626
|
+
});
|
|
627
|
+
it('filters compile-error variants and populates cache when TS enabled', async () => {
|
|
628
|
+
const sourceFile = path.join(tmpDir, 'source.ts');
|
|
629
|
+
fssync.writeFileSync(sourceFile, 'const x = 1', 'utf8');
|
|
630
|
+
const goodVariant = makeVariant('source.ts#0', sourceFile);
|
|
631
|
+
const badVariant = makeVariant('source.ts#1', sourceFile);
|
|
632
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
633
|
+
targets: [sourceFile],
|
|
634
|
+
testMap: new Map([[sourceFile, new Set([testFile])]]),
|
|
635
|
+
directTestMap: new Map(),
|
|
636
|
+
});
|
|
637
|
+
vi.mocked(enumerateAllVariants).mockResolvedValue([goodVariant, badVariant]);
|
|
638
|
+
vi.mocked(resolveTypescriptEnabled).mockReturnValue(true);
|
|
639
|
+
vi.mocked(checkTypes).mockResolvedValue(new Set(['source.ts#1']));
|
|
640
|
+
vi.mocked(prepareTasks)
|
|
641
|
+
.mockReturnValueOnce([
|
|
642
|
+
{ key: 'source.ts#1', v: badVariant, tests: [testFile] },
|
|
643
|
+
]) // compile-error tasks
|
|
644
|
+
.mockReturnValueOnce([
|
|
645
|
+
{ key: 'source.ts#0', v: goodVariant, tests: [testFile] },
|
|
646
|
+
]); // runnable tasks
|
|
647
|
+
await runOrchestrator([], tmpDir);
|
|
648
|
+
// executePool should only receive the good variant
|
|
649
|
+
const poolCall = vi.mocked(executePool).mock.calls[0][0];
|
|
650
|
+
expect(poolCall.tasks.map((t) => t.key)).toEqual(['source.ts#0']);
|
|
651
|
+
});
|
|
652
|
+
it('logs filtered compile-error count when TS enabled', async () => {
|
|
653
|
+
const sourceFile = path.join(tmpDir, 'source.ts');
|
|
654
|
+
fssync.writeFileSync(sourceFile, 'const x = 1', 'utf8');
|
|
655
|
+
const badVariant = makeVariant('source.ts#0', sourceFile);
|
|
656
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
657
|
+
targets: [sourceFile],
|
|
658
|
+
testMap: new Map([[sourceFile, new Set([testFile])]]),
|
|
659
|
+
directTestMap: new Map(),
|
|
660
|
+
});
|
|
661
|
+
vi.mocked(enumerateAllVariants).mockResolvedValue([badVariant]);
|
|
662
|
+
vi.mocked(resolveTypescriptEnabled).mockReturnValue(true);
|
|
663
|
+
vi.mocked(checkTypes).mockResolvedValue(new Set(['source.ts#0']));
|
|
664
|
+
vi.mocked(prepareTasks).mockReturnValue([
|
|
665
|
+
{ key: 'source.ts#0', v: badVariant, tests: [testFile] },
|
|
666
|
+
]);
|
|
667
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
668
|
+
await runOrchestrator([], tmpDir);
|
|
669
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('TypeScript: filtered 1 mutant(s) with compile errors'));
|
|
670
|
+
});
|
|
671
|
+
it('calls executePool with empty tasks when all variants are compile errors', async () => {
|
|
672
|
+
const sourceFile = path.join(tmpDir, 'source.ts');
|
|
673
|
+
fssync.writeFileSync(sourceFile, 'const x = 1', 'utf8');
|
|
674
|
+
const badVariant = makeVariant('source.ts#0', sourceFile);
|
|
675
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
676
|
+
targets: [sourceFile],
|
|
677
|
+
testMap: new Map([[sourceFile, new Set([testFile])]]),
|
|
678
|
+
directTestMap: new Map(),
|
|
679
|
+
});
|
|
680
|
+
vi.mocked(enumerateAllVariants).mockResolvedValue([badVariant]);
|
|
681
|
+
vi.mocked(resolveTypescriptEnabled).mockReturnValue(true);
|
|
682
|
+
vi.mocked(checkTypes).mockResolvedValue(new Set(['source.ts#0']));
|
|
683
|
+
// First call: compile-error cache population; second call: runnable variants (empty)
|
|
684
|
+
vi.mocked(prepareTasks)
|
|
685
|
+
.mockReturnValueOnce([
|
|
686
|
+
{ key: 'source.ts#0', v: badVariant, tests: [testFile] },
|
|
687
|
+
])
|
|
688
|
+
.mockReturnValueOnce([]);
|
|
689
|
+
await runOrchestrator([], tmpDir);
|
|
690
|
+
// executePool is still called, but with empty tasks
|
|
691
|
+
const poolCall = vi.mocked(executePool).mock.calls[0][0];
|
|
692
|
+
expect(poolCall.tasks).toEqual([]);
|
|
693
|
+
});
|
|
694
|
+
it('runs TS checking but skips filtering when no compile errors found', async () => {
|
|
695
|
+
const sourceFile = path.join(tmpDir, 'source.ts');
|
|
696
|
+
fssync.writeFileSync(sourceFile, 'const x = 1', 'utf8');
|
|
697
|
+
const goodVariant = makeVariant('source.ts#0', sourceFile);
|
|
698
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
699
|
+
targets: [sourceFile],
|
|
700
|
+
testMap: new Map([[sourceFile, new Set([testFile])]]),
|
|
701
|
+
directTestMap: new Map(),
|
|
702
|
+
});
|
|
703
|
+
vi.mocked(enumerateAllVariants).mockResolvedValue([goodVariant]);
|
|
704
|
+
vi.mocked(resolveTypescriptEnabled).mockReturnValue(true);
|
|
705
|
+
vi.mocked(checkTypes).mockResolvedValue(new Set()); // no compile errors
|
|
706
|
+
vi.mocked(prepareTasks).mockReturnValue([
|
|
707
|
+
{ key: 'source.ts#0', v: goodVariant, tests: [testFile] },
|
|
708
|
+
]);
|
|
709
|
+
await runOrchestrator([], tmpDir);
|
|
710
|
+
// All variants are runnable; executePool receives the full task list
|
|
711
|
+
const poolCall = vi.mocked(executePool).mock.calls[0][0];
|
|
712
|
+
expect(poolCall.tasks.map((t) => t.key)).toEqual(['source.ts#0']);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
describe('runOrchestrator misc branches', () => {
|
|
716
|
+
const targetFile = '/cwd/src/foo.ts';
|
|
717
|
+
const testFile = '/cwd/src/__tests__/foo.spec.ts';
|
|
718
|
+
beforeEach(() => {
|
|
719
|
+
vi.clearAllMocks();
|
|
720
|
+
process.exitCode = undefined;
|
|
721
|
+
vi.mocked(createVitestAdapter).mockReturnValue(mockAdapter);
|
|
722
|
+
vi.mocked(loadMutineerConfig).mockResolvedValue({});
|
|
723
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
724
|
+
targets: [targetFile],
|
|
725
|
+
testMap: new Map([[targetFile, new Set([testFile])]]),
|
|
726
|
+
directTestMap: new Map(),
|
|
727
|
+
});
|
|
728
|
+
vi.mocked(mockAdapter.runBaseline).mockResolvedValue(true);
|
|
729
|
+
// Reset mocks that may carry over from other describe blocks
|
|
730
|
+
vi.mocked(resolveCoverageConfig).mockResolvedValue({
|
|
731
|
+
enableCoverageForBaseline: false,
|
|
732
|
+
wantsPerTestCoverage: false,
|
|
733
|
+
coverageData: null,
|
|
734
|
+
});
|
|
735
|
+
vi.mocked(loadCoverageAfterBaseline).mockResolvedValue({
|
|
736
|
+
coverageData: null,
|
|
737
|
+
perTestCoverage: null,
|
|
738
|
+
});
|
|
739
|
+
vi.mocked(resolveTypescriptEnabled).mockReturnValue(false);
|
|
740
|
+
vi.mocked(checkTypes).mockResolvedValue(new Set());
|
|
741
|
+
vi.mocked(enumerateAllVariants).mockResolvedValue([]);
|
|
742
|
+
vi.mocked(prepareTasks).mockReturnValue([]);
|
|
743
|
+
vi.mocked(getTargetFile).mockImplementation((t) => typeof t === 'string' ? t : t.file);
|
|
744
|
+
});
|
|
745
|
+
afterEach(() => {
|
|
746
|
+
process.exitCode = undefined;
|
|
747
|
+
});
|
|
748
|
+
it('logs "only covered lines" when --only-covered-lines is passed', async () => {
|
|
749
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
750
|
+
await runOrchestrator(['--only-covered-lines'], '/cwd');
|
|
751
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('only covered lines'));
|
|
752
|
+
});
|
|
753
|
+
it('uses autoDiscover=false to return empty targets when no cfg.targets set', async () => {
|
|
754
|
+
vi.mocked(loadMutineerConfig).mockResolvedValue({ autoDiscover: false });
|
|
755
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
756
|
+
targets: [targetFile],
|
|
757
|
+
testMap: new Map(),
|
|
758
|
+
directTestMap: new Map(),
|
|
759
|
+
});
|
|
760
|
+
await runOrchestrator([], '/cwd');
|
|
761
|
+
// With autoDiscover=false and no cfg.targets, targets is [], no tests found, exit=1
|
|
762
|
+
expect(process.exitCode).toBe(1);
|
|
763
|
+
});
|
|
764
|
+
it('logs coverage collection message when enableCoverageForBaseline is true', async () => {
|
|
765
|
+
vi.mocked(resolveCoverageConfig).mockResolvedValue({
|
|
766
|
+
enableCoverageForBaseline: true,
|
|
767
|
+
wantsPerTestCoverage: false,
|
|
768
|
+
coverageData: null,
|
|
769
|
+
});
|
|
770
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
771
|
+
await runOrchestrator([], '/cwd');
|
|
772
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('collecting coverage'));
|
|
773
|
+
});
|
|
774
|
+
it('logs "no mutants" with coverage note when coverageData is non-null', async () => {
|
|
775
|
+
vi.mocked(loadCoverageAfterBaseline).mockResolvedValue({
|
|
776
|
+
coverageData: new Map(), // non-null
|
|
777
|
+
perTestCoverage: null,
|
|
778
|
+
});
|
|
779
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
780
|
+
await runOrchestrator([], '/cwd');
|
|
781
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('uncovered lines'));
|
|
782
|
+
});
|
|
783
|
+
it('collects test files for relative target path', async () => {
|
|
784
|
+
const relTarget = 'src/foo.ts';
|
|
785
|
+
vi.mocked(getTargetFile).mockImplementation((t) => typeof t === 'string' ? relTarget : t.file);
|
|
786
|
+
vi.mocked(autoDiscoverTargetsAndTests).mockResolvedValue({
|
|
787
|
+
targets: [relTarget],
|
|
788
|
+
testMap: new Map([[targetFile, new Set([testFile])]]),
|
|
789
|
+
directTestMap: new Map(),
|
|
790
|
+
});
|
|
791
|
+
// Relative path gets joined with cwd — should match the testMap key
|
|
792
|
+
await runOrchestrator([], '/cwd');
|
|
793
|
+
// runBaseline should be called since test was found via relative path join
|
|
794
|
+
expect(mockAdapter.runBaseline).toHaveBeenCalled();
|
|
795
|
+
});
|
|
796
|
+
});
|