@mutineerjs/mutineer 0.10.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 +10 -10
- package/dist/__tests__/index.spec.d.ts +1 -0
- 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 +4 -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 +301 -8
- 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 -46
- package/dist/runner/jest/pool.js +4 -10
- package/dist/runner/jest/worker-runtime.js +2 -1
- package/dist/runner/orchestrator.js +7 -7
- 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 +254 -0
- package/dist/runner/vitest/__tests__/plugin.spec.js +28 -1
- package/dist/runner/vitest/__tests__/pool.spec.js +674 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +116 -1
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +21 -0
- package/dist/runner/vitest/adapter.js +8 -46
- package/dist/runner/vitest/plugin.js +1 -7
- package/dist/runner/vitest/pool.js +5 -19
- package/dist/runner/vitest/redirect-loader.js +3 -1
- package/dist/runner/vitest/worker-runtime.js +2 -1
- package/dist/types/config.d.ts +2 -2
- 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 +56 -0
- package/dist/utils/coverage.js +3 -4
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +6 -0
- package/package.json +1 -1
|
@@ -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');
|
|
@@ -70,11 +70,14 @@ vi.mock('../../core/schemata.js', () => ({
|
|
|
70
70
|
import { runOrchestrator, parseMutantTimeoutMs } from '../orchestrator.js';
|
|
71
71
|
import { loadMutineerConfig } from '../config.js';
|
|
72
72
|
import { createVitestAdapter } from '../vitest/index.js';
|
|
73
|
+
import { createJestAdapter } from '../jest/index.js';
|
|
73
74
|
import { autoDiscoverTargetsAndTests } from '../discover.js';
|
|
74
75
|
import { listChangedFiles } from '../changed.js';
|
|
75
76
|
import { executePool } from '../pool-executor.js';
|
|
76
77
|
import { prepareTasks } from '../tasks.js';
|
|
77
|
-
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';
|
|
78
81
|
import { generateSchema } from '../../core/schemata.js';
|
|
79
82
|
import os from 'node:os';
|
|
80
83
|
import fssync from 'node:fs';
|
|
@@ -183,8 +186,8 @@ describe('runOrchestrator discovery logging', () => {
|
|
|
183
186
|
expect(consoleSpy).toHaveBeenCalledWith('Discovery complete: 3 source file(s), 2 test file(s)');
|
|
184
187
|
});
|
|
185
188
|
});
|
|
186
|
-
describe('runOrchestrator --changed-with-
|
|
187
|
-
it('logs uncovered targets when
|
|
189
|
+
describe('runOrchestrator --changed-with-imports diagnostic', () => {
|
|
190
|
+
it('logs uncovered targets when wantsChangedWithImports is true', async () => {
|
|
188
191
|
const cfg = {};
|
|
189
192
|
vi.mocked(loadMutineerConfig).mockResolvedValue(cfg);
|
|
190
193
|
const depFile = '/cwd/src/dep.ts';
|
|
@@ -195,10 +198,10 @@ describe('runOrchestrator --changed-with-deps diagnostic', () => {
|
|
|
195
198
|
directTestMap: new Map(),
|
|
196
199
|
});
|
|
197
200
|
const consoleSpy = vi.spyOn(console, 'log');
|
|
198
|
-
await runOrchestrator(['--changed-with-
|
|
199
|
-
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'));
|
|
200
203
|
});
|
|
201
|
-
it('does not log when all changed-with-
|
|
204
|
+
it('does not log when all changed-with-imports targets have covering tests', async () => {
|
|
202
205
|
const cfg = {};
|
|
203
206
|
vi.mocked(loadMutineerConfig).mockResolvedValue(cfg);
|
|
204
207
|
const depFile = '/cwd/src/dep.ts';
|
|
@@ -211,11 +214,11 @@ describe('runOrchestrator --changed-with-deps diagnostic', () => {
|
|
|
211
214
|
});
|
|
212
215
|
vi.mocked(mockAdapter.runBaseline).mockResolvedValue(true);
|
|
213
216
|
const consoleSpy = vi.spyOn(console, 'log');
|
|
214
|
-
await runOrchestrator(['--changed-with-
|
|
217
|
+
await runOrchestrator(['--changed-with-imports'], '/cwd');
|
|
215
218
|
const diagnosticCalls = consoleSpy.mock.calls.filter(([msg]) => typeof msg === 'string' && msg.includes('have no covering tests'));
|
|
216
219
|
expect(diagnosticCalls).toHaveLength(0);
|
|
217
220
|
});
|
|
218
|
-
it('does not log diagnostic when
|
|
221
|
+
it('does not log diagnostic when wantsChangedWithImports is false', async () => {
|
|
219
222
|
const cfg = {};
|
|
220
223
|
vi.mocked(loadMutineerConfig).mockResolvedValue(cfg);
|
|
221
224
|
const depFile = '/cwd/src/dep.ts';
|
|
@@ -501,3 +504,293 @@ describe('runOrchestrator schema generation', () => {
|
|
|
501
504
|
expect(mockLogDebug).toHaveBeenCalledWith(expect.stringMatching(/Schema: 1 embedded, 0 fallback/));
|
|
502
505
|
});
|
|
503
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
|
+
});
|
|
@@ -211,6 +211,22 @@ describe('executePool', () => {
|
|
|
211
211
|
// default file should NOT exist
|
|
212
212
|
await expect(fs.access(path.join(tmpDir, '.mutineer-cache.json'))).rejects.toThrow();
|
|
213
213
|
});
|
|
214
|
+
it('writes mutineer-report.json when reportFormat=json without shard', async () => {
|
|
215
|
+
const adapter = makeAdapter();
|
|
216
|
+
const cache = {};
|
|
217
|
+
await executePool({
|
|
218
|
+
tasks: [makeTask({ key: 'json-no-shard-key' })],
|
|
219
|
+
adapter,
|
|
220
|
+
cache,
|
|
221
|
+
concurrency: 1,
|
|
222
|
+
progressMode: 'list',
|
|
223
|
+
cwd: tmpDir,
|
|
224
|
+
reportFormat: 'json',
|
|
225
|
+
});
|
|
226
|
+
const reportFile = path.join(tmpDir, 'mutineer-report.json');
|
|
227
|
+
const content = JSON.parse(await fs.readFile(reportFile, 'utf8'));
|
|
228
|
+
expect(content.schemaVersion).toBe(1);
|
|
229
|
+
});
|
|
214
230
|
it('writes shard-suffixed JSON report when shard and reportFormat=json are set', async () => {
|
|
215
231
|
const adapter = makeAdapter();
|
|
216
232
|
const cache = {};
|
|
@@ -516,6 +532,67 @@ describe('executePool', () => {
|
|
|
516
532
|
expect(cacheStringifyCalls.length).toBe(1);
|
|
517
533
|
stringifySpy.mockRestore();
|
|
518
534
|
});
|
|
535
|
+
it('uses bar mode for Progress when progressMode is bar', async () => {
|
|
536
|
+
const adapter = makeAdapter();
|
|
537
|
+
const cache = {};
|
|
538
|
+
// Should not throw, and bar mode writes to stderr (no isTTY in tests so uses console.log)
|
|
539
|
+
await executePool({
|
|
540
|
+
tasks: [makeTask({ key: 'bar-mode-key' })],
|
|
541
|
+
adapter,
|
|
542
|
+
cache,
|
|
543
|
+
concurrency: 1,
|
|
544
|
+
progressMode: 'bar',
|
|
545
|
+
cwd: tmpDir,
|
|
546
|
+
});
|
|
547
|
+
expect(cache['bar-mode-key'].status).toBe('killed');
|
|
548
|
+
});
|
|
549
|
+
it('stores passingTests when result contains passingTests', async () => {
|
|
550
|
+
const tmpFile = path.join(tmpDir, 'passing.ts');
|
|
551
|
+
await fs.writeFile(tmpFile, 'const x = a + b\n');
|
|
552
|
+
const adapter = makeAdapter({
|
|
553
|
+
runMutant: vi.fn().mockResolvedValue({
|
|
554
|
+
status: 'escaped',
|
|
555
|
+
durationMs: 1,
|
|
556
|
+
passingTests: ['/tests/foo.spec.ts'],
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
const cache = {};
|
|
560
|
+
const task = makeTask({
|
|
561
|
+
key: 'passing-key',
|
|
562
|
+
v: {
|
|
563
|
+
id: 'passing.ts#0',
|
|
564
|
+
name: 'flipArith',
|
|
565
|
+
file: tmpFile,
|
|
566
|
+
code: 'const x = a - b\n',
|
|
567
|
+
line: 1,
|
|
568
|
+
col: 10,
|
|
569
|
+
tests: ['/tests/foo.spec.ts'],
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
await executePool({
|
|
573
|
+
tasks: [task],
|
|
574
|
+
adapter,
|
|
575
|
+
cache,
|
|
576
|
+
concurrency: 1,
|
|
577
|
+
progressMode: 'list',
|
|
578
|
+
cwd: tmpDir,
|
|
579
|
+
});
|
|
580
|
+
expect(cache['passing-key'].passingTests).toEqual(['/tests/foo.spec.ts']);
|
|
581
|
+
});
|
|
582
|
+
it('sets exitCode and logs no-mutants note when minKillPercent is set but no mutants evaluated', async () => {
|
|
583
|
+
const adapter = makeAdapter();
|
|
584
|
+
const cache = {};
|
|
585
|
+
await executePool({
|
|
586
|
+
tasks: [],
|
|
587
|
+
adapter,
|
|
588
|
+
cache,
|
|
589
|
+
concurrency: 1,
|
|
590
|
+
progressMode: 'list',
|
|
591
|
+
minKillPercent: 80,
|
|
592
|
+
cwd: tmpDir,
|
|
593
|
+
});
|
|
594
|
+
expect(process.exitCode).toBe(1);
|
|
595
|
+
});
|
|
519
596
|
it('handles adapter errors gracefully and still shuts down', async () => {
|
|
520
597
|
const adapter = makeAdapter({
|
|
521
598
|
runMutant: vi.fn().mockRejectedValue(new Error('adapter failure')),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|