@hughescr/stryker-bun-runner 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4603,6 +4603,10 @@ var Scope;
4603
4603
  Scope2["Transient"] = "transient";
4604
4604
  Scope2["Singleton"] = "singleton";
4605
4605
  })(Scope || (Scope = {}));
4606
+ // src/bun-test-runner.ts
4607
+ import * as fsPromises2 from "node:fs/promises";
4608
+ import { tmpdir } from "node:os";
4609
+ import path4 from "node:path";
4606
4610
  // node_modules/@stryker-mutator/api/dist/src/test-runner/test-status.js
4607
4611
  var TestStatus;
4608
4612
  (function(TestStatus2) {
@@ -4625,267 +4629,9 @@ var DryRunStatus;
4625
4629
  DryRunStatus2["Error"] = "error";
4626
4630
  DryRunStatus2["Timeout"] = "timeout";
4627
4631
  })(DryRunStatus || (DryRunStatus = {}));
4628
- // src/process-runner.ts
4629
- import { spawn } from "node:child_process";
4630
- async function runBunTests(options) {
4631
- const args = ["test"];
4632
- if (options.inspectWaitPort) {
4633
- args.push(`--inspect=${options.inspectWaitPort}`);
4634
- }
4635
- if (options.bunfigPath) {
4636
- args.push(`--config=${options.bunfigPath}`);
4637
- }
4638
- if (options.preloadScript) {
4639
- args.push("--preload", options.preloadScript);
4640
- }
4641
- if (options.testNamePattern) {
4642
- args.push("--test-name-pattern", options.testNamePattern);
4643
- }
4644
- if (options.bail) {
4645
- args.push("--bail");
4646
- }
4647
- if (options.sequentialMode) {
4648
- args.push("--concurrency=1");
4649
- }
4650
- if (options.bunArgs && options.bunArgs.length > 0) {
4651
- args.push(...options.bunArgs);
4652
- }
4653
- if (options.testFiles && options.testFiles.length > 0) {
4654
- args.push(...options.testFiles);
4655
- }
4656
- const env = {
4657
- ...process.env,
4658
- ...options.env
4659
- };
4660
- if (options.activeMutant) {
4661
- env.__STRYKER_ACTIVE_MUTANT__ = options.activeMutant;
4662
- }
4663
- if (options.coverageFile) {
4664
- env.__STRYKER_COVERAGE_FILE__ = options.coverageFile;
4665
- }
4666
- if (options.syncPort) {
4667
- env.__STRYKER_SYNC_PORT__ = String(options.syncPort);
4668
- }
4669
- return new Promise((resolve) => {
4670
- const stdoutChunks = [];
4671
- const stderrChunks = [];
4672
- let timedOut = false;
4673
- let processKilled = false;
4674
- const childProcess = spawn(options.bunPath, args, {
4675
- env,
4676
- stdio: ["ignore", "pipe", "pipe"],
4677
- cwd: process.cwd()
4678
- });
4679
- const timeoutHandle = setTimeout(() => {
4680
- timedOut = true;
4681
- processKilled = true;
4682
- childProcess.kill("SIGKILL");
4683
- }, options.timeout);
4684
- if (childProcess.stdout) {
4685
- childProcess.stdout.on("data", (data) => {
4686
- stdoutChunks.push(data);
4687
- });
4688
- }
4689
- let inspectorUrlExtracted = false;
4690
- if (childProcess.stderr) {
4691
- childProcess.stderr.on("data", (data) => {
4692
- stderrChunks.push(data);
4693
- if (options.inspectWaitPort && !inspectorUrlExtracted && options.onInspectorReady) {
4694
- const text = Buffer.concat(stderrChunks).toString();
4695
- const match = /Listening:[\t\v\f\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*\n\s*(ws:\/\/\S+)/.exec(text);
4696
- if (match) {
4697
- inspectorUrlExtracted = true;
4698
- options.onInspectorReady(match[1]);
4699
- }
4700
- }
4701
- });
4702
- }
4703
- childProcess.on("close", (code) => {
4704
- clearTimeout(timeoutHandle);
4705
- resolve({
4706
- stdout: Buffer.concat(stdoutChunks).toString(),
4707
- stderr: Buffer.concat(stderrChunks).toString(),
4708
- exitCode: processKilled ? null : code,
4709
- timedOut
4710
- });
4711
- });
4712
- childProcess.on("error", (error) => {
4713
- clearTimeout(timeoutHandle);
4714
- const stderrOutput = Buffer.concat(stderrChunks).toString();
4715
- resolve({
4716
- stdout: Buffer.concat(stdoutChunks).toString(),
4717
- stderr: `${stderrOutput}
4718
- Process error: ${error.message}`,
4719
- exitCode: null,
4720
- timedOut
4721
- });
4722
- });
4723
- });
4724
- }
4725
-
4726
- // src/parsers/console-parser.ts
4727
- function parseFilePath(line) {
4728
- const fileMatch = /^([\w./-]+\.(?:test|spec)\.(?:ts|tsx|js|jsx|mts|mjs)):$/.exec(line);
4729
- return fileMatch ? fileMatch[1] : null;
4730
- }
4731
- function parseTestLine(line, currentFile) {
4732
- const passMatch = /^✓ +(\S.*?) \[([0-9.]+)ms\]$/.exec(line);
4733
- if (passMatch) {
4734
- const testName = passMatch[1].trim();
4735
- const fullName = currentFile ? `${currentFile} > ${testName}` : testName;
4736
- return {
4737
- test: {
4738
- name: fullName,
4739
- file: currentFile,
4740
- status: "passed",
4741
- duration: parseFloat(passMatch[2])
4742
- }
4743
- };
4744
- }
4745
- const failMatch = /^✗ +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
4746
- if (failMatch) {
4747
- const testName = failMatch[1].trim();
4748
- const fullName = currentFile ? `${currentFile} > ${testName}` : testName;
4749
- return {
4750
- test: {
4751
- name: fullName,
4752
- file: currentFile,
4753
- status: "failed",
4754
- duration: failMatch[2] ? parseFloat(failMatch[2]) : undefined
4755
- },
4756
- startedCollectingError: true
4757
- };
4758
- }
4759
- const bailFailMatch = /^\(fail\) +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
4760
- if (bailFailMatch) {
4761
- const testName = bailFailMatch[1].trim();
4762
- const fullName = currentFile ? `${currentFile} > ${testName}` : testName;
4763
- return {
4764
- test: {
4765
- name: fullName,
4766
- file: currentFile,
4767
- status: "failed",
4768
- duration: bailFailMatch[2] ? parseFloat(bailFailMatch[2]) : undefined
4769
- },
4770
- startedCollectingError: true
4771
- };
4772
- }
4773
- const skipMatch = /^⏭ +(\S.*)$/.exec(line);
4774
- if (skipMatch) {
4775
- const testName = skipMatch[1].trim();
4776
- const fullName = currentFile ? `${currentFile} > ${testName}` : testName;
4777
- return {
4778
- test: {
4779
- name: fullName,
4780
- file: currentFile,
4781
- status: "skipped"
4782
- }
4783
- };
4784
- }
4785
- return {};
4786
- }
4787
- function finalizeErrorMessage(currentTest, errorLines) {
4788
- if (currentTest && errorLines.length > 0) {
4789
- currentTest.failureMessage = errorLines.join(`
4790
- `).trim();
4791
- }
4792
- }
4793
- function shouldCollectErrorLine(line) {
4794
- if (!line.trim()) {
4795
- return false;
4796
- }
4797
- return !/^\s*\d+\s+(?:pass|fail|skip)/.exec(line);
4798
- }
4799
- function updateCounters(test, counters, parseResult) {
4800
- if (test.status === "passed") {
4801
- counters.passed++;
4802
- return false;
4803
- } else if (test.status === "failed") {
4804
- counters.failed++;
4805
- return parseResult.startedCollectingError ?? false;
4806
- } else {
4807
- counters.skipped++;
4808
- return false;
4809
- }
4810
- }
4811
- function parseSummaryLines(output) {
4812
- const counts = { passed: 0, failed: 0, skipped: 0 };
4813
- const passSummary = /\s(\d+)\s+pass\b/.exec(output);
4814
- const failSummary = /\s(\d+)\s+fail\b/.exec(output);
4815
- const skipSummary = /\s(\d+)\s+skip\b/.exec(output);
4816
- const bailSummary = /Bailed out after (\d+) failures?/.exec(output);
4817
- if (passSummary) {
4818
- counts.passed = parseInt(passSummary[1], 10);
4819
- }
4820
- if (failSummary) {
4821
- counts.failed = parseInt(failSummary[1], 10);
4822
- }
4823
- if (skipSummary) {
4824
- counts.skipped = parseInt(skipSummary[1], 10);
4825
- }
4826
- if (bailSummary) {
4827
- counts.failed = Math.max(counts.failed, parseInt(bailSummary[1], 10));
4828
- }
4829
- const ranTestsSummary = /Ran\s+(\d+)\s+tests?/.exec(output);
4830
- if (ranTestsSummary) {
4831
- const totalFromRan = parseInt(ranTestsSummary[1], 10);
4832
- const totalParsed = counts.passed + counts.failed + counts.skipped;
4833
- if (totalParsed !== totalFromRan && counts.passed === 0 && counts.failed === 0) {
4834
- counts.passed = totalFromRan;
4835
- }
4836
- }
4837
- return counts;
4838
- }
4839
- function parseBunTestOutput(stdout, stderr) {
4840
- const tests = [];
4841
- const counters = { passed: 0, failed: 0, skipped: 0 };
4842
- const output = stdout + `
4843
- ` + stderr;
4844
- const lines = output.split(`
4845
- `);
4846
- let currentTest = null;
4847
- let collectingError = false;
4848
- let errorLines = [];
4849
- let currentFile;
4850
- for (const line of lines) {
4851
- const filePath = parseFilePath(line);
4852
- if (filePath) {
4853
- currentFile = filePath;
4854
- continue;
4855
- }
4856
- const parseResult = parseTestLine(line, currentFile);
4857
- if (parseResult.test) {
4858
- if (currentTest && collectingError) {
4859
- finalizeErrorMessage(currentTest, errorLines);
4860
- errorLines = [];
4861
- }
4862
- currentTest = parseResult.test;
4863
- tests.push(currentTest);
4864
- collectingError = updateCounters(currentTest, counters, parseResult);
4865
- continue;
4866
- }
4867
- if (collectingError && currentTest && shouldCollectErrorLine(line)) {
4868
- errorLines.push(line);
4869
- }
4870
- }
4871
- if (currentTest && collectingError) {
4872
- finalizeErrorMessage(currentTest, errorLines);
4873
- }
4874
- const summaryCounts = parseSummaryLines(output);
4875
- counters.passed = Math.max(counters.passed, summaryCounts.passed);
4876
- counters.failed = Math.max(counters.failed, summaryCounts.failed);
4877
- counters.skipped = Math.max(counters.skipped, summaryCounts.skipped);
4878
- return {
4879
- tests,
4880
- totalTests: counters.passed + counters.failed + counters.skipped,
4881
- passed: counters.passed,
4882
- failed: counters.failed,
4883
- skipped: counters.skipped
4884
- };
4885
- }
4886
4632
  // src/coverage/preload-generator.ts
4887
4633
  import { mkdir, unlink, readFile, writeFile } from "node:fs/promises";
4888
- import { join, dirname as dirname2, resolve as resolve3 } from "node:path";
4634
+ import path from "node:path";
4889
4635
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4890
4636
 
4891
4637
  // node_modules/tinyglobby/dist/index.mjs
@@ -5722,7 +5468,7 @@ async function resolveEagerModulesFromGlobs(mutateGlobs, cwd = process.cwd()) {
5722
5468
  if (p.startsWith("!")) {
5723
5469
  negativePatterns.push(p.slice(1));
5724
5470
  } else {
5725
- positivePatterns.push(p.replace(/:[0-9].*$/, ""));
5471
+ positivePatterns.push(p.replace(/:\d.*$/, ""));
5726
5472
  }
5727
5473
  }
5728
5474
  if (positivePatterns.length === 0) {
@@ -5737,21 +5483,21 @@ async function resolveEagerModulesFromGlobs(mutateGlobs, cwd = process.cwd()) {
5737
5483
  const sourceFileRe = /\.(?:tsx?|[cm]?js)$/;
5738
5484
  const dtsRe = /\.d\.[cm]?ts$/;
5739
5485
  const filtered = paths.filter((p) => sourceFileRe.test(p) && !dtsRe.test(p));
5740
- const resolved = filtered.map((p) => resolve3(p));
5741
- resolved.sort();
5486
+ const resolved = filtered.map((p) => path.resolve(p));
5487
+ resolved.sort((a, b) => a.localeCompare(b));
5742
5488
  return resolved;
5743
5489
  }
5744
5490
  async function generatePreloadScript(options) {
5745
- const preloadPath = join(options.tempDir, `stryker-coverage-preload-${process.pid}.ts`);
5491
+ const preloadPath = path.join(options.tempDir, `stryker-coverage-preload-${process.pid}.ts`);
5746
5492
  await mkdir(options.tempDir, { recursive: true });
5747
- const __dirname2 = dirname2(fileURLToPath2(import.meta.url));
5493
+ const __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
5748
5494
  const isBundled = __dirname2.endsWith("dist") || __dirname2.includes("dist/");
5749
- const templatePath = isBundled ? join(__dirname2, "templates/coverage-preload.ts") : join(__dirname2, "../templates/coverage-preload.ts");
5750
- const template = await readFile(templatePath, "utf-8");
5751
- const preloadLogicPath = isBundled ? join(__dirname2, "coverage/preload-logic.js") : join(__dirname2, "preload-logic.ts");
5495
+ const templatePath = isBundled ? path.join(__dirname2, "templates/coverage-preload.ts") : path.join(__dirname2, "../templates/coverage-preload.ts");
5496
+ const template = await readFile(templatePath, "utf8");
5497
+ const preloadLogicPath = isBundled ? path.join(__dirname2, "coverage/preload-logic.js") : path.join(__dirname2, "preload-logic.ts");
5752
5498
  const eagerModules = options.eagerModules ?? [];
5753
5499
  const content = template.replace("__PRELOAD_LOGIC_PATH__", preloadLogicPath).replace("__EAGER_MODULES__", JSON.stringify(eagerModules));
5754
- await writeFile(preloadPath, content, "utf-8");
5500
+ await writeFile(preloadPath, content, "utf8");
5755
5501
  return preloadPath;
5756
5502
  }
5757
5503
  async function cleanupPreloadScript(preloadPath) {
@@ -5759,6 +5505,7 @@ async function cleanupPreloadScript(preloadPath) {
5759
5505
  await unlink(preloadPath);
5760
5506
  } catch {}
5761
5507
  }
5508
+
5762
5509
  // src/coverage/collector.ts
5763
5510
  import { readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
5764
5511
  function arrayToCoverageData(mutantIds) {
@@ -5776,26 +5523,26 @@ function mergeCoverageData(dataList) {
5776
5523
  const staticSet = new Set;
5777
5524
  for (const data of dataList) {
5778
5525
  for (const [testId, mutantIds] of Object.entries(data.perTest)) {
5779
- if (!merged.perTest[testId]) {
5780
- merged.perTest[testId] = mutantIds;
5781
- } else {
5526
+ if (testId in merged.perTest) {
5782
5527
  const existingSet = new Set(merged.perTest[testId]);
5783
5528
  for (const mutantId of mutantIds) {
5784
5529
  existingSet.add(mutantId);
5785
5530
  }
5786
- merged.perTest[testId] = Array.from(existingSet);
5531
+ merged.perTest[testId] = [...existingSet];
5532
+ } else {
5533
+ merged.perTest[testId] = mutantIds;
5787
5534
  }
5788
5535
  }
5789
5536
  for (const mutantId of data.static) {
5790
5537
  staticSet.add(mutantId);
5791
5538
  }
5792
5539
  }
5793
- merged.static = Array.from(staticSet);
5540
+ merged.static = [...staticSet];
5794
5541
  return merged;
5795
5542
  }
5796
5543
  async function collectCoverage(coverageFile, logger) {
5797
5544
  try {
5798
- const content = await readFile2(coverageFile, "utf-8");
5545
+ const content = await readFile2(coverageFile, "utf8");
5799
5546
  const trimmed = content.trim();
5800
5547
  const lines = trimmed.split(`
5801
5548
  `).filter((line) => line.length > 0);
@@ -5831,6 +5578,7 @@ async function cleanupCoverageFile(coverageFile) {
5831
5578
  await unlink2(coverageFile);
5832
5579
  } catch {}
5833
5580
  }
5581
+
5834
5582
  // src/utils/test-name.ts
5835
5583
  function normalizeTestFilePath(url) {
5836
5584
  if (!url) {
@@ -5843,7 +5591,10 @@ function normalizeTestFilePath(url) {
5843
5591
  return url;
5844
5592
  }
5845
5593
  function normalizeTestName(testName) {
5846
- return testName.replace(/[\x00-\x1F\x7F]/g, "_").trim();
5594
+ return testName.replaceAll(/\p{Cc}/gu, "_").trim();
5595
+ }
5596
+ function buildProjectFileTestName(filePrefix, fullName) {
5597
+ return normalizeTestName(`${filePrefix} > ${fullName}`);
5847
5598
  }
5848
5599
  function buildUniqueTestName(fullName, url) {
5849
5600
  const normalizedPath = normalizeTestFilePath(url);
@@ -5856,46 +5607,36 @@ function buildUniqueTestName(fullName, url) {
5856
5607
  // src/coverage/coverage-mapper.ts
5857
5608
  function mapCoverageToInspectorIds(rawCoverage, executionOrder, testHierarchy, logger) {
5858
5609
  if (!rawCoverage?.perTest || Object.keys(rawCoverage.perTest).length === 0) {
5859
- return rawCoverage;
5610
+ return { coverage: rawCoverage ?? undefined, inspectorIdToProjectFile: new Map };
5860
5611
  }
5861
5612
  const firstKey = Object.keys(rawCoverage.perTest)[0];
5862
- if (/@@test-\d+$/.exec(firstKey)) {
5613
+ if (/@@test-\d+$/.test(firstKey)) {
5863
5614
  return mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
5864
5615
  }
5865
- if (/^test-\d+$/.exec(firstKey)) {
5866
- return mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
5616
+ if (/^test-\d+$/.test(firstKey)) {
5617
+ return { coverage: mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger), inspectorIdToProjectFile: new Map };
5867
5618
  }
5868
- return rawCoverage;
5619
+ return { coverage: rawCoverage, inspectorIdToProjectFile: new Map };
5869
5620
  }
5870
- function stabilizeCoverage(coverage) {
5871
- const perTestEntries = Object.entries(coverage.perTest ?? {});
5872
- if (perTestEntries.length === 0) {
5873
- return coverage;
5874
- }
5875
- const perTestAppearances = new Map;
5621
+ function countPerTestAppearances(perTestEntries) {
5622
+ const appearances = new Map;
5876
5623
  for (const [, counts] of perTestEntries) {
5877
5624
  for (const mutantId of Object.keys(counts)) {
5878
- perTestAppearances.set(mutantId, (perTestAppearances.get(mutantId) ?? 0) + 1);
5625
+ appearances.set(mutantId, (appearances.get(mutantId) ?? 0) + 1);
5879
5626
  }
5880
5627
  }
5881
- const promoteToStatic = new Set(Object.keys(coverage.static ?? {}));
5628
+ return appearances;
5629
+ }
5630
+ function buildPromoteToStaticSet(existingStaticKeys, perTestAppearances) {
5631
+ const promoteToStatic = new Set(existingStaticKeys);
5882
5632
  for (const [mutantId, count] of perTestAppearances) {
5883
5633
  if (count > 1) {
5884
5634
  promoteToStatic.add(mutantId);
5885
5635
  }
5886
5636
  }
5887
- const existingStatic = new Set(Object.keys(coverage.static ?? {}));
5888
- const hasNewPromotions = [...promoteToStatic].some((id) => !existingStatic.has(id));
5889
- const hasPerTestContamination = perTestEntries.some(([, counts]) => Object.keys(counts).some((id) => promoteToStatic.has(id)));
5890
- if (!hasNewPromotions && !hasPerTestContamination) {
5891
- return coverage;
5892
- }
5893
- const newStatic = { ...coverage.static ?? {} };
5894
- for (const mutantId of promoteToStatic) {
5895
- if (!(mutantId in newStatic)) {
5896
- newStatic[mutantId] = 1;
5897
- }
5898
- }
5637
+ return promoteToStatic;
5638
+ }
5639
+ function buildFilteredPerTest(perTestEntries, promoteToStatic) {
5899
5640
  const newPerTest = {};
5900
5641
  for (const [testId, counts] of perTestEntries) {
5901
5642
  const filteredCounts = {};
@@ -5908,76 +5649,168 @@ function stabilizeCoverage(coverage) {
5908
5649
  newPerTest[testId] = filteredCounts;
5909
5650
  }
5910
5651
  }
5911
- return { static: newStatic, perTest: newPerTest };
5652
+ return newPerTest;
5912
5653
  }
5913
- function mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
5914
- const fileToInspectorIds = new Map;
5915
- for (const inspectorId of executionOrder) {
5916
- const testInfo = testHierarchy.get(inspectorId);
5917
- if (!testInfo) {
5918
- continue;
5919
- }
5920
- const relFile = normalizeTestFilePath(testInfo.url) ?? "";
5921
- const bucket = fileToInspectorIds.get(relFile);
5922
- if (bucket) {
5923
- bucket.push(inspectorId);
5924
- } else {
5925
- fileToInspectorIds.set(relFile, [inspectorId]);
5654
+ function stabilizeCoverage(coverage) {
5655
+ const perTestEntries = Object.entries(coverage.perTest);
5656
+ if (perTestEntries.length === 0) {
5657
+ return coverage;
5658
+ }
5659
+ const existingStaticKeys = Object.keys(coverage.static);
5660
+ const perTestAppearances = countPerTestAppearances(perTestEntries);
5661
+ const promoteToStatic = buildPromoteToStaticSet(existingStaticKeys, perTestAppearances);
5662
+ const existingStatic = new Set(existingStaticKeys);
5663
+ const hasNewPromotions = [...promoteToStatic].some((id) => !existingStatic.has(id));
5664
+ const hasPerTestContamination = perTestEntries.some(([, counts]) => Object.keys(counts).some((id) => promoteToStatic.has(id)));
5665
+ if (!hasNewPromotions && !hasPerTestContamination) {
5666
+ return coverage;
5667
+ }
5668
+ const newStatic = { ...coverage.static };
5669
+ for (const mutantId of promoteToStatic) {
5670
+ if (!(mutantId in newStatic)) {
5671
+ newStatic[mutantId] = 1;
5926
5672
  }
5927
5673
  }
5928
- const counterIds = Object.keys(rawCoverage.perTest).sort((a, b) => {
5929
- const nA = parseInt(a.split("@@test-")[1] ?? "0", 10);
5930
- const nB = parseInt(b.split("@@test-")[1] ?? "0", 10);
5931
- return nA - nB;
5674
+ const newPerTest = buildFilteredPerTest(perTestEntries, promoteToStatic);
5675
+ return { static: newStatic, perTest: newPerTest };
5676
+ }
5677
+ function pairKeysWithInspectorIds(globallyOrderedPerTestKeys, executionOrder, testHierarchy, logger) {
5678
+ const nonSkipped = executionOrder.filter((id) => {
5679
+ const status = testHierarchy.get(id)?.status;
5680
+ return status !== "skip" && status !== "todo";
5932
5681
  });
5933
- const testNames = [];
5934
- const resolvedInfos = [];
5935
- for (const key of counterIds) {
5682
+ if (globallyOrderedPerTestKeys.length < nonSkipped.length) {
5683
+ logger?.warn("Coverage/execution count mismatch: %s coverage entries vs %s non-skipped executed tests. " + "Performing partial mapping for %s tests.", globallyOrderedPerTestKeys.length, nonSkipped.length, Math.min(globallyOrderedPerTestKeys.length, nonSkipped.length));
5684
+ }
5685
+ const pairCount = Math.min(globallyOrderedPerTestKeys.length, nonSkipped.length);
5686
+ const pairs = [];
5687
+ for (let i = 0;i < pairCount; i++) {
5688
+ const key = globallyOrderedPerTestKeys[i];
5689
+ const sepIdx = key.indexOf("@@");
5690
+ const filePrefix = sepIdx === -1 ? key : key.slice(0, sepIdx);
5691
+ pairs.push({ filePrefix, inspectorId: nonSkipped[i] });
5692
+ }
5693
+ return pairs;
5694
+ }
5695
+ function resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logger) {
5696
+ return counterIds.map((key) => {
5936
5697
  const sepIdx = key.indexOf("@@");
5937
5698
  const filePrefix = key.slice(0, sepIdx);
5938
5699
  const counterStr = key.slice(sepIdx + 2 + "test-".length);
5939
- const n = parseInt(counterStr, 10);
5700
+ const n = Number.parseInt(counterStr, 10);
5940
5701
  const fileIds = fileToInspectorIds.get(filePrefix);
5941
- const inspectorId = fileIds?.[n - 1];
5942
- const testInfo = inspectorId !== undefined ? testHierarchy.get(inspectorId) : undefined;
5702
+ const clampedIdx = fileIds ? Math.min(n - 1, fileIds.length - 1) : undefined;
5703
+ const inspectorId = clampedIdx === undefined ? undefined : fileIds?.[clampedIdx];
5704
+ const testInfo = inspectorId === undefined ? undefined : testHierarchy.get(inspectorId);
5943
5705
  if (testInfo) {
5944
- testNames.push(buildUniqueTestName(testInfo.fullName, testInfo.url));
5945
- resolvedInfos.push(testInfo);
5946
- } else {
5947
- logger?.warn('Coverage key %s: no inspector test found for file "%s" at position %s ' + "(file has %s tests in execution order). Skipping this test in coverage mapping.", key, filePrefix, n, fileToInspectorIds.get(filePrefix)?.length ?? 0);
5948
- testNames.push(`unknown-${key}`);
5949
- resolvedInfos.push(null);
5706
+ const testName = buildProjectFileTestName(filePrefix, testInfo.fullName);
5707
+ return { name: testName, testInfo, inspectorId };
5950
5708
  }
5709
+ logger?.warn('Coverage key %s: no inspector test found for file "%s" at position %s ' + "(file has %s tests in execution order). Skipping this test in coverage mapping.", key, filePrefix, n, fileToInspectorIds.get(filePrefix)?.length ?? 0);
5710
+ return { name: `unknown-${key}`, testInfo: null, inspectorId: undefined };
5711
+ });
5712
+ }
5713
+ function buildNameInspectorIds(resolved) {
5714
+ const nameInspectorIds = new Map;
5715
+ for (const { name, inspectorId, testInfo } of resolved) {
5716
+ if (!testInfo) {
5717
+ continue;
5718
+ }
5719
+ let ids = nameInspectorIds.get(name);
5720
+ if (!ids) {
5721
+ ids = new Set;
5722
+ nameInspectorIds.set(name, ids);
5723
+ }
5724
+ ids.add(inspectorId);
5951
5725
  }
5952
- const nameCounts = new Map;
5953
- for (const name of testNames) {
5954
- nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
5955
- }
5726
+ return nameInspectorIds;
5727
+ }
5728
+ function resolveEachTestName(baseName, inspectorId, nameInspectorIds, nameIndexes) {
5729
+ const distinctIds = nameInspectorIds.get(baseName);
5730
+ if ((distinctIds?.size ?? 1) <= 1) {
5731
+ return baseName;
5732
+ }
5733
+ const key_ = `${inspectorId}/${baseName}`;
5734
+ const existingIndex = nameIndexes.get(key_);
5735
+ if (existingIndex === undefined) {
5736
+ const nextIndex = nameIndexes.get(baseName) ?? 0;
5737
+ nameIndexes.set(key_, nextIndex);
5738
+ nameIndexes.set(baseName, nextIndex + 1);
5739
+ return `${baseName} [${nextIndex}]`;
5740
+ }
5741
+ return `${baseName} [${existingIndex}]`;
5742
+ }
5743
+ function buildRemappedPerTest(counterIds, resolved, rawPerTest) {
5744
+ const nameInspectorIds = buildNameInspectorIds(resolved);
5956
5745
  const remappedPerTest = {};
5957
5746
  const nameIndexes = new Map;
5958
- for (let i = 0;i < counterIds.length; i++) {
5959
- const key = counterIds[i];
5960
- const testInfo = resolvedInfos[i];
5747
+ for (const [i, key] of counterIds.entries()) {
5748
+ const { name: baseName, testInfo, inspectorId } = resolved[i];
5961
5749
  if (!testInfo) {
5962
5750
  continue;
5963
5751
  }
5964
- const baseName = testNames[i];
5965
- const count = nameCounts.get(baseName) ?? 1;
5966
- let finalName = baseName;
5967
- if (count > 1) {
5968
- const index = nameIndexes.get(baseName) ?? 0;
5969
- finalName = `${baseName} [${index}]`;
5970
- nameIndexes.set(baseName, index + 1);
5752
+ const finalName = resolveEachTestName(baseName, inspectorId, nameInspectorIds, nameIndexes);
5753
+ const incoming = rawPerTest[key];
5754
+ const existing = remappedPerTest[finalName];
5755
+ if (existing) {
5756
+ for (const [mutantId, count_] of Object.entries(incoming)) {
5757
+ existing[mutantId] = (existing[mutantId] ?? 0) + count_;
5758
+ }
5759
+ } else {
5760
+ remappedPerTest[finalName] = { ...incoming };
5971
5761
  }
5972
- remappedPerTest[finalName] = rawCoverage.perTest[key];
5973
5762
  }
5974
- return stabilizeCoverage({
5763
+ return remappedPerTest;
5764
+ }
5765
+ function warnInteriorGapIfPresent(pairs, testHierarchy, logger) {
5766
+ const warnedFiles = new Set;
5767
+ for (const { filePrefix, inspectorId } of pairs) {
5768
+ const testUrl = testHierarchy.get(inspectorId)?.url;
5769
+ if (!testUrl) {
5770
+ continue;
5771
+ }
5772
+ const inspectorFile = normalizeTestFilePath(testUrl);
5773
+ if (!inspectorFile || testUrl.includes("node_modules")) {
5774
+ continue;
5775
+ }
5776
+ if (filePrefix !== inspectorFile && !warnedFiles.has(filePrefix)) {
5777
+ warnedFiles.add(filePrefix);
5778
+ logger?.warn('Interior coverage gap detected for "%s": coverage key paired with inspector test from "%s". ' + "Some tests may have been aborted mid-run (e.g. beforeAll failure). Coverage mapping may be inaccurate.", filePrefix, inspectorFile);
5779
+ }
5780
+ }
5781
+ }
5782
+ function mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
5783
+ const globallyOrderedKeys = Object.keys(rawCoverage.perTest);
5784
+ const pairs = pairKeysWithInspectorIds(globallyOrderedKeys, executionOrder, testHierarchy, logger);
5785
+ const fileToInspectorIds = new Map;
5786
+ const inspectorIdToProjectFile = new Map;
5787
+ for (const { filePrefix, inspectorId } of pairs) {
5788
+ const bucket = fileToInspectorIds.get(filePrefix);
5789
+ if (bucket) {
5790
+ bucket.push(inspectorId);
5791
+ } else {
5792
+ fileToInspectorIds.set(filePrefix, [inspectorId]);
5793
+ }
5794
+ inspectorIdToProjectFile.set(inspectorId, filePrefix);
5795
+ }
5796
+ if (globallyOrderedKeys.length === pairs.length) {
5797
+ warnInteriorGapIfPresent(pairs, testHierarchy, logger);
5798
+ }
5799
+ const counterIds = globallyOrderedKeys.toSorted((a, b) => {
5800
+ const nA = Number.parseInt(a.split("@@test-")[1] ?? "0", 10);
5801
+ const nB = Number.parseInt(b.split("@@test-")[1] ?? "0", 10);
5802
+ return nA - nB;
5803
+ });
5804
+ const resolved = resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logger);
5805
+ const remappedPerTest = buildRemappedPerTest(counterIds, resolved, rawCoverage.perTest);
5806
+ const coverage = stabilizeCoverage({
5975
5807
  static: rawCoverage.static,
5976
5808
  perTest: remappedPerTest
5977
5809
  });
5810
+ return { coverage, inspectorIdToProjectFile };
5978
5811
  }
5979
5812
  function mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
5980
- const counterIds = Object.keys(rawCoverage.perTest).sort((a, b) => parseInt(a.split("-")[1], 10) - parseInt(b.split("-")[1], 10));
5813
+ const counterIds = Object.keys(rawCoverage.perTest).toSorted((a, b) => Number.parseInt(a.split("-")[1] ?? "0", 10) - Number.parseInt(b.split("-")[1] ?? "0", 10));
5981
5814
  if (counterIds.length !== executionOrder.length) {
5982
5815
  logger?.warn("Coverage/execution count mismatch: %s coverage entries vs %s executed tests. " + "Performing partial mapping for %s tests.", counterIds.length, executionOrder.length, Math.min(counterIds.length, executionOrder.length));
5983
5816
  }
@@ -6021,10 +5854,6 @@ function mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger
6021
5854
  perTest: remappedPerTest
6022
5855
  });
6023
5856
  }
6024
- // src/bun-test-runner.ts
6025
- import { tmpdir } from "node:os";
6026
- import { join as join3 } from "node:path";
6027
- import * as fsPromises2 from "node:fs/promises";
6028
5857
 
6029
5858
  // node_modules/ws/wrapper.mjs
6030
5859
  var import_stream = __toESM(require_stream(), 1);
@@ -6086,7 +5915,7 @@ class InspectorClient {
6086
5915
  if (this.ws) {
6087
5916
  throw new Error("Already connected");
6088
5917
  }
6089
- return new Promise((resolve4, reject) => {
5918
+ return new Promise((resolve3, reject) => {
6090
5919
  const timeoutTimer = setTimeout(() => {
6091
5920
  if (this.ws) {
6092
5921
  this.ws.close();
@@ -6098,7 +5927,7 @@ class InspectorClient {
6098
5927
  this.ws = ws;
6099
5928
  ws.addEventListener("open", () => {
6100
5929
  clearTimeout(timeoutTimer);
6101
- resolve4();
5930
+ resolve3();
6102
5931
  });
6103
5932
  ws.addEventListener("error", () => {
6104
5933
  clearTimeout(timeoutTimer);
@@ -6120,12 +5949,12 @@ class InspectorClient {
6120
5949
  }
6121
5950
  const id = ++this.messageId;
6122
5951
  const message = { id, method, params };
6123
- return new Promise((resolve4, reject) => {
5952
+ return new Promise((resolve3, reject) => {
6124
5953
  const timer = setTimeout(() => {
6125
5954
  this.pendingRequests.delete(id);
6126
5955
  reject(new InspectorTimeoutError(`Request timeout after ${this.state.requestTimeout}ms: ${method}`));
6127
5956
  }, this.state.requestTimeout);
6128
- this.pendingRequests.set(id, { resolve: resolve4, reject, timer });
5957
+ this.pendingRequests.set(id, { resolve: resolve3, reject, timer });
6129
5958
  try {
6130
5959
  this.ws.send(JSON.stringify(message));
6131
5960
  } catch (error) {
@@ -6141,7 +5970,7 @@ class InspectorClient {
6141
5970
  }
6142
5971
  this.isClosing = true;
6143
5972
  const error = new InspectorConnectionError("Connection closed");
6144
- for (const pending of Array.from(this.pendingRequests.values())) {
5973
+ for (const pending of this.pendingRequests.values()) {
6145
5974
  clearTimeout(pending.timer);
6146
5975
  pending.reject(error);
6147
5976
  }
@@ -6152,7 +5981,7 @@ class InspectorClient {
6152
5981
  this.ws = null;
6153
5982
  }
6154
5983
  getTests() {
6155
- return Array.from(this.testHierarchy.values());
5984
+ return [...this.testHierarchy.values()];
6156
5985
  }
6157
5986
  getExecutionOrder() {
6158
5987
  return [...this.executionOrder];
@@ -6240,7 +6069,7 @@ class InspectorClient {
6240
6069
  let currentId = parentId;
6241
6070
  while (currentId !== undefined) {
6242
6071
  if (visited.has(currentId)) {
6243
- this.handleError(new Error(`Circular reference detected in test hierarchy: ${Array.from(visited).join(" -> ")} -> ${currentId}`));
6072
+ this.handleError(new Error(`Circular reference detected in test hierarchy: ${[...visited].join(" -> ")} -> ${currentId}`));
6244
6073
  break;
6245
6074
  }
6246
6075
  visited.add(currentId);
@@ -6258,7 +6087,7 @@ class InspectorClient {
6258
6087
  return;
6259
6088
  }
6260
6089
  const error = new InspectorConnectionError("Connection closed unexpectedly");
6261
- for (const pending of Array.from(this.pendingRequests.values())) {
6090
+ for (const pending of this.pendingRequests.values()) {
6262
6091
  clearTimeout(pending.timer);
6263
6092
  pending.reject(error);
6264
6093
  }
@@ -6271,10 +6100,281 @@ class InspectorClient {
6271
6100
  }
6272
6101
  }
6273
6102
  }
6103
+ // src/parsers/console-parser.ts
6104
+ function parseFilePath(line) {
6105
+ const fileMatch = /^([\w./-]+\.(?:test|spec)\.(?:ts|tsx|js|jsx|mts|mjs)):$/.exec(line);
6106
+ return fileMatch ? fileMatch[1] : null;
6107
+ }
6108
+ function buildTestName(testName, currentFile) {
6109
+ return currentFile ? `${currentFile} > ${testName}` : testName;
6110
+ }
6111
+ function parseTestLine(line, currentFile) {
6112
+ const passMatch = /^✓ +(\S.*?) \[([0-9.]+)ms\]$/.exec(line);
6113
+ if (passMatch) {
6114
+ return {
6115
+ test: {
6116
+ name: buildTestName(passMatch[1].trim(), currentFile),
6117
+ file: currentFile,
6118
+ status: "passed",
6119
+ duration: Number.parseFloat(passMatch[2])
6120
+ }
6121
+ };
6122
+ }
6123
+ const failMatch = /^✗ +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
6124
+ if (failMatch) {
6125
+ return {
6126
+ test: {
6127
+ name: buildTestName(failMatch[1].trim(), currentFile),
6128
+ file: currentFile,
6129
+ status: "failed",
6130
+ duration: failMatch[2] ? Number.parseFloat(failMatch[2]) : undefined
6131
+ },
6132
+ startedCollectingError: true
6133
+ };
6134
+ }
6135
+ const bailFailMatch = /^\(fail\) +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
6136
+ if (bailFailMatch) {
6137
+ return {
6138
+ test: {
6139
+ name: buildTestName(bailFailMatch[1].trim(), currentFile),
6140
+ file: currentFile,
6141
+ status: "failed",
6142
+ duration: bailFailMatch[2] ? Number.parseFloat(bailFailMatch[2]) : undefined
6143
+ },
6144
+ startedCollectingError: true
6145
+ };
6146
+ }
6147
+ const skipMatch = /^⏭ +(\S.*)$/.exec(line);
6148
+ if (skipMatch) {
6149
+ return {
6150
+ test: {
6151
+ name: buildTestName(skipMatch[1].trim(), currentFile),
6152
+ file: currentFile,
6153
+ status: "skipped"
6154
+ }
6155
+ };
6156
+ }
6157
+ return {};
6158
+ }
6159
+ function finalizeErrorMessage(currentTest, errorLines) {
6160
+ if (currentTest && errorLines.length > 0) {
6161
+ currentTest.failureMessage = errorLines.join(`
6162
+ `).trim();
6163
+ }
6164
+ }
6165
+ function shouldCollectErrorLine(line) {
6166
+ if (!line.trim()) {
6167
+ return false;
6168
+ }
6169
+ return !/^\s*\d+\s+(?:pass|fail|skip)/.test(line);
6170
+ }
6171
+ function updateCounters(test, counters, parseResult) {
6172
+ if (test.status === "passed") {
6173
+ counters.passed++;
6174
+ return false;
6175
+ } else if (test.status === "failed") {
6176
+ counters.failed++;
6177
+ return parseResult.startedCollectingError ?? false;
6178
+ } else {
6179
+ counters.skipped++;
6180
+ return false;
6181
+ }
6182
+ }
6183
+ function parseSummaryLines(output) {
6184
+ const counts = { passed: 0, failed: 0, skipped: 0 };
6185
+ const passSummary = /\s(\d+)\s+pass\b/.exec(output);
6186
+ const failSummary = /\s(\d+)\s+fail\b/.exec(output);
6187
+ const skipSummary = /\s(\d+)\s+skip\b/.exec(output);
6188
+ const bailSummary = /Bailed out after (\d+) failures?/.exec(output);
6189
+ if (passSummary) {
6190
+ counts.passed = Number.parseInt(passSummary[1], 10);
6191
+ }
6192
+ if (failSummary) {
6193
+ counts.failed = Number.parseInt(failSummary[1], 10);
6194
+ }
6195
+ if (skipSummary) {
6196
+ counts.skipped = Number.parseInt(skipSummary[1], 10);
6197
+ }
6198
+ if (bailSummary) {
6199
+ counts.failed = Math.max(counts.failed, Number.parseInt(bailSummary[1], 10));
6200
+ }
6201
+ const ranTestsSummary = /Ran\s+(\d+)\s+tests?/.exec(output);
6202
+ if (ranTestsSummary) {
6203
+ const totalFromRan = Number.parseInt(ranTestsSummary[1], 10);
6204
+ const totalParsed = counts.passed + counts.failed + counts.skipped;
6205
+ if (totalParsed !== totalFromRan && counts.passed === 0 && counts.failed === 0) {
6206
+ counts.passed = totalFromRan;
6207
+ }
6208
+ }
6209
+ return counts;
6210
+ }
6211
+ function parseBunTestOutput(stdout, stderr) {
6212
+ const tests = [];
6213
+ const counters = { passed: 0, failed: 0, skipped: 0 };
6214
+ const output = `${stdout}
6215
+ ${stderr}`;
6216
+ const lines = output.split(`
6217
+ `);
6218
+ let currentTest = null;
6219
+ let collectingError = false;
6220
+ let errorLines = [];
6221
+ let currentFile;
6222
+ for (const line of lines) {
6223
+ const filePath = parseFilePath(line);
6224
+ if (filePath) {
6225
+ currentFile = filePath;
6226
+ continue;
6227
+ }
6228
+ const parseResult = parseTestLine(line, currentFile);
6229
+ if (parseResult.test) {
6230
+ if (currentTest && collectingError) {
6231
+ finalizeErrorMessage(currentTest, errorLines);
6232
+ errorLines = [];
6233
+ }
6234
+ currentTest = parseResult.test;
6235
+ tests.push(currentTest);
6236
+ collectingError = updateCounters(currentTest, counters, parseResult);
6237
+ continue;
6238
+ }
6239
+ if (collectingError && currentTest && shouldCollectErrorLine(line)) {
6240
+ errorLines.push(line);
6241
+ }
6242
+ }
6243
+ if (currentTest && collectingError) {
6244
+ finalizeErrorMessage(currentTest, errorLines);
6245
+ }
6246
+ const summaryCounts = parseSummaryLines(output);
6247
+ counters.passed = Math.max(counters.passed, summaryCounts.passed);
6248
+ counters.failed = Math.max(counters.failed, summaryCounts.failed);
6249
+ counters.skipped = Math.max(counters.skipped, summaryCounts.skipped);
6250
+ return {
6251
+ tests,
6252
+ totalTests: counters.passed + counters.failed + counters.skipped,
6253
+ passed: counters.passed,
6254
+ failed: counters.failed,
6255
+ skipped: counters.skipped
6256
+ };
6257
+ }
6258
+
6259
+ // src/process-runner.ts
6260
+ import { spawn } from "node:child_process";
6261
+ async function runBunTests(options) {
6262
+ const args = ["test"];
6263
+ if (options.inspectWaitPort) {
6264
+ args.push(`--inspect=${options.inspectWaitPort}`);
6265
+ }
6266
+ if (options.bunfigPath) {
6267
+ args.push(`--config=${options.bunfigPath}`);
6268
+ }
6269
+ if (options.preloadScript) {
6270
+ args.push("--preload", options.preloadScript);
6271
+ }
6272
+ if (options.testNamePattern) {
6273
+ args.push("--test-name-pattern", options.testNamePattern);
6274
+ }
6275
+ if (options.bail) {
6276
+ args.push("--bail");
6277
+ }
6278
+ if (options.sequentialMode) {
6279
+ args.push("--concurrency=1");
6280
+ }
6281
+ if (options.bunArgs && options.bunArgs.length > 0) {
6282
+ args.push(...options.bunArgs);
6283
+ }
6284
+ if (options.testFiles && options.testFiles.length > 0) {
6285
+ args.push(...options.testFiles);
6286
+ }
6287
+ const env = {
6288
+ ...process.env,
6289
+ ...options.env
6290
+ };
6291
+ if (options.activeMutant) {
6292
+ env.__STRYKER_ACTIVE_MUTANT__ = options.activeMutant;
6293
+ }
6294
+ if (options.coverageFile) {
6295
+ env.__STRYKER_COVERAGE_FILE__ = options.coverageFile;
6296
+ }
6297
+ if (options.syncPort) {
6298
+ env.__STRYKER_SYNC_PORT__ = String(options.syncPort);
6299
+ }
6300
+ return new Promise((resolve3) => {
6301
+ const stdoutChunks = [];
6302
+ const stderrChunks = [];
6303
+ let timedOut = false;
6304
+ let processKilled = false;
6305
+ if (options.signal?.aborted) {
6306
+ resolve3({ stdout: "", stderr: "", exitCode: null, timedOut: true });
6307
+ return;
6308
+ }
6309
+ const spawnOpts = {
6310
+ env,
6311
+ stdio: ["ignore", "pipe", "pipe"],
6312
+ cwd: process.cwd()
6313
+ };
6314
+ const childProcess = spawn(options.bunPath, args, spawnOpts);
6315
+ const timeoutHandle = setTimeout(() => {
6316
+ timedOut = true;
6317
+ processKilled = true;
6318
+ childProcess.kill("SIGKILL");
6319
+ }, options.timeout);
6320
+ if (options.signal) {
6321
+ const onAbort = () => {
6322
+ clearTimeout(timeoutHandle);
6323
+ processKilled = true;
6324
+ timedOut = true;
6325
+ childProcess.kill("SIGTERM");
6326
+ setTimeout(() => {
6327
+ childProcess.kill("SIGKILL");
6328
+ }, 500);
6329
+ };
6330
+ options.signal.addEventListener("abort", onAbort, { once: true });
6331
+ }
6332
+ if (childProcess.stdout) {
6333
+ childProcess.stdout.on("data", (data) => {
6334
+ stdoutChunks.push(data);
6335
+ });
6336
+ }
6337
+ let inspectorUrlExtracted = false;
6338
+ if (childProcess.stderr) {
6339
+ childProcess.stderr.on("data", (data) => {
6340
+ stderrChunks.push(data);
6341
+ if (options.inspectWaitPort && !inspectorUrlExtracted && options.onInspectorReady) {
6342
+ const text = Buffer.concat(stderrChunks).toString();
6343
+ const match = /Listening:[\t\v\f\r \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*\n\s*(ws:\/\/\S+)/.exec(text);
6344
+ if (match) {
6345
+ inspectorUrlExtracted = true;
6346
+ options.onInspectorReady(match[1]);
6347
+ }
6348
+ }
6349
+ });
6350
+ }
6351
+ childProcess.on("close", (code) => {
6352
+ clearTimeout(timeoutHandle);
6353
+ resolve3({
6354
+ stdout: Buffer.concat(stdoutChunks).toString(),
6355
+ stderr: Buffer.concat(stderrChunks).toString(),
6356
+ exitCode: processKilled ? null : code,
6357
+ timedOut
6358
+ });
6359
+ });
6360
+ childProcess.on("error", (error) => {
6361
+ clearTimeout(timeoutHandle);
6362
+ const stderrOutput = Buffer.concat(stderrChunks).toString();
6363
+ resolve3({
6364
+ stdout: Buffer.concat(stdoutChunks).toString(),
6365
+ stderr: `${stderrOutput}
6366
+ Process error: ${error.message}`,
6367
+ exitCode: null,
6368
+ timedOut
6369
+ });
6370
+ });
6371
+ });
6372
+ }
6373
+
6274
6374
  // src/utils/port.ts
6275
- import { createServer } from "net";
6375
+ import { createServer } from "node:net";
6276
6376
  async function getAvailablePort() {
6277
- return new Promise((resolve4, reject) => {
6377
+ return new Promise((resolve3, reject) => {
6278
6378
  const server = createServer();
6279
6379
  server.on("error", (err) => {
6280
6380
  reject(new Error(`Failed to get available port: ${err.message}`));
@@ -6292,7 +6392,7 @@ async function getAvailablePort() {
6292
6392
  reject(new Error(`Failed to close server: ${err.message}`));
6293
6393
  return;
6294
6394
  }
6295
- resolve4(port);
6395
+ resolve3(port);
6296
6396
  });
6297
6397
  });
6298
6398
  });
@@ -6303,6 +6403,7 @@ class SyncServer {
6303
6403
  httpServer = null;
6304
6404
  wss = null;
6305
6405
  clients = new Set;
6406
+ readyLatched = false;
6306
6407
  port;
6307
6408
  createHttpServer;
6308
6409
  WebSocketServerClass;
@@ -6314,15 +6415,15 @@ class SyncServer {
6314
6415
  this.webSocketOpenState = options.webSocketOpenState ?? import_websocket.default.OPEN;
6315
6416
  }
6316
6417
  async start() {
6317
- return new Promise((resolve4, reject) => {
6418
+ return new Promise((resolve3, reject) => {
6318
6419
  try {
6319
6420
  this.httpServer = this.createHttpServer((req, res) => {
6320
- if (req.url !== "/sync") {
6321
- res.writeHead(404);
6322
- res.end("Not found");
6323
- } else {
6421
+ if (req.url === "/sync") {
6324
6422
  res.writeHead(400);
6325
6423
  res.end("WebSocket upgrade failed");
6424
+ } else {
6425
+ res.writeHead(404);
6426
+ res.end("Not found");
6326
6427
  }
6327
6428
  });
6328
6429
  this.wss = new this.WebSocketServerClass({
@@ -6331,6 +6432,11 @@ class SyncServer {
6331
6432
  });
6332
6433
  this.wss.on("connection", (ws) => {
6333
6434
  this.clients.add(ws);
6435
+ if (this.readyLatched && ws.readyState === this.webSocketOpenState) {
6436
+ try {
6437
+ ws.send("ready");
6438
+ } catch {}
6439
+ }
6334
6440
  ws.on("close", () => {
6335
6441
  this.clients.delete(ws);
6336
6442
  });
@@ -6338,7 +6444,7 @@ class SyncServer {
6338
6444
  });
6339
6445
  this.httpServer.on("error", reject);
6340
6446
  this.httpServer.listen(this.port, () => {
6341
- resolve4();
6447
+ resolve3();
6342
6448
  });
6343
6449
  } catch (error) {
6344
6450
  reject(error instanceof Error ? error : new Error(String(error)));
@@ -6346,6 +6452,7 @@ class SyncServer {
6346
6452
  });
6347
6453
  }
6348
6454
  signalReady() {
6455
+ this.readyLatched = true;
6349
6456
  for (const client of this.clients) {
6350
6457
  try {
6351
6458
  if (client.readyState === this.webSocketOpenState) {
@@ -6355,6 +6462,7 @@ class SyncServer {
6355
6462
  }
6356
6463
  }
6357
6464
  async close() {
6465
+ this.readyLatched = false;
6358
6466
  for (const client of this.clients) {
6359
6467
  try {
6360
6468
  client.close();
@@ -6362,13 +6470,15 @@ class SyncServer {
6362
6470
  }
6363
6471
  this.clients.clear();
6364
6472
  if (this.wss) {
6365
- this.wss.close();
6473
+ await new Promise((resolve3) => {
6474
+ this.wss.close(() => resolve3());
6475
+ });
6366
6476
  this.wss = null;
6367
6477
  }
6368
6478
  if (this.httpServer) {
6369
- await new Promise((resolve4) => {
6479
+ await new Promise((resolve3) => {
6370
6480
  this.httpServer.close(() => {
6371
- resolve4();
6481
+ resolve3();
6372
6482
  });
6373
6483
  });
6374
6484
  this.httpServer = null;
@@ -6380,7 +6490,7 @@ class SyncServer {
6380
6490
  }
6381
6491
  // src/utils/bunfig-sanitizer.ts
6382
6492
  import { readFile as readFile3, unlink as unlink3, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
6383
- import path from "node:path";
6493
+ import path2 from "node:path";
6384
6494
 
6385
6495
  // node_modules/smol-toml/dist/error.js
6386
6496
  /*!
@@ -7490,7 +7600,7 @@ function absolutizePath(value, projectCwd) {
7490
7600
  if (typeof value !== "string") {
7491
7601
  return value;
7492
7602
  }
7493
- return path.isAbsolute(value) ? value : path.resolve(projectCwd, value);
7603
+ return path2.isAbsolute(value) ? value : path2.resolve(projectCwd, value);
7494
7604
  }
7495
7605
  function absolutizePathValue(value, projectCwd) {
7496
7606
  if (Array.isArray(value)) {
@@ -7501,7 +7611,7 @@ function absolutizePathValue(value, projectCwd) {
7501
7611
  async function generateSanitizedBunfig(projectCwd, tmpDir) {
7502
7612
  await mkdir2(tmpDir, { recursive: true });
7503
7613
  let rawConfig = {};
7504
- const bunfigPath = path.join(projectCwd, "bunfig.toml");
7614
+ const bunfigPath = path2.join(projectCwd, "bunfig.toml");
7505
7615
  try {
7506
7616
  const content = await readFile3(bunfigPath, "utf8");
7507
7617
  rawConfig = parse(content);
@@ -7525,7 +7635,7 @@ async function generateSanitizedBunfig(projectCwd, tmpDir) {
7525
7635
  sanitizedTest.onlyFailures = false;
7526
7636
  sanitized.test = sanitizedTest;
7527
7637
  const serialized = stringify(sanitized);
7528
- const outPath = path.join(tmpDir, `stryker-bun-runner-bunfig-${process.pid}-${Date.now()}.toml`);
7638
+ const outPath = path2.join(tmpDir, `stryker-bun-runner-bunfig-${process.pid}-${Date.now()}.toml`);
7529
7639
  await writeFile2(outPath, serialized, "utf8");
7530
7640
  return outPath;
7531
7641
  }
@@ -7545,20 +7655,14 @@ function buildTestNamePattern(testFilter) {
7545
7655
  }
7546
7656
  const fileExtRe = /\.(?:test|spec)\.(?:[jt]sx?|m[jt]s)$/;
7547
7657
  const dedupSuffixRe = / \[\d+\]$/;
7548
- const hierarchySepRe = / > /g;
7549
- const metaRe = /[.*+?^${}()|[\]\\\/]/g;
7658
+ const metaRe = /[.*+?^${}()|[\]\\/]/g;
7550
7659
  const alternatives = new Set;
7551
7660
  for (const id of testFilter) {
7552
7661
  const firstSepIdx = id.indexOf(" > ");
7553
- let name;
7554
- if (firstSepIdx !== -1 && fileExtRe.test(id.slice(0, firstSepIdx))) {
7555
- name = id.slice(firstSepIdx + 3);
7556
- } else {
7557
- name = id;
7558
- }
7662
+ let name = firstSepIdx !== -1 && fileExtRe.test(id.slice(0, firstSepIdx)) ? id.slice(firstSepIdx + 3) : id;
7559
7663
  name = name.replace(dedupSuffixRe, "");
7560
- name = name.replace(hierarchySepRe, " ");
7561
- name = name.replace(metaRe, "\\$&");
7664
+ name = name.replaceAll(" > ", " ");
7665
+ name = name.replaceAll(metaRe, String.raw`\$&`);
7562
7666
  if (name.length > 0) {
7563
7667
  alternatives.add(name);
7564
7668
  }
@@ -7566,27 +7670,85 @@ function buildTestNamePattern(testFilter) {
7566
7670
  if (alternatives.size === 0) {
7567
7671
  return;
7568
7672
  }
7569
- return `^(?:${Array.from(alternatives).join("|")})$`;
7673
+ return `^(?:${[...alternatives].join("|")})$`;
7570
7674
  }
7571
7675
  // src/utils/test-file-discovery.ts
7572
- import { join as join2, relative as relative2 } from "node:path";
7573
7676
  import * as fsPromises from "node:fs/promises";
7677
+ import path3 from "node:path";
7678
+ var testFileRe = /\.(?:test|spec)\.(?:[jt]sx?|m[jt]s)$/;
7679
+ var excludedDirs = new Set(["node_modules", ".stryker-tmp", "dist", "build", ".git"]);
7680
+ function hasExcludedAncestor(absolutePath) {
7681
+ return absolutePath.split("/").some((seg) => excludedDirs.has(seg));
7682
+ }
7683
+ async function tryRealpath(p) {
7684
+ try {
7685
+ return await fsPromises.realpath(p);
7686
+ } catch {
7687
+ return;
7688
+ }
7689
+ }
7690
+ async function tryStat(p) {
7691
+ try {
7692
+ return await fsPromises.stat(p);
7693
+ } catch {
7694
+ return;
7695
+ }
7696
+ }
7697
+ async function handleSymlinkDir(fullPath, entryName, ctx) {
7698
+ if (excludedDirs.has(entryName)) {
7699
+ return;
7700
+ }
7701
+ const resolvedTarget = await tryRealpath(fullPath);
7702
+ if (!resolvedTarget) {
7703
+ return;
7704
+ }
7705
+ if (hasExcludedAncestor(resolvedTarget)) {
7706
+ ctx.logger?.debug("discoverTestFiles: symlink %s resolves to excluded path %s; skipping", fullPath, resolvedTarget);
7707
+ } else {
7708
+ await ctx.walk(fullPath);
7709
+ }
7710
+ }
7711
+ async function handleSymlinkFile(fullPath, entryName, ctx) {
7712
+ if (!testFileRe.test(entryName)) {
7713
+ return;
7714
+ }
7715
+ const resolvedTarget = await tryRealpath(fullPath);
7716
+ if (resolvedTarget && !hasExcludedAncestor(resolvedTarget)) {
7717
+ ctx.results.push(path3.relative(ctx.cwd, fullPath));
7718
+ }
7719
+ }
7720
+ async function handleSymlink(fullPath, entryName, ctx) {
7721
+ const stat3 = await tryStat(fullPath);
7722
+ if (!stat3) {
7723
+ ctx.logger?.debug("discoverTestFiles: broken symlink at %s; skipping", fullPath);
7724
+ return;
7725
+ }
7726
+ if (stat3.isDirectory()) {
7727
+ await handleSymlinkDir(fullPath, entryName, ctx);
7728
+ } else if (stat3.isFile()) {
7729
+ await handleSymlinkFile(fullPath, entryName, ctx);
7730
+ }
7731
+ }
7732
+ async function processEntry(entry, dir, ctx) {
7733
+ const fullPath = path3.join(dir, entry.name);
7734
+ if (entry.isDirectory()) {
7735
+ if (!excludedDirs.has(entry.name)) {
7736
+ await ctx.walk(fullPath);
7737
+ }
7738
+ } else if (entry.isSymbolicLink()) {
7739
+ await handleSymlink(fullPath, entry.name, ctx);
7740
+ } else if (entry.isFile() && testFileRe.test(entry.name)) {
7741
+ ctx.results.push(path3.relative(ctx.cwd, fullPath));
7742
+ }
7743
+ }
7574
7744
  async function discoverTestFiles(cwd = process.cwd(), logger) {
7575
- const testFileRe = /\.(?:test|spec)\.(?:[jt]sx?|m[jt]s)$/;
7576
- const excludedDirs = new Set(["node_modules", ".stryker-tmp", "dist", "build", ".git"]);
7577
- const hasExcludedAncestor = (absolutePath) => {
7578
- return absolutePath.split("/").some((seg) => excludedDirs.has(seg));
7579
- };
7580
7745
  const results = [];
7581
7746
  const visitedRealPaths = new Set;
7582
- const walk = async (dir) => {
7583
- let realDir;
7584
- try {
7585
- realDir = await fsPromises.realpath(dir);
7586
- } catch {
7587
- if (logger) {
7588
- logger.debug("discoverTestFiles: could not resolve real path of %s; skipping", dir);
7589
- }
7747
+ const ctx = { cwd, results, visitedRealPaths, logger, walk };
7748
+ async function walk(dir) {
7749
+ const realDir = await tryRealpath(dir);
7750
+ if (!realDir) {
7751
+ logger?.debug("discoverTestFiles: could not resolve real path of %s; skipping", dir);
7590
7752
  return;
7591
7753
  }
7592
7754
  if (visitedRealPaths.has(realDir)) {
@@ -7599,69 +7761,27 @@ async function discoverTestFiles(cwd = process.cwd(), logger) {
7599
7761
  } catch {
7600
7762
  return;
7601
7763
  }
7602
- entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
7764
+ entries.sort((a, b) => a.name.localeCompare(b.name));
7603
7765
  for (const entry of entries) {
7604
- const fullPath = join2(dir, entry.name);
7605
- if (entry.isDirectory()) {
7606
- if (!excludedDirs.has(entry.name)) {
7607
- await walk(fullPath);
7608
- }
7609
- } else if (entry.isSymbolicLink()) {
7610
- let stat3;
7611
- try {
7612
- stat3 = await fsPromises.stat(fullPath);
7613
- } catch {
7614
- if (logger) {
7615
- logger.debug("discoverTestFiles: broken symlink at %s; skipping", fullPath);
7616
- }
7617
- continue;
7618
- }
7619
- if (stat3.isDirectory()) {
7620
- if (!excludedDirs.has(entry.name)) {
7621
- let resolvedTarget;
7622
- try {
7623
- resolvedTarget = await fsPromises.realpath(fullPath);
7624
- } catch {
7625
- continue;
7626
- }
7627
- if (hasExcludedAncestor(resolvedTarget)) {
7628
- if (logger) {
7629
- logger.debug("discoverTestFiles: symlink %s resolves to excluded path %s; skipping", fullPath, resolvedTarget);
7630
- }
7631
- } else {
7632
- await walk(fullPath);
7633
- }
7634
- }
7635
- } else if (stat3.isFile() && testFileRe.test(entry.name)) {
7636
- let resolvedTarget;
7637
- try {
7638
- resolvedTarget = await fsPromises.realpath(fullPath);
7639
- } catch {
7640
- continue;
7641
- }
7642
- if (!hasExcludedAncestor(resolvedTarget)) {
7643
- results.push(relative2(cwd, fullPath));
7644
- }
7645
- }
7646
- } else if (entry.isFile() && testFileRe.test(entry.name)) {
7647
- results.push(relative2(cwd, fullPath));
7648
- }
7766
+ await processEntry(entry, dir, ctx);
7649
7767
  }
7650
- };
7768
+ }
7651
7769
  await walk(cwd);
7652
7770
  if (results.length === 0) {
7653
- if (logger) {
7654
- logger.warn("discoverTestFiles: no test files found in %s; falling back to Bun discovery", cwd);
7655
- }
7771
+ logger?.warn("discoverTestFiles: no test files found in %s; falling back to Bun discovery", cwd);
7656
7772
  return;
7657
7773
  }
7658
- results.sort();
7659
- if (logger) {
7660
- logger.debug("discoverTestFiles: found %d test files in %s", results.length, cwd);
7661
- }
7774
+ results.sort((a, b) => a.localeCompare(b));
7775
+ logger?.debug("discoverTestFiles: found %d test files in %s", results.length, cwd);
7662
7776
  return results;
7663
7777
  }
7664
7778
  // src/bun-test-runner.ts
7779
+ function sleep(ms) {
7780
+ return new Promise((resolve3) => {
7781
+ setTimeout(resolve3, ms);
7782
+ });
7783
+ }
7784
+
7665
7785
  class BunTestRunner {
7666
7786
  logger;
7667
7787
  static inject = tokens(commonTokens.logger, commonTokens.options);
@@ -7670,6 +7790,7 @@ class BunTestRunner {
7670
7790
  inspectorTimeout;
7671
7791
  env;
7672
7792
  bunArgs;
7793
+ testFilesOverride;
7673
7794
  mutateGlobs;
7674
7795
  preloadScriptPath;
7675
7796
  coverageFilePath;
@@ -7679,7 +7800,9 @@ class BunTestRunner {
7679
7800
  cachedTestNames;
7680
7801
  baseNameIndex;
7681
7802
  cachedTestFiles;
7803
+ cachedTestFilesCwd;
7682
7804
  cachedEagerModules;
7805
+ cachedEagerModulesCwd;
7683
7806
  lastRegistryTmpPath;
7684
7807
  constructor(logger, options) {
7685
7808
  this.logger = logger;
@@ -7689,6 +7812,12 @@ class BunTestRunner {
7689
7812
  this.inspectorTimeout = bunOptions.inspectorTimeout ?? 5000;
7690
7813
  this.env = bunOptions.env;
7691
7814
  this.bunArgs = bunOptions.bunArgs;
7815
+ if (bunOptions.testFiles?.length === 0) {
7816
+ this.logger.warn("bun.testFiles was set to an empty array — treating as undefined and falling back to auto-discovery");
7817
+ this.testFilesOverride = undefined;
7818
+ } else {
7819
+ this.testFilesOverride = bunOptions.testFiles;
7820
+ }
7692
7821
  this.mutateGlobs = options.mutate ?? [];
7693
7822
  this.logger.debug("BunTestRunner initialized with options: %o", {
7694
7823
  bunPath: this.bunPath,
@@ -7697,26 +7826,68 @@ class BunTestRunner {
7697
7826
  env: this.env,
7698
7827
  bunArgs: this.bunArgs
7699
7828
  });
7829
+ if (this.testFilesOverride?.some((p) => path4.isAbsolute(p))) {
7830
+ const isSandbox = process.cwd().includes(".stryker-tmp/sandbox-");
7831
+ if (isSandbox) {
7832
+ this.logger.warn("bun.testFiles contains absolute path(s) and the current working directory appears to be a Stryker sandbox (%s). " + "Absolute paths point at the ORIGINAL (unmutated) source files — mutations will be silently bypassed. " + "Use relative paths so that Bun resolves them against the sandbox copy.", process.cwd());
7833
+ }
7834
+ }
7700
7835
  }
7701
7836
  get registryPath() {
7702
- return join3(process.cwd(), ".stryker-bun-runner-registry.json");
7837
+ return path4.join(process.cwd(), ".stryker-bun-runner-registry.json");
7703
7838
  }
7704
7839
  get registryTmpPath() {
7705
- return this.registryPath + ".tmp";
7840
+ return `${this.registryPath}.tmp`;
7706
7841
  }
7707
7842
  capabilities() {
7708
7843
  return {
7709
7844
  reloadEnvironment: true
7710
7845
  };
7711
7846
  }
7847
+ async getOrDiscoverTestFiles() {
7848
+ if (this.testFilesOverride !== undefined) {
7849
+ return this.testFilesOverride;
7850
+ }
7851
+ const cwd = process.cwd();
7852
+ if (this.cachedTestFiles !== undefined && this.cachedTestFilesCwd === cwd) {
7853
+ return this.cachedTestFiles;
7854
+ }
7855
+ this.cachedTestFiles = await discoverTestFiles(cwd, this.logger);
7856
+ this.cachedTestFilesCwd = cwd;
7857
+ return this.cachedTestFiles;
7858
+ }
7859
+ testFilesCacheHit(cwd) {
7860
+ if (this.testFilesOverride !== undefined) {
7861
+ return this.testFilesOverride;
7862
+ }
7863
+ return this.cachedTestFiles !== undefined && this.cachedTestFilesCwd === cwd ? this.cachedTestFiles : null;
7864
+ }
7712
7865
  async init() {
7713
7866
  this.logger.debug("BunTestRunner init starting...");
7714
- const tempDir = join3(tmpdir(), "stryker-bun-runner");
7867
+ if (this.preloadScriptPath) {
7868
+ try {
7869
+ await cleanupPreloadScript(this.preloadScriptPath);
7870
+ } catch (error) {
7871
+ this.logger.debug("Failed to clean up previous preload script on re-init: %s", error instanceof Error ? error.message : String(error));
7872
+ }
7873
+ this.preloadScriptPath = undefined;
7874
+ }
7875
+ if (this.coverageFilePath) {
7876
+ try {
7877
+ await cleanupCoverageFile(this.coverageFilePath);
7878
+ } catch (error) {
7879
+ this.logger.debug("Failed to clean up previous coverage file on re-init: %s", error instanceof Error ? error.message : String(error));
7880
+ }
7881
+ this.coverageFilePath = undefined;
7882
+ }
7883
+ const tempDir = path4.join(tmpdir(), "stryker-bun-runner");
7715
7884
  this.tempDir = tempDir;
7716
- this.coverageFilePath = join3(tempDir, `coverage-${Date.now()}.json`);
7885
+ this.coverageFilePath = path4.join(tempDir, `coverage-${Date.now()}.json`);
7717
7886
  this.logger.debug("Generating coverage preload script...");
7718
- if (!this.cachedEagerModules) {
7887
+ const eagerCwd = process.cwd();
7888
+ if (this.cachedEagerModules === undefined || this.cachedEagerModulesCwd !== eagerCwd) {
7719
7889
  this.cachedEagerModules = await resolveEagerModulesFromGlobs(this.mutateGlobs);
7890
+ this.cachedEagerModulesCwd = eagerCwd;
7720
7891
  this.logger.debug("Resolved %d eager modules from mutate globs", this.cachedEagerModules.length);
7721
7892
  }
7722
7893
  this.preloadScriptPath = await generatePreloadScript({
@@ -7726,7 +7897,7 @@ class BunTestRunner {
7726
7897
  });
7727
7898
  this.logger.debug("Preload script generated at: %s", this.preloadScriptPath);
7728
7899
  await this.ensureSanitizedBunfig();
7729
- this.cachedTestFiles = await discoverTestFiles(process.cwd(), this.logger);
7900
+ this.cachedTestFiles = await this.getOrDiscoverTestFiles();
7730
7901
  }
7731
7902
  async ensureSanitizedBunfig() {
7732
7903
  const cwd = process.cwd();
@@ -7736,7 +7907,7 @@ class BunTestRunner {
7736
7907
  if (this.sanitizedBunfigPath) {
7737
7908
  await cleanupSanitizedBunfig(this.sanitizedBunfigPath);
7738
7909
  }
7739
- const tempDir = this.tempDir ?? join3(tmpdir(), "stryker-bun-runner");
7910
+ const tempDir = this.tempDir ?? path4.join(tmpdir(), "stryker-bun-runner");
7740
7911
  this.sanitizedBunfigPath = await generateSanitizedBunfig(cwd, tempDir);
7741
7912
  this.sanitizedBunfigCwd = cwd;
7742
7913
  this.logger.debug("Sanitized bunfig (re)generated at: %s for cwd: %s", this.sanitizedBunfigPath, cwd);
@@ -7745,7 +7916,7 @@ class BunTestRunner {
7745
7916
  async loadRegistryFile() {
7746
7917
  const registryPath = this.registryPath;
7747
7918
  try {
7748
- const raw = await fsPromises2.readFile(registryPath, "utf-8");
7919
+ const raw = await fsPromises2.readFile(registryPath, "utf8");
7749
7920
  const parsed = JSON.parse(raw);
7750
7921
  if (parsed.version !== 1) {
7751
7922
  this.logger.warn("dryRun registry file has unexpected version %s; skipping", String(parsed.version));
@@ -7759,15 +7930,15 @@ class BunTestRunner {
7759
7930
  this.baseNameIndex = new Map(parsed.baseNameIndex);
7760
7931
  this.logger.debug("Loaded dryRun registry from %s (%d entries)", registryPath, this.cachedTestNames.size);
7761
7932
  } catch (err) {
7762
- const code = err?.code;
7933
+ const code = err.code;
7763
7934
  if (code === "ENOENT") {
7764
- this.logger.warn("dryRun registry file not found at %s; killedBy names for static-coverage mutants may be unresolved", registryPath);
7935
+ this.logger.debug("dryRun registry file not found at %s; this worker has no static-coverage registry (expected on non-dryRun workers)", registryPath);
7765
7936
  } else {
7766
7937
  this.logger.warn("Failed to load dryRun registry from %s: %s", registryPath, err instanceof Error ? err.message : String(err));
7767
7938
  }
7768
7939
  }
7769
7940
  }
7770
- buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs) {
7941
+ buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs, inspectorIdToProjectFile) {
7771
7942
  if (executionOrder.length === 0) {
7772
7943
  return parsed.tests.map((t) => {
7773
7944
  const normalizedName = normalizeTestName(t.name);
@@ -7811,16 +7982,19 @@ class BunTestRunner {
7811
7982
  timeSpentMs: timePerTest
7812
7983
  };
7813
7984
  }
7814
- const uniqueName = buildUniqueTestName(testInfo.fullName, testInfo.url);
7985
+ const projectFile = inspectorIdToProjectFile?.get(inspectorId);
7986
+ const uniqueName = projectFile ? buildProjectFileTestName(projectFile, testInfo.fullName) : buildUniqueTestName(testInfo.fullName, testInfo.url);
7987
+ const fileName = projectFile ?? normalizeTestFilePath(testInfo.url);
7815
7988
  const status = testInfo.status;
7816
- const elapsed = testInfo.elapsed !== undefined ? Math.round(testInfo.elapsed / 1e6) : timePerTest;
7989
+ const elapsed = testInfo.elapsed === undefined ? timePerTest : Math.round(testInfo.elapsed / 1e6);
7990
+ const startPosition = testInfo.line === undefined ? undefined : { line: testInfo.line, column: 0 };
7817
7991
  if (status === "fail") {
7818
7992
  const parsedTest = parsed.tests.find((t) => t.name.includes(testInfo.name));
7819
7993
  return {
7820
7994
  id: uniqueName,
7821
7995
  name: uniqueName,
7822
- fileName: normalizeTestFilePath(testInfo.url),
7823
- startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
7996
+ fileName,
7997
+ startPosition,
7824
7998
  status: TestStatus.Failed,
7825
7999
  failureMessage: parsedTest?.failureMessage ?? testInfo.error?.message ?? "Test failed",
7826
8000
  timeSpentMs: elapsed
@@ -7830,8 +8004,8 @@ class BunTestRunner {
7830
8004
  return {
7831
8005
  id: uniqueName,
7832
8006
  name: uniqueName,
7833
- fileName: normalizeTestFilePath(testInfo.url),
7834
- startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
8007
+ fileName,
8008
+ startPosition,
7835
8009
  status: TestStatus.Skipped,
7836
8010
  timeSpentMs: elapsed
7837
8011
  };
@@ -7839,8 +8013,8 @@ class BunTestRunner {
7839
8013
  return {
7840
8014
  id: uniqueName,
7841
8015
  name: uniqueName,
7842
- fileName: normalizeTestFilePath(testInfo.url),
7843
- startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
8016
+ fileName,
8017
+ startPosition,
7844
8018
  status: TestStatus.Success,
7845
8019
  timeSpentMs: elapsed
7846
8020
  };
@@ -7866,8 +8040,7 @@ class BunTestRunner {
7866
8040
  const lineB = "startPosition" in b && b.startPosition ? b.startPosition.line : Infinity;
7867
8041
  return lineA - lineB;
7868
8042
  });
7869
- for (let i = 0;i < group.length; i++) {
7870
- const test = group[i];
8043
+ for (const [i, test] of group.entries()) {
7871
8044
  const uniqueName = `${test.name} [${i}]`;
7872
8045
  test.id = uniqueName;
7873
8046
  test.name = uniqueName;
@@ -7877,6 +8050,7 @@ class BunTestRunner {
7877
8050
  }
7878
8051
  async dryRun() {
7879
8052
  this.logger.debug("Running dry run with inspector-based coverage collection...");
8053
+ const abortController = new AbortController;
7880
8054
  const inspectPort = await getAvailablePort();
7881
8055
  const syncPort = await getAvailablePort();
7882
8056
  this.logger.debug("Using inspector port: %d, sync port: %d", inspectPort, syncPort);
@@ -7893,109 +8067,104 @@ class BunTestRunner {
7893
8067
  };
7894
8068
  }
7895
8069
  const startTime = Date.now();
7896
- let inspector = null;
7897
8070
  let inspectorUrl = null;
7898
- const cwd = process.cwd();
7899
- const bunfigPath = this.sanitizedBunfigPath && this.sanitizedBunfigCwd === cwd ? this.sanitizedBunfigPath : await this.ensureSanitizedBunfig();
7900
- if (!this.cachedTestFiles) {
7901
- this.cachedTestFiles = await discoverTestFiles(process.cwd(), this.logger);
7902
- }
7903
- const testProcess = runBunTests({
7904
- bunPath: this.bunPath,
7905
- timeout: this.timeout,
7906
- env: this.env,
7907
- bunArgs: this.bunArgs,
7908
- bunfigPath,
7909
- preloadScript: this.preloadScriptPath,
7910
- coverageFile: this.coverageFilePath,
7911
- inspectWaitPort: inspectPort,
7912
- sequentialMode: true,
7913
- syncPort,
7914
- testFiles: this.cachedTestFiles,
7915
- onInspectorReady: (url) => {
7916
- inspectorUrl = url;
7917
- }
7918
- });
7919
- const waitStart = Date.now();
7920
- while (!inspectorUrl && Date.now() - waitStart < this.inspectorTimeout) {
7921
- await new Promise((resolve4) => setTimeout(resolve4, 50));
7922
- }
7923
- if (!inspectorUrl) {
7924
- const diagnosticResult = await testProcess;
7925
- const stdoutPreview = (diagnosticResult.stdout ?? "").slice(0, 1000);
7926
- const stderrPreview = (diagnosticResult.stderr ?? "").slice(0, 1000);
7927
- this.logger.error(`Failed to get inspector URL within timeout (%dms).
8071
+ try {
8072
+ const cwd = process.cwd();
8073
+ const bunfigPath = this.sanitizedBunfigPath && this.sanitizedBunfigCwd === cwd ? this.sanitizedBunfigPath : await this.ensureSanitizedBunfig();
8074
+ const testFilesCached = this.testFilesCacheHit(cwd);
8075
+ const testFiles = testFilesCached === null ? await this.getOrDiscoverTestFiles() : testFilesCached;
8076
+ const testProcess = runBunTests({
8077
+ bunPath: this.bunPath,
8078
+ timeout: this.timeout,
8079
+ env: this.env,
8080
+ bunArgs: this.bunArgs,
8081
+ bunfigPath,
8082
+ preloadScript: this.preloadScriptPath,
8083
+ coverageFile: this.coverageFilePath,
8084
+ inspectWaitPort: inspectPort,
8085
+ sequentialMode: true,
8086
+ syncPort,
8087
+ testFiles,
8088
+ signal: abortController.signal,
8089
+ onInspectorReady: (url) => {
8090
+ inspectorUrl = url;
8091
+ }
8092
+ });
8093
+ const waitStart = Date.now();
8094
+ while (!inspectorUrl && Date.now() - waitStart < this.inspectorTimeout) {
8095
+ await sleep(50);
8096
+ }
8097
+ if (!inspectorUrl) {
8098
+ const diagnosticResult = await testProcess;
8099
+ const stdoutPreview = diagnosticResult.stdout.slice(0, 1000);
8100
+ const stderrPreview = diagnosticResult.stderr.slice(0, 1000);
8101
+ this.logger.error(`Failed to get inspector URL within timeout (%dms).
7928
8102
  exit=%s timedOut=%s
7929
8103
  ` + `--- STDOUT (first 1000 chars) ---
7930
8104
  %s
7931
8105
  ` + `--- STDERR (first 1000 chars) ---
7932
8106
  %s`, this.inspectorTimeout, String(diagnosticResult.exitCode), String(diagnosticResult.timedOut), stdoutPreview || "(empty)", stderrPreview || "(empty)");
7933
- await syncServer.close();
7934
- return {
7935
- status: DryRunStatus.Error,
7936
- errorMessage: "Timeout waiting for inspector URL"
7937
- };
7938
- }
7939
- this.logger.debug("Inspector URL: %s", inspectorUrl);
7940
- inspector = new InspectorClient({
7941
- url: inspectorUrl,
7942
- connectionTimeout: this.inspectorTimeout,
7943
- requestTimeout: this.inspectorTimeout,
7944
- handlers: {}
7945
- });
7946
- try {
7947
- await inspector.connect();
7948
- await inspector.send("TestReporter.enable", {});
7949
- this.logger.debug("Inspector connected and TestReporter enabled");
7950
- } catch (error) {
7951
- const errorMsg = error instanceof Error ? error.message : String(error);
7952
- this.logger.error("Failed to connect inspector: %s", errorMsg);
8107
+ return {
8108
+ status: DryRunStatus.Error,
8109
+ errorMessage: "Timeout waiting for inspector URL"
8110
+ };
8111
+ }
8112
+ this.logger.debug("Inspector URL: %s", inspectorUrl);
8113
+ const inspector = new InspectorClient({
8114
+ url: inspectorUrl,
8115
+ connectionTimeout: this.inspectorTimeout,
8116
+ requestTimeout: this.inspectorTimeout,
8117
+ handlers: {}
8118
+ });
8119
+ try {
8120
+ await inspector.connect();
8121
+ await inspector.send("TestReporter.enable", {});
8122
+ this.logger.debug("Inspector connected and TestReporter enabled");
8123
+ } catch (error) {
8124
+ const errorMsg = error instanceof Error ? error.message : String(error);
8125
+ this.logger.error("Failed to connect inspector: %s", errorMsg);
8126
+ abortController.abort();
8127
+ await inspector.close();
8128
+ return {
8129
+ status: DryRunStatus.Error,
8130
+ errorMessage: `Failed to connect to Bun inspector: ${errorMsg}`
8131
+ };
8132
+ }
8133
+ syncServer.signalReady();
8134
+ this.logger.debug("Signaled preload script to proceed");
8135
+ const result = await testProcess;
8136
+ const totalElapsedMs = Date.now() - startTime;
8137
+ const testHierarchy = inspector.getTests();
8138
+ const executionOrder = inspector.getExecutionOrder();
7953
8139
  await inspector.close();
7954
- await syncServer.close();
7955
- return {
7956
- status: DryRunStatus.Error,
7957
- errorMessage: `Failed to connect to Bun inspector: ${errorMsg}`
7958
- };
7959
- }
7960
- syncServer.signalReady();
7961
- this.logger.debug("Signaled preload script to proceed");
7962
- const result = await testProcess;
7963
- const totalElapsedMs = Date.now() - startTime;
7964
- const testHierarchy = inspector.getTests();
7965
- const executionOrder = inspector.getExecutionOrder();
7966
- await inspector.close();
7967
- await syncServer.close();
7968
- this.logger.debug("Inspector collected %d tests in hierarchy, %d in execution order", testHierarchy.length, executionOrder.length);
7969
- if (result.timedOut) {
7970
- this.logger.warn("Dry run timed out");
7971
- return { status: DryRunStatus.Timeout };
7972
- }
7973
- const parsed = parseBunTestOutput(result.stdout, result.stderr);
7974
- if (result.exitCode !== 0 && parsed.failed === 0) {
8140
+ this.logger.debug("Inspector collected %d tests in hierarchy, %d in execution order", testHierarchy.length, executionOrder.length);
8141
+ const parsed = parseBunTestOutput(result.stdout, result.stderr);
8142
+ const earlyResult = this.checkDryRunProcessResult(result, parsed);
8143
+ if (earlyResult) {
8144
+ return earlyResult;
8145
+ }
8146
+ const { coverage: mutantCoverage, inspectorIdToProjectFile } = await this.collectAndRemapCoverage(testHierarchy, executionOrder);
8147
+ const tests = this.buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs, inspectorIdToProjectFile);
8148
+ tests.sort((a, b) => a.name.localeCompare(b.name));
8149
+ await this.buildAndPersistTestRegistry(tests);
7975
8150
  return {
7976
- status: DryRunStatus.Error,
7977
- errorMessage: `Bun test process failed with exit code ${result.exitCode}
7978
- ${result.stderr}`
8151
+ status: DryRunStatus.Complete,
8152
+ tests,
8153
+ mutantCoverage
7979
8154
  };
8155
+ } finally {
8156
+ abortController.abort();
8157
+ await syncServer.close();
7980
8158
  }
7981
- let mutantCoverage;
7982
- if (this.coverageFilePath) {
7983
- mutantCoverage = await collectCoverage(this.coverageFilePath, this.logger);
7984
- await cleanupCoverageFile(this.coverageFilePath);
7985
- }
7986
- if (mutantCoverage) {
7987
- const testMap = new Map(testHierarchy.map((t) => [t.id, t]));
7988
- mutantCoverage = mapCoverageToInspectorIds(mutantCoverage, executionOrder, testMap, this.logger);
7989
- }
7990
- const tests = this.buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs);
7991
- tests.sort((a, b) => a.name.localeCompare(b.name));
8159
+ }
8160
+ async buildAndPersistTestRegistry(tests) {
7992
8161
  this.cachedTestNames = new Set(tests.map((t) => t.name));
7993
8162
  if (tests.length !== this.cachedTestNames.size) {
7994
8163
  const nameCount = new Map;
7995
8164
  for (const test of tests) {
7996
8165
  nameCount.set(test.name, (nameCount.get(test.name) ?? 0) + 1);
7997
8166
  }
7998
- const duplicates = Array.from(nameCount.entries()).filter(([_, count]) => count > 1).map(([name, count]) => `"${name}" (${count}x)`);
8167
+ const duplicates = [...nameCount.entries()].filter(([_, count]) => count > 1).map(([name, count]) => `"${name}" (${count}x)`);
7999
8168
  this.logger.warn("Found %d duplicate test names (total: %d, unique: %d): %s", tests.length - this.cachedTestNames.size, tests.length, this.cachedTestNames.size, duplicates.join(", "));
8000
8169
  }
8001
8170
  this.baseNameIndex = new Map;
@@ -8019,49 +8188,29 @@ ${result.stderr}`
8019
8188
  const registryData = JSON.stringify({
8020
8189
  version: 1,
8021
8190
  writtenAt: Date.now(),
8022
- cachedTestNames: Array.from(this.cachedTestNames),
8023
- baseNameIndex: Array.from(this.baseNameIndex.entries())
8191
+ cachedTestNames: [...this.cachedTestNames],
8192
+ baseNameIndex: [...this.baseNameIndex.entries()]
8024
8193
  });
8025
- await fsPromises2.writeFile(tmpPath, registryData, "utf-8");
8194
+ await fsPromises2.writeFile(tmpPath, registryData, "utf8");
8026
8195
  this.lastRegistryTmpPath = tmpPath;
8027
8196
  await fsPromises2.rename(tmpPath, registryPath);
8197
+ this.lastRegistryTmpPath = undefined;
8028
8198
  this.logger.debug("Wrote dryRun registry to %s (%d entries)", registryPath, this.cachedTestNames.size);
8029
- } catch (registryErr) {
8030
- this.logger.warn("Failed to write dryRun registry file: %s", registryErr instanceof Error ? registryErr.message : String(registryErr));
8199
+ } catch (error) {
8200
+ this.logger.warn("Failed to write dryRun registry file: %s", error instanceof Error ? error.message : String(error));
8031
8201
  }
8032
- return {
8033
- status: DryRunStatus.Complete,
8034
- tests,
8035
- mutantCoverage
8036
- };
8037
8202
  }
8038
8203
  async mutantRun(options) {
8039
8204
  this.logger.debug("Running mutant run for mutant %s", options.activeMutant.id);
8040
8205
  const testNamePattern = buildTestNamePattern(options.testFilter ?? []);
8041
8206
  const localTestFilter = options.testFilter ?? [];
8042
- const localRegistry = new Set(localTestFilter);
8043
- const localSuffixRe = / \[\d+\]$/;
8044
- const localBaseIndex = new Map;
8045
- for (const id of localRegistry) {
8046
- const base = localSuffixRe.test(id) ? id.replace(localSuffixRe, "") : id;
8047
- const bucket = localBaseIndex.get(base);
8048
- if (bucket) {
8049
- bucket.push(id);
8050
- } else {
8051
- localBaseIndex.set(base, [id]);
8052
- }
8053
- if (base !== id) {
8054
- localBaseIndex.set(id, [id]);
8055
- }
8056
- }
8207
+ const { localRegistry, localBaseIndex } = this.buildLocalTestFilterIndex(localTestFilter);
8057
8208
  if (!this.cachedTestNames) {
8058
8209
  await this.loadRegistryFile();
8059
8210
  }
8060
8211
  const mutantCwd = process.cwd();
8061
8212
  const bunfigPath = this.sanitizedBunfigPath && this.sanitizedBunfigCwd === mutantCwd ? this.sanitizedBunfigPath : await this.ensureSanitizedBunfig();
8062
- if (!this.cachedTestFiles) {
8063
- this.cachedTestFiles = await discoverTestFiles(process.cwd(), this.logger);
8064
- }
8213
+ this.cachedTestFiles = await this.getOrDiscoverTestFiles();
8065
8214
  const result = await runBunTests({
8066
8215
  bunPath: this.bunPath,
8067
8216
  timeout: this.timeout,
@@ -8089,72 +8238,128 @@ ${result.stderr}`
8089
8238
  exitCode: result.exitCode
8090
8239
  });
8091
8240
  if (result.exitCode !== 0) {
8092
- const rawFailedNames = parsed.tests.filter((test) => test.status === "failed").map((test) => normalizeTestName(test.name));
8093
- if (rawFailedNames.length === 0 && parsed.tests.length === 0) {
8094
- const stderr = result.stderr ?? "";
8095
- const isRuntimeError = stderr.includes("Unhandled error") || stderr.includes("Cannot find module") || stderr.includes("SyntaxError") || stderr.includes("TypeError") || stderr.includes("ReferenceError") || stderr.includes("is not defined") || stderr.includes("Unexpected token");
8096
- if (isRuntimeError) {
8097
- this.logger.debug("Mutant %s caused runtime error (tests could not run): %s", options.activeMutant.id, stderr.slice(0, 200));
8098
- return {
8099
- status: MutantRunStatus.Error,
8100
- errorMessage: stderr.slice(0, 500) || `Runtime error with exit code ${result.exitCode}`
8101
- };
8102
- }
8103
- }
8104
- const killedBySet = new Set;
8105
- for (const name of rawFailedNames) {
8106
- if (localRegistry.has(name)) {
8107
- killedBySet.add(name);
8108
- continue;
8109
- }
8110
- const localBucket = localBaseIndex.get(name);
8111
- if (localBucket) {
8112
- this.logger.debug('Expanded killedBy base name "%s" → %d local registry IDs for mutant %s', name, localBucket.length, options.activeMutant.id);
8113
- for (const id of localBucket) {
8114
- killedBySet.add(id);
8115
- }
8116
- continue;
8117
- }
8118
- if (this.cachedTestNames?.has(name)) {
8119
- killedBySet.add(name);
8120
- continue;
8121
- }
8122
- const instanceBucket = this.baseNameIndex?.get(name);
8123
- if (instanceBucket) {
8124
- this.logger.debug('Expanded killedBy base name "%s" → %d instance registry IDs for mutant %s', name, instanceBucket.length, options.activeMutant.id);
8125
- for (const id of instanceBucket) {
8126
- killedBySet.add(id);
8127
- }
8128
- continue;
8129
- }
8130
- this.logger.warn('killedBy name "%s" for mutant %s not found in test registry; ' + "including as-is (may break incremental cache)", name, options.activeMutant.id);
8131
- killedBySet.add(name);
8241
+ return this.buildMutantKilledResult(result, parsed, localRegistry, localBaseIndex, options.activeMutant.id);
8242
+ }
8243
+ return {
8244
+ status: MutantRunStatus.Survived,
8245
+ nrOfTests: parsed.totalTests
8246
+ };
8247
+ }
8248
+ buildMutantKilledResult(result, parsed, localRegistry, localBaseIndex, mutantId) {
8249
+ const rawFailedNames = parsed.tests.filter((test) => test.status === "failed").map((test) => normalizeTestName(test.name));
8250
+ if (rawFailedNames.length === 0 && parsed.tests.length === 0) {
8251
+ const runtimeResult = this.checkRuntimeError(result, mutantId);
8252
+ if (runtimeResult) {
8253
+ return runtimeResult;
8132
8254
  }
8133
- const killedBy = Array.from(killedBySet);
8134
- if (killedBy.length === 0) {
8135
- const stdoutPreview = (result.stdout ?? "").slice(0, 600);
8136
- const stderrPreview = (result.stderr ?? "").slice(0, 600);
8137
- this.logger.warn("No failed tests identified for mutant %s — Bun output could not be parsed; " + `using "unknown" fallback (breaks incremental cache)
8255
+ }
8256
+ const killedBy = this.resolveKilledBy(rawFailedNames, localRegistry, localBaseIndex, mutantId);
8257
+ if (killedBy.length === 0) {
8258
+ this.logger.warn("No failed tests identified for mutant %s — Bun output could not be parsed; " + `using "unknown" fallback (breaks incremental cache)
8138
8259
  ` + `exit=%s
8139
8260
  --- STDOUT (first 600 chars) ---
8140
8261
  %s
8141
8262
  ` + `--- STDERR (first 600 chars) ---
8142
- %s`, options.activeMutant.id, String(result.exitCode), stdoutPreview || "(empty)", stderrPreview || "(empty)");
8143
- killedBy.push("unknown");
8144
- }
8145
- return {
8146
- status: MutantRunStatus.Killed,
8147
- killedBy,
8148
- failureMessage: parsed.tests.filter((test) => test.status === "failed").map((test) => test.failureMessage).filter((msg) => !!msg).join(`
8263
+ %s`, mutantId, String(result.exitCode), result.stdout.slice(0, 600) || "(empty)", result.stderr.slice(0, 600) || "(empty)");
8264
+ killedBy.push("unknown");
8265
+ }
8266
+ return {
8267
+ status: MutantRunStatus.Killed,
8268
+ killedBy,
8269
+ failureMessage: parsed.tests.filter((test) => test.status === "failed").map((test) => test.failureMessage).filter((msg) => !!msg).join(`
8149
8270
 
8150
8271
  `) || `Tests failed with exit code ${result.exitCode}`,
8151
- nrOfTests: parsed.totalTests || 1
8272
+ nrOfTests: parsed.totalTests || 1
8273
+ };
8274
+ }
8275
+ checkRuntimeError(result, mutantId) {
8276
+ const stderr = result.stderr;
8277
+ const isRuntimeError = stderr.includes("Unhandled error") || stderr.includes("Cannot find module") || stderr.includes("SyntaxError") || stderr.includes("TypeError") || stderr.includes("ReferenceError") || stderr.includes("is not defined") || stderr.includes("Unexpected token");
8278
+ if (isRuntimeError) {
8279
+ this.logger.debug("Mutant %s caused runtime error (tests could not run): %s", mutantId, stderr.slice(0, 200));
8280
+ return {
8281
+ status: MutantRunStatus.Error,
8282
+ errorMessage: stderr.slice(0, 500) || `Runtime error with exit code ${result.exitCode}`
8152
8283
  };
8153
8284
  }
8154
- return {
8155
- status: MutantRunStatus.Survived,
8156
- nrOfTests: parsed.totalTests
8157
- };
8285
+ return null;
8286
+ }
8287
+ checkDryRunProcessResult(result, parsed) {
8288
+ if (result.timedOut) {
8289
+ this.logger.warn("Dry run timed out");
8290
+ return { status: DryRunStatus.Timeout };
8291
+ }
8292
+ if (result.exitCode !== 0 && parsed.failed === 0) {
8293
+ return {
8294
+ status: DryRunStatus.Error,
8295
+ errorMessage: `Bun test process failed with exit code ${result.exitCode}
8296
+ ${result.stderr}`
8297
+ };
8298
+ }
8299
+ return null;
8300
+ }
8301
+ async collectAndRemapCoverage(testHierarchy, executionOrder) {
8302
+ const testMap = new Map(testHierarchy.map((t) => [t.id, t]));
8303
+ if (!this.coverageFilePath) {
8304
+ return { coverage: undefined, inspectorIdToProjectFile: new Map };
8305
+ }
8306
+ const rawCoverage = await collectCoverage(this.coverageFilePath, this.logger);
8307
+ await cleanupCoverageFile(this.coverageFilePath);
8308
+ if (!rawCoverage) {
8309
+ return { coverage: undefined, inspectorIdToProjectFile: new Map };
8310
+ }
8311
+ const { coverage, inspectorIdToProjectFile } = mapCoverageToInspectorIds(rawCoverage, executionOrder, testMap, this.logger);
8312
+ return { coverage, inspectorIdToProjectFile };
8313
+ }
8314
+ buildLocalTestFilterIndex(testFilter) {
8315
+ const localRegistry = new Set(testFilter);
8316
+ const localSuffixRe = / \[\d+\]$/;
8317
+ const localBaseIndex = new Map;
8318
+ for (const id of localRegistry) {
8319
+ const base = localSuffixRe.test(id) ? id.replace(localSuffixRe, "") : id;
8320
+ const bucket = localBaseIndex.get(base);
8321
+ if (bucket) {
8322
+ bucket.push(id);
8323
+ } else {
8324
+ localBaseIndex.set(base, [id]);
8325
+ }
8326
+ if (base !== id) {
8327
+ localBaseIndex.set(id, [id]);
8328
+ }
8329
+ }
8330
+ return { localRegistry, localBaseIndex };
8331
+ }
8332
+ resolveKilledBy(rawFailedNames, localRegistry, localBaseIndex, mutantId) {
8333
+ const killedBySet = new Set;
8334
+ for (const name of rawFailedNames) {
8335
+ if (localRegistry.has(name)) {
8336
+ killedBySet.add(name);
8337
+ continue;
8338
+ }
8339
+ const localBucket = localBaseIndex.get(name);
8340
+ if (localBucket) {
8341
+ this.logger.debug('Expanded killedBy base name "%s" → %d local registry IDs for mutant %s', name, localBucket.length, mutantId);
8342
+ for (const id of localBucket) {
8343
+ killedBySet.add(id);
8344
+ }
8345
+ continue;
8346
+ }
8347
+ if (this.cachedTestNames?.has(name)) {
8348
+ killedBySet.add(name);
8349
+ continue;
8350
+ }
8351
+ const instanceBucket = this.baseNameIndex?.get(name);
8352
+ if (instanceBucket) {
8353
+ this.logger.debug('Expanded killedBy base name "%s" → %d instance registry IDs for mutant %s', name, instanceBucket.length, mutantId);
8354
+ for (const id of instanceBucket) {
8355
+ killedBySet.add(id);
8356
+ }
8357
+ continue;
8358
+ }
8359
+ this.logger.debug('killedBy name "%s" for mutant %s not found in test registry; including as-is', name, mutantId);
8360
+ killedBySet.add(name);
8361
+ }
8362
+ return [...killedBySet];
8158
8363
  }
8159
8364
  async dispose() {
8160
8365
  this.logger.debug("Disposing BunTestRunner");
@@ -8202,7 +8407,7 @@ var strykerValidationSchema = {
8202
8407
  timeout: {
8203
8408
  type: "number",
8204
8409
  minimum: 0,
8205
- description: "Timeout per test in milliseconds (default: 10000)",
8410
+ description: "Child-process timeout in milliseconds (default: 10000). Controls how long the entire bun test subprocess may run before being killed. Independent from the per-test timeout in bunfig.toml [test].timeout, which Bun uses to declare individual tests timed out.",
8206
8411
  default: 1e4
8207
8412
  },
8208
8413
  inspectorTimeout: {
@@ -8224,6 +8429,14 @@ var strykerValidationSchema = {
8224
8429
  items: {
8225
8430
  type: "string"
8226
8431
  }
8432
+ },
8433
+ testFiles: {
8434
+ type: "array",
8435
+ description: "Explicit list of test file paths (relative paths preferred in Stryker context — absolute paths bypass the sandbox copy and will NOT be mutated). When provided, skips auto-discovery and uses this list verbatim. Relative paths resolve against the bun subprocess's cwd. An empty array is invalid (use undefined/omit to enable auto-discovery).",
8436
+ minItems: 1,
8437
+ items: {
8438
+ type: "string"
8439
+ }
8227
8440
  }
8228
8441
  },
8229
8442
  additionalProperties: false