@t09tanaka/test-gates 0.4.2 → 0.4.3

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/cli.js CHANGED
@@ -1034,13 +1034,13 @@ function judgeSelfcheck(input) {
1034
1034
  function selectControls(input) {
1035
1035
  const { gates, mode, only, shard } = input;
1036
1036
  if (only.length > 0) {
1037
- const unknown = only.filter((path13) => !gates.includes(path13));
1037
+ const unknown = only.filter((path15) => !gates.includes(path15));
1038
1038
  if (unknown.length > 0) {
1039
1039
  return {
1040
1040
  error: `--gate ${unknown.join(", ")}: not a gate of test-gates.json (write the path as it is written there)`
1041
1041
  };
1042
1042
  }
1043
- return { selected: gates.filter((path13) => only.includes(path13)) };
1043
+ return { selected: gates.filter((path15) => only.includes(path15)) };
1044
1044
  }
1045
1045
  if (shard !== null) {
1046
1046
  if (shard.total > gates.length) {
@@ -1193,8 +1193,8 @@ function planControls(input) {
1193
1193
  if (silent !== void 0) {
1194
1194
  return everyoneAlone(`the gate run left no note for the spec of ${silent.path}`);
1195
1195
  }
1196
- const partsOf2 = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
1197
- const short = all.find((gate) => accountedFor(totals[gate.file], partsOf2(gate)) === "no");
1196
+ const partsOf3 = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
1197
+ const short = all.find((gate) => accountedFor(totals[gate.file], partsOf3(gate)) === "no");
1198
1198
  if (short !== void 0) {
1199
1199
  return everyoneAlone(
1200
1200
  `the notes of the specs do not add up to what the runner counted for ${short.path}`
@@ -1208,7 +1208,7 @@ function planControls(input) {
1208
1208
  if (all.some((other) => other.path !== gate.path && other.path.includes(gate.path))) {
1209
1209
  return false;
1210
1210
  }
1211
- if (accountedFor(totals[gate.file], partsOf2(gate)) !== "yes") {
1211
+ if (accountedFor(totals[gate.file], partsOf3(gate)) !== "yes") {
1212
1212
  return false;
1213
1213
  }
1214
1214
  const touching = notes.filter((note) => {
@@ -1419,9 +1419,9 @@ function runCheck(dir, io2) {
1419
1419
  }
1420
1420
 
1421
1421
  // src/commands/doctor.ts
1422
- import { spawnSync as spawnSync3 } from "child_process";
1423
- import fs9 from "fs";
1424
- import path9 from "path";
1422
+ import { spawnSync as spawnSync5 } from "child_process";
1423
+ import fs11 from "fs";
1424
+ import path11 from "path";
1425
1425
 
1426
1426
  // src/core/mutation-result.ts
1427
1427
  var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
@@ -1688,6 +1688,80 @@ function judgeMutationReport(input) {
1688
1688
  }
1689
1689
  return { violations, survivors, unexpectedTimeouts, summary };
1690
1690
  }
1691
+ var UNKNOWN_TEST = "not-in-the-first-run:";
1692
+ function sameMutant(a, b) {
1693
+ return a.mutatorName === b.mutatorName && (a.replacement ?? "") === (b.replacement ?? "") && a.location.start.line === b.location.start.line && a.location.start.column === b.location.start.column && a.location.end.line === b.location.end.line && a.location.end.column === b.location.end.column;
1694
+ }
1695
+ function mergeRetry(first, retry, gates) {
1696
+ const outcome = { retried: 0, statuses: {}, missing: 0 };
1697
+ const firstId = translator(first, retry);
1698
+ const files = { ...first.files };
1699
+ const again = retry.files ?? {};
1700
+ for (const named of gates) {
1701
+ const gate = named.path.split("/").filter((part) => part !== ".").join("/");
1702
+ const result = Object.hasOwn(files, gate) ? files[gate] : void 0;
1703
+ if (result?.mutants === void 0) {
1704
+ continue;
1705
+ }
1706
+ const expected = expectedTimeoutMutants(named, result.source, result.mutants);
1707
+ const rerun = Object.hasOwn(again, gate) ? again[gate]?.mutants : void 0;
1708
+ files[gate] = {
1709
+ ...result,
1710
+ mutants: result.mutants.map((mutant) => {
1711
+ if (mutant.status !== "Timeout" || expected.has(mutant)) {
1712
+ return mutant;
1713
+ }
1714
+ outcome.retried += 1;
1715
+ const found = rerun?.find((other) => sameMutant(other, mutant));
1716
+ if (found === void 0) {
1717
+ outcome.missing += 1;
1718
+ return mutant;
1719
+ }
1720
+ outcome.statuses[found.status] = (outcome.statuses[found.status] ?? 0) + 1;
1721
+ return {
1722
+ ...found,
1723
+ ...found.coveredBy === void 0 ? {} : { coveredBy: found.coveredBy.map(firstId) },
1724
+ ...found.killedBy === void 0 ? {} : { killedBy: found.killedBy.map(firstId) }
1725
+ };
1726
+ })
1727
+ };
1728
+ }
1729
+ return { report: { ...first, files }, outcome };
1730
+ }
1731
+ function testKey(file, test) {
1732
+ const start = test.location?.start;
1733
+ return JSON.stringify([file, test.name ?? "", start?.line, start?.column]);
1734
+ }
1735
+ function* keyedTests(report) {
1736
+ for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
1737
+ if (entry.tests !== void 0) {
1738
+ for (const test of entry.tests) {
1739
+ yield { id: test.id, key: testKey(file, test) };
1740
+ }
1741
+ }
1742
+ }
1743
+ }
1744
+ function translator(first, retry) {
1745
+ const firstIds = /* @__PURE__ */ new Map();
1746
+ for (const { id, key } of keyedTests(first)) {
1747
+ firstIds.set(key, firstIds.has(key) ? null : id);
1748
+ }
1749
+ const ids = /* @__PURE__ */ new Map();
1750
+ for (const { id, key } of keyedTests(retry)) {
1751
+ ids.set(id, firstIds.get(key));
1752
+ }
1753
+ return (id) => {
1754
+ const found = ids.get(id);
1755
+ return typeof found === "string" ? found : `${UNKNOWN_TEST}${id}`;
1756
+ };
1757
+ }
1758
+ function formatRetry(outcome) {
1759
+ const statuses = Object.entries(outcome.statuses).sort(([a], [b]) => a.localeCompare(b)).map(([status, count]) => `${count} ${status}`);
1760
+ if (outcome.missing > 0) {
1761
+ statuses.push(`${outcome.missing} not in the report (still Timeout)`);
1762
+ }
1763
+ return `test-gates mutation: ${outcome.retried} mutant(s) that timed out were run again one at a time (--concurrency 1): ${statuses.join(", ")}`;
1764
+ }
1691
1765
  function formatSurvivor(survivor) {
1692
1766
  const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
1693
1767
  return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
@@ -1697,7 +1771,7 @@ function formatSummary(summary) {
1697
1771
  }
1698
1772
 
1699
1773
  // src/core/reuse-guard.ts
1700
- var RECORD_FORMAT = 2;
1774
+ var RECORD_FORMAT = 3;
1701
1775
  function isStringRecord(value) {
1702
1776
  return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
1703
1777
  }
@@ -1709,11 +1783,14 @@ function parseAffected(value) {
1709
1783
  }
1710
1784
  const parsed = /* @__PURE__ */ Object.create(null);
1711
1785
  for (const [gate, entry] of Object.entries(gates)) {
1712
- const { specs, files } = entry ?? {};
1786
+ const { specs, files, blind } = entry ?? {};
1713
1787
  if (!isStringList(specs) || !isStringList(files)) {
1714
1788
  return null;
1715
1789
  }
1716
- parsed[gate] = { specs, files };
1790
+ if (blind !== void 0 && typeof blind !== "string") {
1791
+ return null;
1792
+ }
1793
+ parsed[gate] = { specs, files, ...blind === void 0 ? {} : { blind } };
1717
1794
  }
1718
1795
  return { tests, gates: parsed };
1719
1796
  }
@@ -1876,7 +1953,8 @@ var CONFIG_NAMES = [
1876
1953
  /\.d\.[cm]?ts$/
1877
1954
  ];
1878
1955
  var ROOT_MOCKS = "__mocks__";
1879
- var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc)$/i;
1956
+ var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc|html?)$/i;
1957
+ var ENTRY_PAGE = /^index\.html?$/i;
1880
1958
  var PASSIVE = /\.(?:[cm]?[jt]sx?|json|snap|css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico)$/i;
1881
1959
  function kindOf(key, subproject) {
1882
1960
  const segments = localSegments(key, subproject);
@@ -1887,7 +1965,7 @@ function kindOf(key, subproject) {
1887
1965
  if (isNestedMock(key, subproject)) {
1888
1966
  return "other";
1889
1967
  }
1890
- if (DOCUMENT.test(name)) {
1968
+ if (DOCUMENT.test(name) && !ENTRY_PAGE.test(name)) {
1891
1969
  return "document";
1892
1970
  }
1893
1971
  const inside = subproject === "" || key.startsWith(`${subproject}/`);
@@ -1904,6 +1982,7 @@ function isNestedMock(key, subproject) {
1904
1982
  }
1905
1983
  function gateDependencies(input) {
1906
1984
  const closureOf = (key) => Object.hasOwn(input.closures, key) ? input.closures[key] : [key];
1985
+ const blind = input.blind ?? {};
1907
1986
  const result = /* @__PURE__ */ Object.create(null);
1908
1987
  for (const gate of input.gates) {
1909
1988
  const covering = Object.hasOwn(input.covering, gate.key) ? input.covering[gate.key] : [];
@@ -1917,7 +1996,13 @@ function gateDependencies(input) {
1917
1996
  for (const spec of specs) {
1918
1997
  closureOf(spec).forEach((file) => files.add(file));
1919
1998
  }
1920
- result[gate.key] = { specs: [...specs].sort(), files: [...files].sort() };
1999
+ const sorted2 = [...files].sort();
2000
+ const reason = sorted2.find((file) => Object.hasOwn(blind, file));
2001
+ result[gate.key] = {
2002
+ specs: [...specs].sort(),
2003
+ files: sorted2,
2004
+ ...reason === void 0 ? {} : { blind: blind[reason] }
2005
+ };
1921
2006
  }
1922
2007
  return result;
1923
2008
  }
@@ -1948,6 +2033,15 @@ function planAffected(input) {
1948
2033
  rerun.push({ gate: gate.path, changed: [] });
1949
2034
  continue;
1950
2035
  }
2036
+ const blind = gate.blind ?? last.blind;
2037
+ if (blind !== void 0) {
2038
+ if (changed.length > 0 || input.seeded === true) {
2039
+ rerun.push({ gate: gate.path, changed, blind });
2040
+ } else {
2041
+ reused.push(gate.path);
2042
+ }
2043
+ continue;
2044
+ }
1951
2045
  const files = [.../* @__PURE__ */ new Set([...last.files, ...gate.files])];
1952
2046
  const differing = files.filter((file) => input.before.files[file] !== input.now[file]).sort();
1953
2047
  if (differing.length > 0) {
@@ -1999,13 +2093,23 @@ function describePlan(plan, shown = 5, timeouts = 0, expected = 0) {
1999
2093
  const lines = [
2000
2094
  `test-gates mutation --affected: ${plan.reused.length} of ${total} gate(s) reused, ${plan.rerun.length} run again`
2001
2095
  ];
2002
- for (const { gate, changed } of plan.rerun.slice(0, shown)) {
2096
+ const told = plan.rerun.filter((entry) => entry.blind === void 0);
2097
+ for (const { gate, changed } of told.slice(0, shown)) {
2003
2098
  lines.push(
2004
2099
  changed.length === 0 ? ` ${gate}: the last run left no result of it that can be reused` : ` ${gate}: changed ${listed(changed)}`
2005
2100
  );
2006
2101
  }
2007
- if (plan.rerun.length > shown) {
2008
- lines.push(` and ${plan.rerun.length - shown} more`);
2102
+ if (told.length > shown) {
2103
+ lines.push(` and ${told.length - shown} more`);
2104
+ }
2105
+ const reasons = [
2106
+ ...new Set(plan.rerun.flatMap((entry) => entry.blind === void 0 ? [] : [entry.blind]))
2107
+ ];
2108
+ if (reasons.length > 0) {
2109
+ const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
2110
+ lines.push(
2111
+ ` ${plan.rerun.length - told.length} gate(s) run again for any change, as what they depend on cannot be told: ${reasons[0]}${more}`
2112
+ );
2009
2113
  }
2010
2114
  if (timeouts > 0) {
2011
2115
  lines.push(` and ${timeouts} mutant(s) of reused gates that timed out in the last run`);
@@ -2052,12 +2156,22 @@ function diagnose(facts) {
2052
2156
  fix: "write such a path from the directory of its file (path.resolve(__dirname, \u2026)) so that it can be followed, or keep it out of the files the run loads"
2053
2157
  });
2054
2158
  }
2159
+ const everyBlind = facts.blindGates.length >= facts.gates;
2055
2160
  if (facts.blind !== null) {
2056
2161
  findings.push({
2057
2162
  level: facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
2058
2163
  text: `with --affected, a change of any file runs every mutant: what a gate depends on cannot be told (${facts.blind})`,
2059
2164
  fix: "keep what reads files or loads modules by a computed name out of what the gates and their specs import, or run without --affected"
2060
2165
  });
2166
+ } else if (facts.blindGates.length > 0) {
2167
+ const gates = facts.blindGates.map((one) => one.gate);
2168
+ const reasons = [...new Set(facts.blindGates.map((one) => one.reason))];
2169
+ const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
2170
+ findings.push({
2171
+ level: everyBlind && facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
2172
+ text: `with --affected, a change of any file runs ${gates.length} of ${facts.gates} gate(s) again (${listed(gates)}): what they depend on cannot be told (${reasons[0]}${more})`,
2173
+ fix: "keep what reads files by a path no import names out of what those gates and their specs import"
2174
+ });
2061
2175
  }
2062
2176
  if (facts.scope !== null && "every" in facts.scope) {
2063
2177
  findings.push({
@@ -2066,6 +2180,21 @@ function diagnose(facts) {
2066
2180
  fix: facts.scope.every.startsWith(NOT_THE_HELPER) ? "make the runner config with createJestGatesConfig or createVitestGatesConfig" : null
2067
2181
  });
2068
2182
  }
2183
+ if (facts.sandboxes !== null) {
2184
+ const { tempDir, count, kept, running } = facts.sandboxes;
2185
+ findings.push(
2186
+ running === null ? {
2187
+ level: "note",
2188
+ text: `${count} sandbox(es) an interrupted Stryker run left in ${tempDir}: a tool that walks the subproject (a test runner, a script) finds a copy of every file in them`,
2189
+ fix: kept === null ? "the next test-gates mutation removes them" : `remove them by hand; the next test-gates mutation keeps them, because ${kept}`
2190
+ } : {
2191
+ // The sandboxes may be those of that run: removing them would break it.
2192
+ level: "note",
2193
+ text: `another test-gates mutation is running in this subproject (pid ${running}): the ${count} sandbox(es) in ${tempDir} are in use`,
2194
+ fix: "leave them; the first test-gates mutation after that run ends removes what is left"
2195
+ }
2196
+ );
2197
+ }
2069
2198
  if (facts.hooks.length === 0) {
2070
2199
  findings.push({
2071
2200
  level: "note",
@@ -2073,7 +2202,8 @@ function diagnose(facts) {
2073
2202
  fix: null
2074
2203
  });
2075
2204
  }
2076
- for (const hook of facts.hooks.filter((one) => !one.affected && facts.blind === null)) {
2205
+ const toldApart = facts.blind === null && !everyBlind;
2206
+ for (const hook of facts.hooks.filter((one) => !one.affected && toldApart)) {
2077
2207
  findings.push({
2078
2208
  level: "problem",
2079
2209
  text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
@@ -2099,39 +2229,322 @@ function formatFindings(findings) {
2099
2229
  var RUNS_MUTATION = /(?<![\w-])test-gates(?:\.js)?\s+mutation(?![\w-])|(?<![\w:-])test:gates:mutation(?![\w:-])/;
2100
2230
  var TARGET = /(?:--dir|--prefix|--workspace|-w)(?:=|\s+)(['"]?)([^\s'";&|)]+)\1/;
2101
2231
  var CD = /\bcd\s+(['"]?)([^\s'";&|)]+)\1/;
2232
+ var NONE = [];
2233
+ var COMMAND = String.raw`(?<=(?:^|[\n;&|(){]|\b(?:then|else|do))\s*)`;
2234
+ var TOKEN = new RegExp(
2235
+ String.raw`\n|;;&?|;&|${COMMAND}(?:case\s+(\S+)\s+in|for\s+(\w+)(?:\s+in\s+([^;\n]*))?|(esac|do|done))(?![\w-])`,
2236
+ "g"
2237
+ );
2238
+ var PATTERN = /\s*(?:esac(?![\w-])|\(?([^()\n;]*)\))/y;
2239
+ var VARIABLE = /^("?)\$(?:(\w+)|\{(\w+)\})\1$/;
2240
+ var REFERENCE = /\$(?:(\w+)|\{(\w+)\})/g;
2241
+ function namesOf(words) {
2242
+ const names = words.map((word) => word.trim().replace(/^(["'])(.*)\1$/, "$2"));
2243
+ return names.some((name) => name === "" || /[$*?[{}~\\`"']/.test(name)) ? null : names;
2244
+ }
2245
+ function partsOf(text) {
2246
+ const parts = [];
2247
+ let scopes = NONE;
2248
+ let start = 0;
2249
+ let loop = null;
2250
+ const enter = (scope) => {
2251
+ scopes = [...scopes, scope];
2252
+ };
2253
+ const leave = (kind) => {
2254
+ const left = scopes.at(-1)?.kind === kind;
2255
+ scopes = scopes.slice(0, -1);
2256
+ return left;
2257
+ };
2258
+ TOKEN.lastIndex = 0;
2259
+ for (; ; ) {
2260
+ const token = TOKEN.exec(text);
2261
+ parts.push({ text: text.slice(start, token?.index), scopes });
2262
+ if (token === null) {
2263
+ return scopes.length === 0 ? parts : null;
2264
+ }
2265
+ const [word, subject, variable, list, keyword] = token;
2266
+ let awaiting = false;
2267
+ if (subject !== void 0) {
2268
+ const named = VARIABLE.exec(subject);
2269
+ enter({
2270
+ kind: "case",
2271
+ id: /* @__PURE__ */ Symbol(),
2272
+ variable: named?.[2] ?? named?.[3] ?? null,
2273
+ names: null
2274
+ });
2275
+ awaiting = true;
2276
+ } else if (variable !== void 0) {
2277
+ loop = {
2278
+ kind: "loop",
2279
+ id: /* @__PURE__ */ Symbol(),
2280
+ variable,
2281
+ names: list === void 0 ? null : namesOf(list.trim().split(/\s+/))
2282
+ };
2283
+ } else if (keyword === "do") {
2284
+ enter(loop ?? { kind: "loop", id: /* @__PURE__ */ Symbol(), variable: null, names: null });
2285
+ loop = null;
2286
+ } else if (keyword === "done" || keyword === "esac") {
2287
+ if (!leave(keyword === "done" ? "loop" : "case")) {
2288
+ return null;
2289
+ }
2290
+ } else if (word === ";&") {
2291
+ return null;
2292
+ } else if (word !== "\n") {
2293
+ if (scopes.at(-1)?.kind !== "case") {
2294
+ return null;
2295
+ }
2296
+ awaiting = true;
2297
+ }
2298
+ start = TOKEN.lastIndex;
2299
+ if (awaiting) {
2300
+ PATTERN.lastIndex = start;
2301
+ const pattern = PATTERN.exec(text);
2302
+ if (pattern === null) {
2303
+ return null;
2304
+ }
2305
+ start = PATTERN.lastIndex;
2306
+ TOKEN.lastIndex = start;
2307
+ const top = scopes.at(-1);
2308
+ scopes = scopes.slice(0, -1);
2309
+ if (pattern[1] !== void 0) {
2310
+ enter({ ...top, names: namesOf(pattern[1].split("|")) });
2311
+ }
2312
+ }
2313
+ }
2314
+ }
2315
+ function sibling(of, scopes) {
2316
+ const apart = of.findIndex((scope, at) => scope !== scopes[at]);
2317
+ return apart !== -1 && of[apart].id === scopes[apart]?.id;
2318
+ }
2319
+ function expansions(target, scopes) {
2320
+ return [...target.matchAll(REFERENCE)].reduce(
2321
+ (words, [reference, plain2, braced]) => {
2322
+ const names = [...scopes].reverse().find((scope) => scope.variable === (plain2 ?? braced))?.names ?? [reference];
2323
+ return words.flatMap((word) => names.map((name) => word.replace(reference, name)));
2324
+ },
2325
+ [target]
2326
+ );
2327
+ }
2102
2328
  function hookUses(input) {
2103
2329
  const lines = input.text.split("\n").map((line) => line.replace(/(?:^|\s)#.*/, ""));
2330
+ const parts = partsOf(lines.join("\n")) ?? lines.map((text) => ({ text, scopes: NONE }));
2104
2331
  const uses = [];
2105
- lines.forEach((line, index) => {
2106
- if (!RUNS_MUTATION.test(line)) {
2332
+ const cds = parts.map((part) => ({ cd: CD.exec(part.text)?.[2], scopes: part.scopes }));
2333
+ parts.forEach((part, index) => {
2334
+ if (!RUNS_MUTATION.test(part.text)) {
2107
2335
  return;
2108
2336
  }
2109
- const named = TARGET.exec(line) ?? CD.exec(line);
2110
- const target = named?.[2] ?? lines.slice(0, index).reverse().map((before) => CD.exec(before)?.[2]).find((found) => found !== void 0);
2111
- const normalized = target?.split("/").filter((part) => part !== "." && part !== "").join("/");
2112
- const ours = normalized === void 0 || normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
2337
+ const named = TARGET.exec(part.text) ?? CD.exec(part.text);
2338
+ const before = cds.slice(0, index).reverse().find((one) => one.cd !== void 0 && !sibling(one.scopes, part.scopes));
2339
+ const target = named?.[2] ?? before?.cd;
2340
+ const scopes = named !== null || before?.scopes === part.scopes ? part.scopes : NONE;
2341
+ const ours = target === void 0 || expansions(target, scopes).some((word) => {
2342
+ const normalized = word.split("/").filter((one) => one !== "." && one !== "").join("/");
2343
+ return normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
2344
+ });
2113
2345
  if (ours) {
2114
2346
  uses.push({
2115
2347
  file: input.file,
2116
- affected: /--affected\b/.test(line) || /test:gates:mutation\b/.test(line) && input.script.includes("--affected")
2348
+ affected: /--affected\b/.test(part.text) || /test:gates:mutation\b/.test(part.text) && input.script.includes("--affected")
2117
2349
  });
2118
2350
  }
2119
2351
  });
2120
2352
  return uses;
2121
2353
  }
2122
2354
 
2123
- // src/snapshot.ts
2355
+ // src/sandboxes.ts
2124
2356
  import { spawnSync } from "child_process";
2357
+ import fs4 from "fs";
2358
+ import path4 from "path";
2359
+
2360
+ // src/core/sandboxes.ts
2361
+ var SANDBOX = /^sandbox-[A-Za-z0-9]{6}$/;
2362
+ var MARKER = /^test-gates-run-([1-9][0-9]*)$/;
2363
+ function markerName(pid) {
2364
+ return `test-gates-run-${pid}`;
2365
+ }
2366
+ function markerText(pid, started) {
2367
+ return `${pid}
2368
+ ${started ?? ""}
2369
+ `;
2370
+ }
2371
+ function markerAlive(input) {
2372
+ const recorded = input.text?.split("\n")[1]?.trim() || null;
2373
+ return input.there && (recorded === null || input.started === null || sameStart(recorded, input.started));
2374
+ }
2375
+ var START_SLACK_SECONDS = 5;
2376
+ function sameStart(a, b) {
2377
+ const difference = Math.abs(Date.parse(a) - Date.parse(b));
2378
+ return Number.isNaN(difference) ? a === b : difference <= START_SLACK_SECONDS * 1e3;
2379
+ }
2380
+ function insideSubproject(tempDir) {
2381
+ const parts = tempDir.split("/");
2382
+ return !/^[A-Za-z]:/.test(tempDir) && parts.every((part) => part !== "" && part !== "." && part !== "..");
2383
+ }
2384
+ function planSandboxes(facts) {
2385
+ const sandboxes = facts.entries.filter((entry) => entry.directory && SANDBOX.test(entry.name)).map((entry) => entry.name).sort();
2386
+ if (facts.tempDir === null) {
2387
+ return {
2388
+ kept: sandboxes,
2389
+ why: "the Stryker config could not be loaded to see where they are",
2390
+ running: null
2391
+ };
2392
+ }
2393
+ if (!insideSubproject(facts.tempDir)) {
2394
+ return {
2395
+ kept: sandboxes,
2396
+ why: `tempDirName (${facts.tempDir}) is not a directory inside the subproject`,
2397
+ running: null
2398
+ };
2399
+ }
2400
+ if (facts.keeps) {
2401
+ return { kept: sandboxes, why: "the Stryker config sets cleanTempDir: false", running: null };
2402
+ }
2403
+ const markers = [];
2404
+ for (const entry of facts.entries) {
2405
+ const pid = MARKER.exec(entry.name)?.[1];
2406
+ if (pid === void 0 || entry.directory) {
2407
+ continue;
2408
+ }
2409
+ if (facts.alive(Number(pid), entry.name)) {
2410
+ return {
2411
+ kept: sandboxes,
2412
+ why: `another test-gates mutation is running in this subproject (pid ${pid})`,
2413
+ running: Number(pid)
2414
+ };
2415
+ }
2416
+ markers.push(entry.name);
2417
+ }
2418
+ return { remove: sandboxes, markers };
2419
+ }
2420
+
2421
+ // src/sandboxes.ts
2422
+ function there(pid) {
2423
+ try {
2424
+ process.kill(pid, 0);
2425
+ return true;
2426
+ } catch (error) {
2427
+ return error.code === "EPERM";
2428
+ }
2429
+ }
2430
+ function startedAt(pid) {
2431
+ const result = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
2432
+ encoding: "utf8",
2433
+ env: { ...process.env, LC_ALL: "C" },
2434
+ timeout: 5e3
2435
+ });
2436
+ const started = result.status === 0 ? result.stdout.trim() : "";
2437
+ return started === "" ? null : started;
2438
+ }
2439
+ function alive(dir) {
2440
+ return (pid, marker) => {
2441
+ if (!there(pid)) {
2442
+ return false;
2443
+ }
2444
+ let text = null;
2445
+ try {
2446
+ text = fs4.readFileSync(path4.join(dir, marker), "utf8");
2447
+ } catch {
2448
+ }
2449
+ return markerAlive({ there: true, text, started: startedAt(pid) });
2450
+ };
2451
+ }
2452
+ function relativeTempDir(subproject, dir) {
2453
+ let real = dir;
2454
+ try {
2455
+ real = fs4.realpathSync(dir);
2456
+ } catch {
2457
+ }
2458
+ return path4.relative(fs4.realpathSync(subproject), real).split(path4.sep).join("/");
2459
+ }
2460
+ function sandboxPlan(subproject, setting) {
2461
+ const tempDir = setting.dir === null ? null : relativeTempDir(subproject, setting.dir);
2462
+ let entries = [];
2463
+ if (setting.dir !== null) {
2464
+ try {
2465
+ entries = fs4.readdirSync(setting.dir, { withFileTypes: true });
2466
+ } catch {
2467
+ }
2468
+ }
2469
+ const plan = planSandboxes({
2470
+ tempDir,
2471
+ entries: entries.map((entry) => ({ name: entry.name, directory: entry.isDirectory() })),
2472
+ keeps: setting.keeps,
2473
+ alive: alive(setting.dir ?? subproject)
2474
+ });
2475
+ return { plan, shown: tempDir ?? ".stryker-tmp" };
2476
+ }
2477
+ function removeStaleSandboxes(subproject, setting) {
2478
+ const { plan, shown } = sandboxPlan(subproject, setting);
2479
+ if ("kept" in plan) {
2480
+ if (plan.kept.length === 0) {
2481
+ return [];
2482
+ }
2483
+ return plan.running === null ? [
2484
+ `test-gates mutation: ${plan.kept.length} sandbox(es) left in ${shown} by an earlier Stryker run are kept: ${plan.why}`
2485
+ ] : [
2486
+ `test-gates mutation: ${plan.why}, so the ${plan.kept.length} sandbox(es) in ${shown} are kept: they are in use`
2487
+ ];
2488
+ }
2489
+ const dir = setting.dir;
2490
+ for (const marker of plan.markers) {
2491
+ try {
2492
+ fs4.rmSync(path4.join(dir, marker), { force: true });
2493
+ } catch {
2494
+ }
2495
+ }
2496
+ const failed = [];
2497
+ for (const sandbox of plan.remove) {
2498
+ try {
2499
+ fs4.rmSync(path4.join(dir, sandbox), { recursive: true, force: true });
2500
+ } catch (error) {
2501
+ failed.push(`${sandbox} (${error.message})`);
2502
+ }
2503
+ }
2504
+ const removed = plan.remove.length - failed.length;
2505
+ return [
2506
+ ...removed > 0 ? [
2507
+ `test-gates mutation: removed ${removed} sandbox(es) an earlier Stryker run left in ${shown}`
2508
+ ] : [],
2509
+ ...failed.length > 0 ? [`test-gates mutation: could not remove from ${shown}: ${failed.join(", ")}`] : []
2510
+ ];
2511
+ }
2512
+ function markRun(subproject, setting) {
2513
+ const dir = setting.dir;
2514
+ if (dir === null || !insideSubproject(relativeTempDir(subproject, dir))) {
2515
+ return () => void 0;
2516
+ }
2517
+ const marker = path4.join(dir, markerName(process.pid));
2518
+ try {
2519
+ fs4.mkdirSync(dir, { recursive: true });
2520
+ if (!insideSubproject(relativeTempDir(subproject, dir))) {
2521
+ return () => void 0;
2522
+ }
2523
+ fs4.writeFileSync(marker, markerText(process.pid, startedAt(process.pid)));
2524
+ } catch {
2525
+ return () => void 0;
2526
+ }
2527
+ return () => {
2528
+ try {
2529
+ fs4.rmSync(marker, { force: true });
2530
+ fs4.rmdirSync(dir);
2531
+ } catch {
2532
+ }
2533
+ };
2534
+ }
2535
+
2536
+ // src/snapshot.ts
2537
+ import { spawnSync as spawnSync2 } from "child_process";
2125
2538
  import { createHash } from "crypto";
2126
- import fs5 from "fs";
2539
+ import fs6 from "fs";
2127
2540
  import { createRequire } from "module";
2128
2541
  import os from "os";
2129
- import path5 from "path";
2542
+ import path6 from "path";
2130
2543
 
2131
2544
  // src/commands/loaded-files.ts
2132
- import fs4 from "fs";
2545
+ import fs5 from "fs";
2133
2546
  import { builtinModules } from "module";
2134
- import path4 from "path";
2547
+ import path5 from "path";
2135
2548
  var SCRIPT = /\.[cm]?[jt]sx?$/;
2136
2549
  var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
2137
2550
  var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
@@ -2139,7 +2552,7 @@ var QUOTED = /(['"`])([^'"`\n]+)\1/g;
2139
2552
  var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|createMockFromModule|genMockFromModule)\s*(?:<[^()\n]*>)?\s*\(\s*(['"`])([^'"`\n]+)\1/g;
2140
2553
  function isFile(file) {
2141
2554
  try {
2142
- return fs4.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
2555
+ return fs5.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
2143
2556
  } catch {
2144
2557
  return false;
2145
2558
  }
@@ -2172,9 +2585,9 @@ function filesOf(base, directory, declarations) {
2172
2585
  ...extensions.map((extension) => base + extension),
2173
2586
  // `./fee.js` written in a TypeScript file is `./fee.ts`
2174
2587
  ...extensions.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
2175
- ...directory ? extensions.map((extension) => path4.join(base, `index${extension}`)) : []
2588
+ ...directory ? extensions.map((extension) => path5.join(base, `index${extension}`)) : []
2176
2589
  ];
2177
- const data = [`${base}.json`, ...directory ? [path4.join(base, "index.json")] : []];
2590
+ const data = [`${base}.json`, ...directory ? [path5.join(base, "index.json")] : []];
2178
2591
  const scripts = [.../* @__PURE__ */ new Set([base, ...candidates(EXTENSIONS), ...data])].filter(isFile);
2179
2592
  return scripts.length > 0 || !declarations ? scripts : [...new Set(candidates(DECLARATIONS))].filter(isFile);
2180
2593
  }
@@ -2183,11 +2596,11 @@ function resolveFile(base, directory = true, declarations = true) {
2183
2596
  }
2184
2597
  function installed(specifier, from) {
2185
2598
  const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
2186
- for (let current = path4.dirname(from); ; current = path4.dirname(current)) {
2187
- if (fs4.existsSync(path4.join(current, "node_modules", name))) {
2599
+ for (let current = path5.dirname(from); ; current = path5.dirname(current)) {
2600
+ if (fs5.existsSync(path5.join(current, "node_modules", name))) {
2188
2601
  return true;
2189
2602
  }
2190
- if (current === path4.dirname(current)) {
2603
+ if (current === path5.dirname(current)) {
2191
2604
  return false;
2192
2605
  }
2193
2606
  }
@@ -2197,18 +2610,18 @@ function loadedFiles(manifest, configFile) {
2197
2610
  }
2198
2611
  function loadedFilesNow(manifest, configFile) {
2199
2612
  const aliases = manifest.settings.importAliases;
2200
- const relative = (file) => path4.relative(manifest.dir, file).split(path4.sep).join("/");
2613
+ const relative = (file) => path5.relative(manifest.dir, file).split(path5.sep).join("/");
2201
2614
  const target = (specifier, from) => {
2202
2615
  if (specifier.startsWith(".")) {
2203
- return resolveFiles(path4.resolve(path4.dirname(from), specifier));
2616
+ return resolveFiles(path5.resolve(path5.dirname(from), specifier));
2204
2617
  }
2205
2618
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
2206
2619
  return alias === void 0 ? [] : resolveFiles(
2207
- path4.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
2620
+ path5.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
2208
2621
  );
2209
2622
  };
2210
2623
  const read2 = (file) => {
2211
- const source = fs4.readFileSync(file, "utf8");
2624
+ const source = fs5.readFileSync(file, "utf8");
2212
2625
  const imports = parseImports(source).filter((ref) => !ref.typeOnly);
2213
2626
  const specifiers = [
2214
2627
  .../* @__PURE__ */ new Set([
@@ -2220,7 +2633,7 @@ function loadedFilesNow(manifest, configFile) {
2220
2633
  return { source, specifiers, strings };
2221
2634
  };
2222
2635
  const named = /* @__PURE__ */ new Set();
2223
- const configs = configFile === null ? [] : [path4.resolve(manifest.dir, configFile)];
2636
+ const configs = configFile === null ? [] : [path5.resolve(manifest.dir, configFile)];
2224
2637
  const seenConfigs = new Set(configs);
2225
2638
  const entry = configs[0];
2226
2639
  for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
@@ -2228,8 +2641,8 @@ function loadedFilesNow(manifest, configFile) {
2228
2641
  const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
2229
2642
  for (const text of paths) {
2230
2643
  const stripped = text.replace(/^<rootDir>\//, "");
2231
- const here = resolveFiles(path4.resolve(path4.dirname(file), stripped), false);
2232
- const found = here.length > 0 ? here : resolveFiles(path4.resolve(manifest.dir, stripped), false);
2644
+ const here = resolveFiles(path5.resolve(path5.dirname(file), stripped), false);
2645
+ const found = here.length > 0 ? here : resolveFiles(path5.resolve(manifest.dir, stripped), false);
2233
2646
  for (const one of found.filter((candidate) => SCRIPT.test(candidate))) {
2234
2647
  named.add(one);
2235
2648
  }
@@ -2270,7 +2683,7 @@ function loadedFilesNow(manifest, configFile) {
2270
2683
  }
2271
2684
  }
2272
2685
  for (const text of strings.filter((one) => one.startsWith("."))) {
2273
- next.push(...resolveFiles(path4.resolve(path4.dirname(file), text)));
2686
+ next.push(...resolveFiles(path5.resolve(path5.dirname(file), text)));
2274
2687
  }
2275
2688
  const visited = {
2276
2689
  file: { path: relative(file), source, imports },
@@ -2300,7 +2713,7 @@ function loadedFilesNow(manifest, configFile) {
2300
2713
  const specs = [...new Set(manifest.gates.flatMap((gate) => gate.existingSpecs))];
2301
2714
  const roots = [
2302
2715
  closure(null, [...named]),
2303
- ...specs.map((spec) => closure(spec, [path4.join(manifest.dir, spec)]))
2716
+ ...specs.map((spec) => closure(spec, [path5.join(manifest.dir, spec)]))
2304
2717
  ];
2305
2718
  return { files: [...known.values()].map((visited) => visited.file), roots };
2306
2719
  }
@@ -2352,17 +2765,17 @@ function hashOf(content) {
2352
2765
  }
2353
2766
  function hashFile(file) {
2354
2767
  try {
2355
- return hashOf(fs5.readFileSync(file));
2768
+ return hashOf(fs6.readFileSync(file));
2356
2769
  } catch {
2357
2770
  return null;
2358
2771
  }
2359
2772
  }
2360
2773
  function isInside(parent, child) {
2361
- const relative = path5.relative(parent, child);
2362
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
2774
+ const relative = path6.relative(parent, child);
2775
+ return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
2363
2776
  }
2364
2777
  function toPosix(file) {
2365
- return file.split(path5.sep).join("/");
2778
+ return file.split(path6.sep).join("/");
2366
2779
  }
2367
2780
  function gitEnvironment(env) {
2368
2781
  if (env.GIT_DIR === void 0 || env.GIT_WORK_TREE !== void 0) {
@@ -2373,7 +2786,7 @@ function gitEnvironment(env) {
2373
2786
  return rest;
2374
2787
  }
2375
2788
  function git(cwd, args) {
2376
- const result = spawnSync("git", args, {
2789
+ const result = spawnSync2("git", args, {
2377
2790
  cwd,
2378
2791
  env: gitEnvironment(process.env),
2379
2792
  encoding: "utf8",
@@ -2386,7 +2799,7 @@ function globLiteral(relative) {
2386
2799
  }
2387
2800
  function gitLister(root) {
2388
2801
  return (directory, recursive) => {
2389
- const relative = globLiteral(toPosix(path5.relative(root, directory)));
2802
+ const relative = globLiteral(toPosix(path6.relative(root, directory)));
2390
2803
  const pattern = recursive ? `${relative === "" ? "" : `${relative}/`}**` : `${relative === "" ? "" : `${relative}/`}*`;
2391
2804
  const listed2 = git(root, [
2392
2805
  "ls-files",
@@ -2405,8 +2818,8 @@ function gitLister(root) {
2405
2818
  }
2406
2819
  function walkLister(root) {
2407
2820
  const walk = (directory, recursive, found) => {
2408
- for (const entry of fs5.readdirSync(directory, { withFileTypes: true })) {
2409
- const file = path5.join(directory, entry.name);
2821
+ for (const entry of fs6.readdirSync(directory, { withFileTypes: true })) {
2822
+ const file = path6.join(directory, entry.name);
2410
2823
  if (NEVER.includes(entry.name)) {
2411
2824
  continue;
2412
2825
  }
@@ -2415,7 +2828,7 @@ function walkLister(root) {
2415
2828
  walk(file, true, found);
2416
2829
  }
2417
2830
  } else {
2418
- found.push(toPosix(path5.relative(root, file)));
2831
+ found.push(toPosix(path6.relative(root, file)));
2419
2832
  }
2420
2833
  }
2421
2834
  return found;
@@ -2425,16 +2838,16 @@ function walkLister(root) {
2425
2838
  function upTo(from, to) {
2426
2839
  const directories = [from];
2427
2840
  while (directories[directories.length - 1] !== to) {
2428
- directories.push(path5.dirname(directories[directories.length - 1]));
2841
+ directories.push(path6.dirname(directories[directories.length - 1]));
2429
2842
  }
2430
2843
  return directories;
2431
2844
  }
2432
2845
  function declaresWorkspaces(directory) {
2433
- if (fs5.existsSync(path5.join(directory, "pnpm-workspace.yaml"))) {
2846
+ if (fs6.existsSync(path6.join(directory, "pnpm-workspace.yaml"))) {
2434
2847
  return true;
2435
2848
  }
2436
2849
  try {
2437
- const manifest = JSON.parse(fs5.readFileSync(path5.join(directory, "package.json"), "utf8"));
2850
+ const manifest = JSON.parse(fs6.readFileSync(path6.join(directory, "package.json"), "utf8"));
2438
2851
  return manifest.workspaces !== void 0;
2439
2852
  } catch {
2440
2853
  return false;
@@ -2442,7 +2855,7 @@ function declaresWorkspaces(directory) {
2442
2855
  }
2443
2856
  function walkRoot(dir) {
2444
2857
  const candidates = withinBounds(dir);
2445
- const repository = candidates.find((directory) => fs5.existsSync(path5.join(directory, ".git")));
2858
+ const repository = candidates.find((directory) => fs6.existsSync(path6.join(directory, ".git")));
2446
2859
  if (repository !== void 0) {
2447
2860
  return repository;
2448
2861
  }
@@ -2451,11 +2864,11 @@ function walkRoot(dir) {
2451
2864
  function withinBounds(dir) {
2452
2865
  let home = null;
2453
2866
  try {
2454
- home = fs5.realpathSync(os.homedir());
2867
+ home = fs6.realpathSync(os.homedir());
2455
2868
  } catch {
2456
2869
  }
2457
- return upTo(dir, path5.parse(dir).root).filter(
2458
- (directory) => directory !== path5.parse(dir).root && (home === null || !isInside(directory, home))
2870
+ return upTo(dir, path6.parse(dir).root).filter(
2871
+ (directory) => directory !== path6.parse(dir).root && (home === null || !isInside(directory, home))
2459
2872
  );
2460
2873
  }
2461
2874
  function linkedPackages(nodeModules, root) {
@@ -2463,20 +2876,20 @@ function linkedPackages(nodeModules, root) {
2463
2876
  const visit = (directory, depth) => {
2464
2877
  let entries;
2465
2878
  try {
2466
- entries = fs5.readdirSync(directory, { withFileTypes: true });
2879
+ entries = fs6.readdirSync(directory, { withFileTypes: true });
2467
2880
  } catch {
2468
2881
  return;
2469
2882
  }
2470
2883
  for (const entry of entries) {
2471
- const file = path5.join(directory, entry.name);
2884
+ const file = path6.join(directory, entry.name);
2472
2885
  if (entry.isSymbolicLink()) {
2473
2886
  let target;
2474
2887
  try {
2475
- target = fs5.realpathSync(file);
2888
+ target = fs6.realpathSync(file);
2476
2889
  } catch {
2477
2890
  continue;
2478
2891
  }
2479
- if (target.split(path5.sep).includes("node_modules") || !fs5.statSync(target).isDirectory()) {
2892
+ if (target.split(path6.sep).includes("node_modules") || !fs6.statSync(target).isDirectory()) {
2480
2893
  continue;
2481
2894
  }
2482
2895
  if (!isInside(root, target)) {
@@ -2498,11 +2911,11 @@ function installedVersion(start, name) {
2498
2911
  for (; ; ) {
2499
2912
  try {
2500
2913
  const manifest = JSON.parse(
2501
- fs5.readFileSync(path5.join(current, "node_modules", name, "package.json"), "utf8")
2914
+ fs6.readFileSync(path6.join(current, "node_modules", name, "package.json"), "utf8")
2502
2915
  );
2503
2916
  return typeof manifest.version === "string" ? manifest.version : "unknown";
2504
2917
  } catch {
2505
- const parent = path5.dirname(current);
2918
+ const parent = path6.dirname(current);
2506
2919
  if (parent === current) {
2507
2920
  return null;
2508
2921
  }
@@ -2511,7 +2924,7 @@ function installedVersion(start, name) {
2511
2924
  }
2512
2925
  }
2513
2926
  function nodeOnPath(cwd) {
2514
- const result = spawnSync("node", ["--version"], {
2927
+ const result = spawnSync2("node", ["--version"], {
2515
2928
  cwd,
2516
2929
  encoding: "utf8",
2517
2930
  shell: process.platform === "win32",
@@ -2528,12 +2941,12 @@ function snapshotOf(input) {
2528
2941
  return resolving(() => snapshotNow(input));
2529
2942
  }
2530
2943
  function snapshotNow(input) {
2531
- const dir = fs5.realpathSync(input.dir);
2944
+ const dir = fs6.realpathSync(input.dir);
2532
2945
  const top = git(dir, ["rev-parse", "--show-toplevel"]);
2533
- let root = top === null ? dir : fs5.realpathSync(top.trim());
2946
+ let root = top === null ? dir : fs6.realpathSync(top.trim());
2534
2947
  let lister = gitLister(root);
2535
2948
  let mode = "git";
2536
- const manifest = toPosix(path5.relative(root, path5.join(dir, "test-gates.json")));
2949
+ const manifest = toPosix(path6.relative(root, path6.join(dir, "test-gates.json")));
2537
2950
  if (top === null || !lister(dir, false).includes(manifest)) {
2538
2951
  mode = "walk";
2539
2952
  root = walkRoot(dir);
@@ -2541,17 +2954,17 @@ function snapshotNow(input) {
2541
2954
  }
2542
2955
  const listRoot = root;
2543
2956
  const list = lister;
2544
- const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path5.join(listRoot, file)));
2957
+ const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path6.join(listRoot, file)));
2545
2958
  const walk = walkLister(listRoot);
2546
2959
  const written = input.written.map((file) => {
2547
- const target = path5.resolve(dir, file);
2960
+ const target = path6.resolve(dir, file);
2548
2961
  try {
2549
- return fs5.realpathSync(target);
2962
+ return fs6.realpathSync(target);
2550
2963
  } catch {
2551
2964
  return target;
2552
2965
  }
2553
2966
  });
2554
- const swallowing = written.find((target) => isInside(target, path5.join(dir, "test-gates.json")));
2967
+ const swallowing = written.find((target) => isInside(target, path6.join(dir, "test-gates.json")));
2555
2968
  if (swallowing !== void 0) {
2556
2969
  throw new SnapshotError(`the run writes to ${swallowing}, which holds test-gates.json`);
2557
2970
  }
@@ -2568,12 +2981,12 @@ function snapshotNow(input) {
2568
2981
  if (!isInside(listRoot, directory)) {
2569
2982
  const written2 = reference === void 0 ? "" : ` with ${JSON.stringify(reference)}`;
2570
2983
  throw new SnapshotError(
2571
- `${toPosix(path5.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
2984
+ `${toPosix(path6.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
2572
2985
  );
2573
2986
  }
2574
2987
  if (directory === listRoot && listRoot !== dir && widened.length < WIDENED_SHOWN) {
2575
2988
  const written2 = reference === void 0 ? "" : ` ${JSON.stringify(reference)}`;
2576
- widened.push(`${toPosix(path5.relative(listRoot, from))}${written2}`);
2989
+ widened.push(`${toPosix(path6.relative(listRoot, from))}${written2}`);
2577
2990
  }
2578
2991
  if (![...queued].some((region) => isInside(region, directory))) {
2579
2992
  queued.add(directory);
@@ -2582,10 +2995,10 @@ function snapshotNow(input) {
2582
2995
  };
2583
2996
  const follow = (fixed, open, anchored, file, reference) => {
2584
2997
  const from = (start) => {
2585
- const target = path5.resolve(start, fixed);
2998
+ const target = path6.resolve(start, fixed);
2586
2999
  let isDirectory2 = false;
2587
3000
  try {
2588
- isDirectory2 = fs5.statSync(target).isDirectory();
3001
+ isDirectory2 = fs6.statSync(target).isDirectory();
2589
3002
  } catch {
2590
3003
  }
2591
3004
  return {
@@ -2594,9 +3007,9 @@ function snapshotNow(input) {
2594
3007
  found: isDirectory2 || resolveFile(target, true, false) !== null
2595
3008
  };
2596
3009
  };
2597
- const fromFile = from(path5.dirname(file));
3010
+ const fromFile = from(path6.dirname(file));
2598
3011
  const targets = [fromFile];
2599
- if (!anchored && path5.dirname(file) !== dir) {
3012
+ if (!anchored && path6.dirname(file) !== dir) {
2600
3013
  const fromRun = from(dir);
2601
3014
  if (fromRun.found && !fromFile.found) {
2602
3015
  targets[0] = fromRun;
@@ -2615,24 +3028,24 @@ function snapshotNow(input) {
2615
3028
  continue;
2616
3029
  }
2617
3030
  if (!isDirectory2 && !isInside(listRoot, target) && resolveFile(target, true, false) === null) {
2618
- files[toPosix(path5.relative(listRoot, target))] = "missing";
3031
+ files[toPosix(path6.relative(listRoot, target))] = "missing";
2619
3032
  continue;
2620
3033
  }
2621
- const directory = isDirectory2 ? target : path5.dirname(target);
2622
- if (fs5.existsSync(directory)) {
3034
+ const directory = isDirectory2 ? target : path6.dirname(target);
3035
+ if (fs6.existsSync(directory)) {
2623
3036
  enqueue(directory, file, reference, anchored && !open);
2624
3037
  }
2625
3038
  }
2626
3039
  };
2627
3040
  const reached = /* @__PURE__ */ new Set();
2628
3041
  const reach = (file) => {
2629
- if (reached.has(file) || DOCUMENTS.includes(path5.extname(file).toLowerCase())) {
3042
+ if (reached.has(file) || DOCUMENTS.includes(path6.extname(file).toLowerCase())) {
2630
3043
  return;
2631
3044
  }
2632
3045
  reached.add(file);
2633
3046
  let text;
2634
3047
  try {
2635
- const content = fs5.readFileSync(file);
3048
+ const content = fs6.readFileSync(file);
2636
3049
  if (content.length > SCAN_LIMIT || content.subarray(0, 8e3).includes(0)) {
2637
3050
  return;
2638
3051
  }
@@ -2664,7 +3077,7 @@ function snapshotNow(input) {
2664
3077
  const end = (match.index ?? 0) + match[0].length;
2665
3078
  const open = match[3] === void 0 || /^\s*[,+.}[]/.test(text.slice(end, end + 200));
2666
3079
  const reference = match[0].slice(0, match[0].length - (match[3]?.length ?? 0));
2667
- follow(path5.posix.join(...segments), open, true, file, reference);
3080
+ follow(path6.posix.join(...segments), open, true, file, reference);
2668
3081
  }
2669
3082
  for (const match of text.matchAll(UPWARD)) {
2670
3083
  const at = match.index ?? 0;
@@ -2684,35 +3097,35 @@ function snapshotNow(input) {
2684
3097
  }
2685
3098
  };
2686
3099
  const add = (relative) => {
2687
- const file = path5.join(listRoot, relative);
3100
+ const file = path6.join(listRoot, relative);
2688
3101
  if (relative in files || written.some((target) => isInside(target, file))) {
2689
3102
  return;
2690
3103
  }
2691
3104
  let stat;
2692
3105
  try {
2693
- stat = fs5.lstatSync(file);
3106
+ stat = fs6.lstatSync(file);
2694
3107
  } catch {
2695
3108
  files[relative] = "missing";
2696
3109
  return;
2697
3110
  }
2698
3111
  if (stat.isSymbolicLink()) {
2699
- files[relative] = `link ${fs5.readlinkSync(file)}`;
3112
+ files[relative] = `link ${fs6.readlinkSync(file)}`;
2700
3113
  let target;
2701
3114
  try {
2702
- target = fs5.realpathSync(file);
3115
+ target = fs6.realpathSync(file);
2703
3116
  } catch {
2704
3117
  return;
2705
3118
  }
2706
- enqueue(fs5.statSync(target).isDirectory() ? target : path5.dirname(target), file);
3119
+ enqueue(fs6.statSync(target).isDirectory() ? target : path6.dirname(target), file);
2707
3120
  } else if (stat.isDirectory()) {
2708
3121
  files[relative] = "directory";
2709
3122
  walk(file, true).forEach(add);
2710
3123
  } else if (!stat.isFile()) {
2711
3124
  files[relative] = "special";
2712
3125
  } else {
2713
- const content = fs5.readFileSync(file);
3126
+ const content = fs6.readFileSync(file);
2714
3127
  files[relative] = hashOf(content);
2715
- const document = DOCUMENTS.includes(path5.extname(file).toLowerCase());
3128
+ const document = DOCUMENTS.includes(path6.extname(file).toLowerCase());
2716
3129
  const loaded = followed === null || !isInside(dir, file) || followed.has(file);
2717
3130
  if (loaded && !document && content.length <= SCAN_LIMIT && !content.subarray(0, 8e3).includes(0)) {
2718
3131
  scan(content.toString("utf8").replace(/\\[\\/]/g, "/"), file);
@@ -2723,32 +3136,32 @@ function snapshotNow(input) {
2723
3136
  const besideAncestors = /* @__PURE__ */ new Set();
2724
3137
  const ancestorFiles = ancestors.flatMap((directory) => list(directory, false));
2725
3138
  for (const relative of ancestorFiles) {
2726
- besideAncestors.add(path5.join(listRoot, relative));
3139
+ besideAncestors.add(path6.join(listRoot, relative));
2727
3140
  }
2728
3141
  ancestorFiles.forEach(add);
2729
3142
  for (const directory of [dir, ...ancestors]) {
2730
- const patches = path5.join(directory, "patches");
2731
- if (fs5.existsSync(patches) && fs5.statSync(patches).isDirectory()) {
3143
+ const patches = path6.join(directory, "patches");
3144
+ if (fs6.existsSync(patches) && fs6.statSync(patches).isDirectory()) {
2732
3145
  enqueue(patches, directory);
2733
3146
  }
2734
3147
  }
2735
3148
  for (const directory of [dir, ...ancestors]) {
2736
- for (const linked of linkedPackages(path5.join(directory, "node_modules"), listRoot)) {
3149
+ for (const linked of linkedPackages(path6.join(directory, "node_modules"), listRoot)) {
2737
3150
  if (!isInside(linked, dir)) {
2738
- enqueue(linked, path5.join(directory, "node_modules"));
3151
+ enqueue(linked, path6.join(directory, "node_modules"));
2739
3152
  }
2740
3153
  }
2741
3154
  }
2742
3155
  const above = withinBounds(listRoot).slice(1);
2743
3156
  for (const directory of [dir, ...ancestors, ...above]) {
2744
3157
  const candidates = [
2745
- ...INSTALL_MARKERS.map((marker) => path5.join(directory, "node_modules", marker)),
2746
- ...above.includes(directory) ? LOCK_FILES.map((name) => path5.join(directory, name)) : []
3158
+ ...INSTALL_MARKERS.map((marker) => path6.join(directory, "node_modules", marker)),
3159
+ ...above.includes(directory) ? LOCK_FILES.map((name) => path6.join(directory, name)) : []
2747
3160
  ];
2748
3161
  for (const file of candidates) {
2749
3162
  const hash = hashFile(file);
2750
3163
  if (hash !== null) {
2751
- files[toPosix(path5.relative(listRoot, file))] = hash;
3164
+ files[toPosix(path6.relative(listRoot, file))] = hash;
2752
3165
  }
2753
3166
  }
2754
3167
  }
@@ -2760,9 +3173,9 @@ function snapshotNow(input) {
2760
3173
  };
2761
3174
  drain();
2762
3175
  for (const file of followed ?? []) {
2763
- const parts = path5.relative(listRoot, file).split(path5.sep);
2764
- if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) && fs5.statSync(file, { throwIfNoEntry: false })?.isFile() === true) {
2765
- add(toPosix(path5.relative(listRoot, file)));
3176
+ const parts = path6.relative(listRoot, file).split(path6.sep);
3177
+ if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) && fs6.statSync(file, { throwIfNoEntry: false })?.isFile() === true) {
3178
+ add(toPosix(path6.relative(listRoot, file)));
2766
3179
  }
2767
3180
  }
2768
3181
  drain();
@@ -2782,14 +3195,18 @@ function snapshotNow(input) {
2782
3195
  }
2783
3196
 
2784
3197
  // src/affected-files.ts
2785
- import fs6 from "fs";
3198
+ import fs7 from "fs";
2786
3199
  import { builtinModules as builtinModules2 } from "module";
2787
- import path6 from "path";
3200
+ import path7 from "path";
2788
3201
  var AffectedError = class extends Error {
2789
3202
  };
2790
- var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readdirSync)\b|\bimport\.meta\.(?:glob|resolve)\b/;
3203
+ var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readFile|readdirSync|fromFile|fromURL)\b|\bimport\.meta\.(?:glob|resolve)\b/;
3204
+ var PATH_FROM_TOP = /^[^\s./?#*:][^\s?#*:]*\.(?:html?|md|json)$/i;
3205
+ var withoutQuery = (specifier) => specifier.replace(/\?[^/]*$/, "");
3206
+ var RUNTIME_LOADS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire)\b|\bimport\.meta\.(?:glob|resolve)\b/;
2791
3207
  var LOCATION = /\b(?:__dirname|__filename)\b|\bimport\.meta\.(?:url|dirname|filename)\b/;
2792
3208
  var LOCATION_READS = /\b(?:readFile|readdir|readlink|existsSync|stat|statSync|lstat|lstatSync|access|accessSync|realpath|realpathSync|open|openSync|opendir|opendirSync|createReadStream|watch|watchFile|glob|globSync|promises)\b|\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
3209
+ var LOCATION_LOADS = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
2793
3210
  var LOCATION_SAFE = /* @__PURE__ */ new Set([
2794
3211
  "path",
2795
3212
  "path/posix",
@@ -2810,30 +3227,31 @@ var FILE_MODULES = /* @__PURE__ */ new Set([
2810
3227
  "fast-glob",
2811
3228
  "globby"
2812
3229
  ]);
3230
+ var LOADING_MODULES = /* @__PURE__ */ new Set(["child_process", "module", "worker_threads"]);
2813
3231
  var bare = (specifier) => specifier.replace(/^node:/, "");
2814
3232
  function linkedPackage(specifier, from) {
2815
3233
  const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
2816
- for (let current = path6.dirname(from); ; current = path6.dirname(current)) {
2817
- const candidate = path6.join(current, "node_modules", name);
2818
- if (fs6.existsSync(candidate)) {
3234
+ for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
3235
+ const candidate = path7.join(current, "node_modules", name);
3236
+ if (fs7.existsSync(candidate)) {
2819
3237
  const target = realpath(candidate);
2820
- return target.split(path6.sep).includes("node_modules") ? null : target;
3238
+ return target.split(path7.sep).includes("node_modules") ? null : target;
2821
3239
  }
2822
- if (current === path6.dirname(current)) {
3240
+ if (current === path7.dirname(current)) {
2823
3241
  return null;
2824
3242
  }
2825
3243
  }
2826
3244
  }
2827
- var toPosix2 = (file) => file.split(path6.sep).join("/");
3245
+ var toPosix2 = (file) => file.split(path7.sep).join("/");
2828
3246
  function realpath(file) {
2829
3247
  try {
2830
- return fs6.realpathSync.native(file);
3248
+ return fs7.realpathSync.native(file);
2831
3249
  } catch {
2832
3250
  return file;
2833
3251
  }
2834
3252
  }
2835
3253
  function runnerConfigs(dir, stryker, options) {
2836
- const files = [path6.resolve(dir, stryker)];
3254
+ const files = [path7.resolve(dir, stryker)];
2837
3255
  const named = options.testRunner === "vitest" ? options.vitestConfig : options.jestConfig;
2838
3256
  if (options.testRunner !== "vitest" && options.testRunner !== "jest") {
2839
3257
  throw new AffectedError(
@@ -2841,20 +3259,20 @@ function runnerConfigs(dir, stryker, options) {
2841
3259
  );
2842
3260
  }
2843
3261
  if (typeof named === "string") {
2844
- files.push(path6.resolve(dir, named));
3262
+ files.push(path7.resolve(dir, named));
2845
3263
  } else {
2846
3264
  const names = options.testRunner === "vitest" ? ["vitest.config", "vite.config"] : ["jest.config"];
2847
3265
  for (const name of names) {
2848
3266
  for (const extension of [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]) {
2849
- const file = path6.join(dir, name + extension);
2850
- if (fs6.existsSync(file)) {
3267
+ const file = path7.join(dir, name + extension);
3268
+ if (fs7.existsSync(file)) {
2851
3269
  files.push(file);
2852
3270
  }
2853
3271
  }
2854
3272
  }
2855
3273
  }
2856
3274
  return {
2857
- files: files.filter((file) => fs6.existsSync(file)).map(realpath),
3275
+ files: files.filter((file) => fs7.existsSync(file)).map(realpath),
2858
3276
  runner: options.testRunner,
2859
3277
  checkers: Array.isArray(options.checkers) && options.checkers.length > 0
2860
3278
  };
@@ -2875,24 +3293,24 @@ function typesErased(configs, texts, compiler) {
2875
3293
  erased = /\bts-jest\b/.test(all) ? TS_JEST_STRIPS.test(all) && !TS_JEST_UNSURE.test(all) : STRIPPING_TRANSFORMS.test(all) && !/\bpreset\b/.test(all);
2876
3294
  }
2877
3295
  }
2878
- const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(path6.basename(file))) && compiler.every(({ text }) => text !== null && !KEEPS_IMPORTS.test(text));
3296
+ const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(path7.basename(file))) && compiler.every(({ text }) => text !== null && !KEEPS_IMPORTS.test(text));
2879
3297
  return { erased, inline };
2880
3298
  }
2881
3299
  function compilerConfigs(dir, root, keys) {
2882
3300
  const above = /* @__PURE__ */ new Set();
2883
- for (let current = path6.dirname(dir); ; current = path6.dirname(current)) {
2884
- const relative = path6.relative(root, current);
2885
- if (relative.startsWith("..") || path6.isAbsolute(relative)) {
3301
+ for (let current = path7.dirname(dir); ; current = path7.dirname(current)) {
3302
+ const relative = path7.relative(root, current);
3303
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) {
2886
3304
  break;
2887
3305
  }
2888
3306
  above.add(current);
2889
- if (current === path6.dirname(current)) {
3307
+ if (current === path7.dirname(current)) {
2890
3308
  break;
2891
3309
  }
2892
3310
  }
2893
- const compared = new Set(keys.map((key) => path6.join(root, key)));
3311
+ const compared = new Set(keys.map((key) => path7.join(root, key)));
2894
3312
  const starts = [...compared].filter(
2895
- (file) => (COMPILER_CONFIG.test(path6.basename(file)) || path6.basename(file) === "package.json") && (file.startsWith(`${dir}${path6.sep}`) || above.has(path6.dirname(file)))
3313
+ (file) => (COMPILER_CONFIG.test(path7.basename(file)) || path7.basename(file) === "package.json") && (file.startsWith(`${dir}${path7.sep}`) || above.has(path7.dirname(file)))
2896
3314
  );
2897
3315
  const found = [];
2898
3316
  const seen = /* @__PURE__ */ new Set();
@@ -2903,9 +3321,9 @@ function compilerConfigs(dir, root, keys) {
2903
3321
  }
2904
3322
  seen.add(file);
2905
3323
  let text = null;
2906
- if (compared.has(file) || file.split(path6.sep).includes("node_modules")) {
3324
+ if (compared.has(file) || file.split(path7.sep).includes("node_modules")) {
2907
3325
  try {
2908
- text = fs6.readFileSync(file, "utf8");
3326
+ text = fs7.readFileSync(file, "utf8");
2909
3327
  } catch {
2910
3328
  text = null;
2911
3329
  }
@@ -2921,24 +3339,24 @@ function extended(base, from) {
2921
3339
  const candidates = (start) => [
2922
3340
  start,
2923
3341
  `${start}.json`,
2924
- path6.join(start, "tsconfig.json")
3342
+ path7.join(start, "tsconfig.json")
2925
3343
  ];
2926
- if (base.startsWith(".") || path6.isAbsolute(base)) {
2927
- return candidates(path6.resolve(path6.dirname(from), base)).find(isFile2) ?? null;
3344
+ if (base.startsWith(".") || path7.isAbsolute(base)) {
3345
+ return candidates(path7.resolve(path7.dirname(from), base)).find(isFile2) ?? null;
2928
3346
  }
2929
- for (let current = path6.dirname(from); ; current = path6.dirname(current)) {
2930
- const file = candidates(path6.join(current, "node_modules", base)).find(isFile2);
3347
+ for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
3348
+ const file = candidates(path7.join(current, "node_modules", base)).find(isFile2);
2931
3349
  if (file !== void 0) {
2932
3350
  return file;
2933
3351
  }
2934
- if (current === path6.dirname(current)) {
3352
+ if (current === path7.dirname(current)) {
2935
3353
  return null;
2936
3354
  }
2937
3355
  }
2938
3356
  }
2939
3357
  function isFile2(file) {
2940
3358
  try {
2941
- return fs6.statSync(file).isFile();
3359
+ return fs7.statSync(file).isFile();
2942
3360
  } catch {
2943
3361
  return false;
2944
3362
  }
@@ -2949,29 +3367,33 @@ function fileEdges(file, context) {
2949
3367
  if (known !== void 0) {
2950
3368
  return known;
2951
3369
  }
2952
- const shown = (one) => toPosix2(path6.relative(dir, one));
3370
+ const shown = (one) => toPosix2(path7.relative(dir, one));
2953
3371
  const target = (specifier, from) => {
2954
3372
  if (specifier.startsWith(".")) {
2955
- return resolveFiles(path6.resolve(path6.dirname(from), specifier));
3373
+ return resolveFiles(path7.resolve(path7.dirname(from), specifier));
2956
3374
  }
2957
3375
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
2958
- return alias === void 0 ? [] : resolveFiles(path6.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
3376
+ return alias === void 0 ? [] : resolveFiles(path7.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
3377
+ };
3378
+ const found = { loads: [], unknown: null, loading: null, computed: false, unnamed: [] };
3379
+ const loading = (reason) => {
3380
+ found.unknown ??= reason;
3381
+ found.loading ??= reason;
2959
3382
  };
2960
- const found = { loads: [], unknown: null, computed: false, unnamed: [] };
2961
3383
  cache.set(file, found);
2962
- const snapshotFile = path6.join(
2963
- path6.dirname(file),
3384
+ const snapshotFile = path7.join(
3385
+ path7.dirname(file),
2964
3386
  "__snapshots__",
2965
- `${path6.basename(file)}.snap`
3387
+ `${path7.basename(file)}.snap`
2966
3388
  );
2967
- if (fs6.existsSync(snapshotFile)) {
3389
+ if (fs7.existsSync(snapshotFile)) {
2968
3390
  found.loads.push(realpath(snapshotFile));
2969
3391
  }
2970
- const mocks = path6.join(path6.dirname(file), "__mocks__");
2971
- const name = path6.basename(file);
3392
+ const mocks = path7.join(path7.dirname(file), "__mocks__");
3393
+ const name = path7.basename(file);
2972
3394
  const candidates = isDirectory(mocks) ? [
2973
- path6.join(mocks, name),
2974
- ...resolveFiles(path6.join(mocks, name.replace(/\.[^.]+$/, "")), false, false)
3395
+ path7.join(mocks, name),
3396
+ ...resolveFiles(path7.join(mocks, name.replace(/\.[^.]+$/, "")), false, false)
2975
3397
  ] : [];
2976
3398
  for (const mock of new Set(candidates)) {
2977
3399
  if (isFile2(mock)) {
@@ -2981,18 +3403,23 @@ function fileEdges(file, context) {
2981
3403
  if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
2982
3404
  return found;
2983
3405
  }
2984
- const source = fs6.readFileSync(file, "utf8");
3406
+ const source = fs7.readFileSync(file, "utf8");
2985
3407
  const code = withoutComments(source);
2986
3408
  if (COMPONENT.test(file)) {
2987
- found.unknown = `${shown(file)} is a component, which can use others that it does not import`;
3409
+ loading(`${shown(file)} is a component, which can use others that it does not import`);
2988
3410
  }
2989
3411
  const uncheckable = findUncheckableImports(code)[0];
2990
3412
  const runtime = RUNTIME_PATHS.exec(code);
3413
+ const runtimeLoads = RUNTIME_LOADS.exec(code);
2991
3414
  if (uncheckable !== void 0) {
2992
3415
  found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
3416
+ found.loading = found.unknown;
2993
3417
  found.computed = true;
2994
3418
  } else if (runtime !== null) {
2995
3419
  found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
3420
+ if (runtimeLoads !== null) {
3421
+ found.loading ??= `${shown(file)} uses ${runtimeLoads[0]}, which can reach files that no import names`;
3422
+ }
2996
3423
  }
2997
3424
  const location = LOCATION.exec(code);
2998
3425
  const refs = parseImports(code);
@@ -3007,30 +3434,39 @@ function fileEdges(file, context) {
3007
3434
  typeLines.set(key, (typeLines.get(key) ?? 0) + 1);
3008
3435
  }
3009
3436
  for (const specifier of specifiers) {
3010
- if (path6.isAbsolute(specifier) || specifier.startsWith("file:")) {
3437
+ if (path7.isAbsolute(specifier) || specifier.startsWith("file:")) {
3011
3438
  const resolved2 = resolveFiles(
3012
- specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
3439
+ withoutQuery(specifier.startsWith("file:") ? new URL(specifier).pathname : specifier)
3013
3440
  );
3014
3441
  if (resolved2.length === 0) {
3015
- found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
3442
+ loading(`${shown(file)} imports ${specifier}, which leads to no file`);
3016
3443
  } else {
3017
3444
  found.loads.push(...resolved2.map(realpath));
3018
3445
  }
3019
3446
  } else if (isProjectImport(specifier, aliases)) {
3020
- const resolved2 = target(specifier, file);
3447
+ const resolved2 = target(withoutQuery(specifier), file);
3021
3448
  if (resolved2.length === 0) {
3022
- found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
3449
+ loading(`${shown(file)} imports ${specifier}, which leads to no file`);
3023
3450
  } else {
3024
3451
  found.loads.push(...resolved2.map(realpath));
3025
3452
  }
3026
3453
  } else if (FILE_MODULES.has(bare(specifier))) {
3027
- found.unknown ??= `${shown(file)} imports ${specifier}, which can reach files that no import names`;
3454
+ const reason = `${shown(file)} imports ${specifier}, which can reach files that no import names`;
3455
+ if (LOADING_MODULES.has(bare(specifier))) {
3456
+ loading(reason);
3457
+ } else {
3458
+ found.unknown ??= reason;
3459
+ }
3028
3460
  } else if (!specifier.startsWith("node:") && !builtinModules2.includes(specifier)) {
3029
3461
  if (!installed(specifier, file)) {
3030
- found.unknown ??= `${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`;
3462
+ loading(
3463
+ `${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`
3464
+ );
3031
3465
  found.unnamed.push(specifier);
3032
3466
  } else if (linkedPackage(specifier, file) !== null) {
3033
- found.unknown ??= `${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`;
3467
+ loading(
3468
+ `${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`
3469
+ );
3034
3470
  found.computed = true;
3035
3471
  }
3036
3472
  }
@@ -3038,10 +3474,14 @@ function fileEdges(file, context) {
3038
3474
  if (location !== null) {
3039
3475
  const reads = LOCATION_READS.exec(code);
3040
3476
  const reader = [...specifiers].find(
3041
- (specifier) => !specifier.startsWith(".") && !path6.isAbsolute(specifier) && !specifier.startsWith("file:") && !isProjectImport(specifier, aliases) && !LOCATION_SAFE.has(bare(specifier))
3477
+ (specifier) => !specifier.startsWith(".") && !path7.isAbsolute(specifier) && !specifier.startsWith("file:") && !isProjectImport(specifier, aliases) && !LOCATION_SAFE.has(bare(specifier))
3042
3478
  );
3043
3479
  if (reads !== null) {
3044
3480
  found.unknown ??= `${shown(file)} uses ${location[0]} and ${reads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
3481
+ const loads = LOCATION_LOADS.exec(code);
3482
+ if (loads !== null) {
3483
+ found.loading ??= `${shown(file)} uses ${location[0]} and ${loads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
3484
+ }
3045
3485
  } else if (reader !== void 0) {
3046
3486
  found.unknown ??= `${shown(file)} uses ${location[0]} and imports ${reader}, which can read files by it that no import names`;
3047
3487
  }
@@ -3060,7 +3500,9 @@ function fileEdges(file, context) {
3060
3500
  if (left > 0) {
3061
3501
  typeLines.set(key, left - 1);
3062
3502
  } else if (text.startsWith(".") && !specifiers.has(text)) {
3063
- found.loads.push(...resolveFiles(path6.resolve(path6.dirname(file), text)).map(realpath));
3503
+ found.loads.push(...resolveFiles(path7.resolve(path7.dirname(file), text)).map(realpath));
3504
+ } else if (PATH_FROM_TOP.test(text) && isFile2(path7.resolve(dir, text))) {
3505
+ found.loads.push(realpath(path7.resolve(dir, text)));
3064
3506
  }
3065
3507
  }
3066
3508
  return found;
@@ -3072,25 +3514,25 @@ function inputsOf(manifest, input) {
3072
3514
  const dir = realpath(manifest.dir);
3073
3515
  const aliases = manifest.settings.importAliases;
3074
3516
  const placed = [...input.tests, ...Object.values(input.covering).flat()];
3075
- const absolute = placed.find((file) => path6.isAbsolute(file) || file.startsWith(".."));
3517
+ const absolute = placed.find((file) => path7.isAbsolute(file) || file.startsWith(".."));
3076
3518
  if (absolute !== void 0) {
3077
3519
  throw new AffectedError(`the report names a spec file outside of the subproject: ${absolute}`);
3078
3520
  }
3079
3521
  const keyOf2 = (file) => {
3080
- const relative = path6.relative(input.root, realpath(file));
3081
- if (relative.startsWith("..") || path6.isAbsolute(relative)) {
3522
+ const relative = path7.relative(input.root, realpath(file));
3523
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) {
3082
3524
  throw new AffectedError(`${file} is outside of the files that are compared`);
3083
3525
  }
3084
3526
  return toPosix2(relative);
3085
3527
  };
3086
- const shown = (file) => toPosix2(path6.relative(dir, file));
3528
+ const shown = (file) => toPosix2(path7.relative(dir, file));
3087
3529
  const plain2 = /* @__PURE__ */ new Map();
3088
3530
  const configTexts = [];
3089
3531
  const runnerSources = [];
3090
3532
  const reached = new Set(input.configs.files);
3091
3533
  const pending = [...input.configs.files];
3092
3534
  for (let file = pending.shift(); file !== void 0; file = pending.shift()) {
3093
- const text = fs6.readFileSync(file, "utf8");
3535
+ const text = fs7.readFileSync(file, "utf8");
3094
3536
  configTexts.push(text);
3095
3537
  runnerSources.push({ file, text });
3096
3538
  for (const next of fileEdges(file, { dir, aliases, cache: plain2 }).loads) {
@@ -3108,7 +3550,7 @@ function inputsOf(manifest, input) {
3108
3550
  // Not what lies outside of the compared files: this package, imported by path, is installed.
3109
3551
  runner: [
3110
3552
  ...runnerSources.filter(({ file }) => {
3111
- const relative = path6.relative(input.root, realpath(file));
3553
+ const relative = path7.relative(input.root, realpath(file));
3112
3554
  return !relative.startsWith("..") && toPosix2(relative) in input.files;
3113
3555
  }),
3114
3556
  ...jestInPackage(dir, input.configs)
@@ -3121,14 +3563,21 @@ function inputsOf(manifest, input) {
3121
3563
  }
3122
3564
  const edges = /* @__PURE__ */ new Map();
3123
3565
  const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges, types });
3566
+ const blind = /* @__PURE__ */ Object.create(null);
3124
3567
  const closure = (start, strict) => {
3125
3568
  const seen = /* @__PURE__ */ new Set([start]);
3126
3569
  const queue = [start];
3127
3570
  for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
3128
3571
  const found = edgesOf(file);
3129
- if (strict && found.unknown !== null) {
3572
+ if (strict === "every" && found.unknown !== null) {
3130
3573
  throw new AffectedError(found.unknown);
3131
3574
  }
3575
+ if (strict === "gate" && found.unknown !== null) {
3576
+ if (found.loading !== null) {
3577
+ throw new AffectedError(found.loading);
3578
+ }
3579
+ blind[keyOf2(file)] = found.unknown;
3580
+ }
3132
3581
  for (const next of found.loads) {
3133
3582
  if (!seen.has(next)) {
3134
3583
  seen.add(next);
@@ -3139,8 +3588,8 @@ function inputsOf(manifest, input) {
3139
3588
  return [...seen];
3140
3589
  };
3141
3590
  const isCompared = (file) => {
3142
- const relative = path6.relative(input.root, realpath(file));
3143
- return !relative.startsWith("..") && !path6.isAbsolute(relative) && toPosix2(relative) in input.files;
3591
+ const relative = path7.relative(input.root, realpath(file));
3592
+ return !relative.startsWith("..") && !path7.isAbsolute(relative) && toPosix2(relative) in input.files;
3144
3593
  };
3145
3594
  const keysOf = (files) => files.map((file) => {
3146
3595
  const key = keyOf2(file);
@@ -3159,10 +3608,10 @@ function inputsOf(manifest, input) {
3159
3608
  if (!SCRIPT.test(loaded)) {
3160
3609
  continue;
3161
3610
  }
3162
- for (const match of fs6.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
3611
+ for (const match of fs7.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
3163
3612
  const text = match[2].replace(/^<rootDir>\//, "");
3164
- const here = resolveFiles(path6.resolve(path6.dirname(loaded), text), false, false);
3165
- const candidates = here.length > 0 ? here : resolveFiles(path6.resolve(dir, text), false, false);
3613
+ const here = resolveFiles(path7.resolve(path7.dirname(loaded), text), false, false);
3614
+ const candidates = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false, false);
3166
3615
  for (const found of candidates) {
3167
3616
  if (text.includes("*") || !isCompared(found)) {
3168
3617
  continue;
@@ -3177,7 +3626,7 @@ function inputsOf(manifest, input) {
3177
3626
  }
3178
3627
  }
3179
3628
  const gatePaths = new Map(
3180
- manifest.gates.map((gate) => [realpath(path6.join(dir, gate.path)), gate.path])
3629
+ manifest.gates.map((gate) => [realpath(path7.join(dir, gate.path)), gate.path])
3181
3630
  );
3182
3631
  const tests = [
3183
3632
  .../* @__PURE__ */ new Set([
@@ -3188,13 +3637,13 @@ function inputsOf(manifest, input) {
3188
3637
  ];
3189
3638
  const runAsSuch = new Set(
3190
3639
  [...tests, ...manifest.gates.map((gate) => gate.path)].map(
3191
- (file) => realpath(path6.join(dir, file))
3640
+ (file) => realpath(path7.join(dir, file))
3192
3641
  )
3193
3642
  );
3194
- const subprojectKey = toPosix2(path6.relative(input.root, dir));
3643
+ const subprojectKey = toPosix2(path7.relative(input.root, dir));
3195
3644
  const isConfig = (file) => input.configs.files.includes(file) || kindOf(keyOf2(file), subprojectKey) === "config";
3196
3645
  for (const file of named.filter((one) => !isConfig(one) && !runAsSuch.has(one))) {
3197
- for (const loaded of closure(file, true)) {
3646
+ for (const loaded of closure(file, "every")) {
3198
3647
  const gate = gatePaths.get(loaded);
3199
3648
  if (gate !== void 0) {
3200
3649
  throw new AffectedError(
@@ -3204,16 +3653,16 @@ function inputsOf(manifest, input) {
3204
3653
  config.add(loaded);
3205
3654
  }
3206
3655
  }
3207
- const fromSubproject = (file) => path6.join(dir, file);
3656
+ const fromSubproject = (file) => path7.join(dir, file);
3208
3657
  const closures = /* @__PURE__ */ Object.create(null);
3209
3658
  for (const file of [...manifest.gates.map((gate) => gate.path), ...tests]) {
3210
3659
  const absolute2 = fromSubproject(file);
3211
- if (!fs6.existsSync(absolute2)) {
3660
+ if (!fs7.existsSync(absolute2)) {
3212
3661
  continue;
3213
3662
  }
3214
- closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), true));
3663
+ closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), "gate"));
3215
3664
  }
3216
- const keyed = (files) => files.map((file) => toPosix2(path6.relative(input.root, path6.join(dir, file))));
3665
+ const keyed = (files) => files.map((file) => toPosix2(path7.relative(input.root, path7.join(dir, file))));
3217
3666
  const dependencies = gateDependencies({
3218
3667
  gates: manifest.gates.map((gate) => ({
3219
3668
  key: keyed([gate.path])[0],
@@ -3223,9 +3672,10 @@ function inputsOf(manifest, input) {
3223
3672
  covering: Object.fromEntries(
3224
3673
  Object.entries(input.covering).map(([gate, specs]) => [keyed([gate])[0], keyed(specs)])
3225
3674
  ),
3226
- closures
3675
+ closures,
3676
+ blind
3227
3677
  });
3228
- const subproject = toPosix2(path6.relative(input.root, dir));
3678
+ const subproject = toPosix2(path7.relative(input.root, dir));
3229
3679
  const prefix = subproject === "" ? "" : `${subproject}/`;
3230
3680
  const gates = /* @__PURE__ */ Object.create(null);
3231
3681
  for (const gate of manifest.gates) {
@@ -3236,12 +3686,13 @@ function inputsOf(manifest, input) {
3236
3686
  specs: found.specs.map(
3237
3687
  (spec) => spec.startsWith(prefix) ? spec.slice(prefix.length) : spec
3238
3688
  ),
3239
- files: found.files
3689
+ files: found.files,
3690
+ ...found.blind === void 0 ? {} : { blind: found.blind }
3240
3691
  };
3241
3692
  }
3242
3693
  const packageMocks = Object.keys(input.files).filter(
3243
3694
  (key) => isNestedMock(key, subproject) && mockNames(key).some(
3244
- (name) => builtinModules2.includes(name) || installed(name, path6.join(input.root, key))
3695
+ (name) => builtinModules2.includes(name) || installed(name, path7.join(input.root, key))
3245
3696
  )
3246
3697
  );
3247
3698
  return {
@@ -3297,7 +3748,7 @@ function scopeOf(manifest, configs, listed2) {
3297
3748
  const cache = /* @__PURE__ */ new Map();
3298
3749
  const context = { dir, aliases: manifest.settings.importAliases, cache };
3299
3750
  const runner = configs.files.slice(1);
3300
- const texts = runner.map((file) => withoutComments(fs6.readFileSync(file, "utf8")));
3751
+ const texts = runner.map((file) => withoutComments(fs7.readFileSync(file, "utf8")));
3301
3752
  if (runner.length === 0 || !texts.every((text) => OWN.test(text))) {
3302
3753
  return { every: NOT_THE_HELPER2 };
3303
3754
  }
@@ -3312,21 +3763,21 @@ function scopeOf(manifest, configs, listed2) {
3312
3763
  fileEdges(file, context).loads.forEach((loaded) => parts.add(loaded));
3313
3764
  }
3314
3765
  }
3315
- if ([...parts].some((file) => PROJECTS.test(withoutComments(fs6.readFileSync(file, "utf8"))))) {
3766
+ if ([...parts].some((file) => PROJECTS.test(withoutComments(fs7.readFileSync(file, "utf8"))))) {
3316
3767
  return {
3317
3768
  every: "the runner config, or a file it is put together from, names projects or a workspace"
3318
3769
  };
3319
3770
  }
3320
- const named = fs6.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path6.join(dir, name));
3321
- const mocks = path6.join(dir, "__mocks__");
3771
+ const named = fs7.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path7.join(dir, name));
3772
+ const mocks = path7.join(dir, "__mocks__");
3322
3773
  const seeds = [
3323
3774
  ...configs.files,
3324
3775
  ...named,
3325
- ...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path6.join(dir, file)),
3326
- ...listed2.filter((file) => file.startsWith(`${mocks}${path6.sep}`))
3327
- ].filter((file) => fs6.existsSync(file)).map(realpath);
3776
+ ...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path7.join(dir, file)),
3777
+ ...listed2.filter((file) => file.startsWith(`${mocks}${path7.sep}`))
3778
+ ].filter((file) => fs7.existsSync(file)).map(realpath);
3328
3779
  const naming = new Set(
3329
- [...configs.files, ...named].filter((file) => fs6.existsSync(file)).map(realpath)
3780
+ [...configs.files, ...named].filter((file) => fs7.existsSync(file)).map(realpath)
3330
3781
  );
3331
3782
  const seen = new Set(seeds);
3332
3783
  const queue = [...seeds];
@@ -3338,7 +3789,7 @@ function scopeOf(manifest, configs, listed2) {
3338
3789
  }
3339
3790
  };
3340
3791
  for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
3341
- if (fs6.statSync(file).isDirectory()) {
3792
+ if (fs7.statSync(file).isDirectory()) {
3342
3793
  continue;
3343
3794
  }
3344
3795
  const edges = fileEdges(file, context);
@@ -3349,11 +3800,11 @@ function scopeOf(manifest, configs, listed2) {
3349
3800
  for (const specifier of edges.unnamed) {
3350
3801
  endingAs(specifier, listed2).forEach(add);
3351
3802
  }
3352
- if (naming.has(file) || SCRIPT.test(file) && path6.dirname(file) === dir) {
3803
+ if (naming.has(file) || SCRIPT.test(file) && path7.dirname(file) === dir) {
3353
3804
  for (const named2 of namedIn(file)) {
3354
3805
  const text = named2.replace(/^<rootDir>\//, "");
3355
- const here = resolveFiles(path6.resolve(path6.dirname(file), text), false);
3356
- const found = here.length > 0 ? here : resolveFiles(path6.resolve(dir, text), false);
3806
+ const here = resolveFiles(path7.resolve(path7.dirname(file), text), false);
3807
+ const found = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false);
3357
3808
  for (const one of text.includes("*") ? [] : found) {
3358
3809
  if (!SCRIPT.test(one)) {
3359
3810
  naming.add(realpath(one));
@@ -3377,7 +3828,7 @@ function endingAs(specifier, files) {
3377
3828
  });
3378
3829
  }
3379
3830
  function namedIn(file) {
3380
- const text = fs6.readFileSync(file, "utf8");
3831
+ const text = fs7.readFileSync(file, "utf8");
3381
3832
  if (SCRIPT.test(file)) {
3382
3833
  return [...text.matchAll(QUOTED)].map((match) => match[2]);
3383
3834
  }
@@ -3531,15 +3982,15 @@ function mayStartPattern(before) {
3531
3982
  return before.last === "" || !")]".includes(before.last);
3532
3983
  }
3533
3984
  function jestInPackage(dir, configs) {
3534
- const file = path6.join(dir, "package.json");
3535
- return configs.runner === "jest" && configs.files.length === 1 && fs6.existsSync(file) ? [{ file, text: fs6.readFileSync(file, "utf8"), mapperOnly: true }] : [];
3985
+ const file = path7.join(dir, "package.json");
3986
+ return configs.runner === "jest" && configs.files.length === 1 && fs7.existsSync(file) ? [{ file, text: fs7.readFileSync(file, "utf8"), mapperOnly: true }] : [];
3536
3987
  }
3537
3988
  var STRING_LITERAL = String.raw`'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"`;
3538
3989
  function stringValue(text) {
3539
3990
  const match = new RegExp(`^\\s*(${STRING_LITERAL})\\s*$`).exec(text);
3540
3991
  return match === null ? null : match[1].slice(1, -1).replace(/\\(.)/g, "$1");
3541
3992
  }
3542
- function partsOf(text, at) {
3993
+ function partsOf2(text, at) {
3543
3994
  const parts = [];
3544
3995
  let depth = 0;
3545
3996
  let start = at + 1;
@@ -3576,7 +4027,7 @@ function property(part) {
3576
4027
  }
3577
4028
  function aliasPath(expression, file, text, depth = 0) {
3578
4029
  const value = expression.trim();
3579
- const here = path6.dirname(file);
4030
+ const here = path7.dirname(file);
3580
4031
  if (value === "__dirname" || value === "import.meta.dirname" || /^(?:path\.)?dirname\(\s*(?:url\.)?fileURLToPath\(\s*import\.meta\.url\s*\)\s*\)$/.test(value)) {
3581
4032
  return here;
3582
4033
  }
@@ -3584,18 +4035,18 @@ function aliasPath(expression, file, text, depth = 0) {
3584
4035
  `^(?:(?:url\\.)?fileURLToPath\\(\\s*new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\)|new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\.pathname)$`
3585
4036
  ).exec(value);
3586
4037
  if (url !== null) {
3587
- return path6.resolve(here, stringValue(url[1] ?? url[2]));
4038
+ return path7.resolve(here, stringValue(url[1] ?? url[2]));
3588
4039
  }
3589
4040
  const call = /^(?:path\.)?(?:resolve|join)\s*\(/.exec(value);
3590
4041
  if (call !== null) {
3591
- const found = partsOf(value, call[0].length - 1);
4042
+ const found = partsOf2(value, call[0].length - 1);
3592
4043
  if (found === null || value.slice(found.end + 1).trim() !== "" || found.parts.length === 0) {
3593
4044
  return null;
3594
4045
  }
3595
4046
  const [first, ...rest] = found.parts;
3596
4047
  const base = aliasPath(first, file, text, depth + 1);
3597
4048
  const parts = rest.map(stringValue);
3598
- return base === null || parts.some((part) => part === null) ? null : path6.resolve(base, ...parts);
4049
+ return base === null || parts.some((part) => part === null) ? null : path7.resolve(base, ...parts);
3599
4050
  }
3600
4051
  if (/^[A-Za-z_$][\w$]*$/.test(value) && depth < 4) {
3601
4052
  const declarations = text.match(
@@ -3611,7 +4062,7 @@ function aliasPath(expression, file, text, depth = 0) {
3611
4062
  var PACKAGE_NAME = /^(?:@[\w-][\w.-]*\/)?[\w-][\w.-]*$/;
3612
4063
  function isDirectory(file) {
3613
4064
  try {
3614
- return fs6.statSync(file).isDirectory();
4065
+ return fs7.statSync(file).isDirectory();
3615
4066
  } catch {
3616
4067
  return false;
3617
4068
  }
@@ -3637,8 +4088,8 @@ function unexplainedMaps(input) {
3637
4088
  const { dir, aliases } = input;
3638
4089
  const reasons = [];
3639
4090
  const mapped = [];
3640
- const shown = (file) => toPosix2(path6.relative(dir, file));
3641
- const under = (base, rest) => rest === "" ? `${base}/` : path6.join(base, rest);
4091
+ const shown = (file) => toPosix2(path7.relative(dir, file));
4092
+ const under = (base, rest) => rest === "" ? `${base}/` : path7.join(base, rest);
3642
4093
  const named = (prefix, target) => {
3643
4094
  const alias = aliases.find((candidate) => prefix.startsWith(candidate.prefix));
3644
4095
  if (alias === void 0) {
@@ -3646,10 +4097,10 @@ function unexplainedMaps(input) {
3646
4097
  }
3647
4098
  const full = alias.target + prefix.slice(alias.prefix.length);
3648
4099
  const slash = full === "" || full.endsWith("/");
3649
- return path6.resolve(dir, full) === path6.resolve(target) && slash === target.endsWith("/");
4100
+ return path7.resolve(dir, full) === path7.resolve(target) && slash === target.endsWith("/");
3650
4101
  };
3651
4102
  const advice = (prefix, target) => {
3652
- const relative = toPosix2(path6.relative(dir, target));
4103
+ const relative = toPosix2(path7.relative(dir, target));
3653
4104
  const shownTarget = relative === "" ? "./" : `${relative}/`;
3654
4105
  const shorter = aliases.find((alias) => prefix.startsWith(alias.prefix));
3655
4106
  const before = shorter === void 0 ? "" : shorter.prefix === prefix ? `, in place of the alias of ${prefix}` : `, before the alias of ${shorter.prefix}`;
@@ -3662,7 +4113,7 @@ function unexplainedMaps(input) {
3662
4113
  let prefix = null;
3663
4114
  let to = null;
3664
4115
  if (list) {
3665
- const object = /^\s*\{/.test(part) ? partsOf(part, part.indexOf("{")) : null;
4116
+ const object = /^\s*\{/.test(part) ? partsOf2(part, part.indexOf("{")) : null;
3666
4117
  const fields = Object.fromEntries(
3667
4118
  (object?.parts ?? []).map(property).map((one) => [one?.key ?? "", one?.value ?? ""])
3668
4119
  );
@@ -3714,7 +4165,7 @@ function unexplainedMaps(input) {
3714
4165
  const word = match[0];
3715
4166
  const opening = /^['"]?\s*:\s*/.exec(text.slice(match.index + word.length));
3716
4167
  const at = opening === null ? -1 : match.index + word.length + opening[0].length;
3717
- const found = at === -1 ? null : partsOf(text, at);
4168
+ const found = at === -1 ? null : partsOf2(text, at);
3718
4169
  const unreadable = `${shown(file)} names ${word} in a way that cannot be read as a list of aliases`;
3719
4170
  if (found === null || text[at] !== "{" && !(word === "alias" && text[at] === "[")) {
3720
4171
  reasons.push(unreadable);
@@ -3744,7 +4195,7 @@ function unexplainedMaps(input) {
3744
4195
  }
3745
4196
  const prefix = mapperPrefix(entry.key);
3746
4197
  const target = value === null ? null : /^<rootDir>\/(.*)\$1$/.exec(value);
3747
- const to = target === null ? null : under(path6.dirname(file), target[1]);
4198
+ const to = target === null ? null : under(path7.dirname(file), target[1]);
3748
4199
  if (prefix === null || to === null) {
3749
4200
  reasons.push(missing(file, entry.key, word));
3750
4201
  } else if (!named(prefix, to)) {
@@ -3755,19 +4206,19 @@ function unexplainedMaps(input) {
3755
4206
  }
3756
4207
  }
3757
4208
  }
3758
- const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(path6.basename(file)));
4209
+ const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(path7.basename(file)));
3759
4210
  const withBase = tsconfigs.filter(({ text }) => text !== null && /"baseUrl"\s*:/.test(text));
3760
4211
  for (const { file, text } of input.compiler) {
3761
4212
  if (text === null) {
3762
4213
  continue;
3763
4214
  }
3764
4215
  const plain2 = withoutComments(text);
3765
- const name = path6.basename(file);
4216
+ const name = path7.basename(file);
3766
4217
  const swc = name === ".swcrc";
3767
4218
  if (!/^[jt]sconfig/.test(name) && !swc) {
3768
4219
  const babel = /"babel"\s*:\s*/.exec(plain2);
3769
4220
  const own2 = name !== "package.json" ? plain2 : babel === null ? "" : (() => {
3770
- const found2 = partsOf(plain2, babel.index + babel[0].length);
4221
+ const found2 = partsOf2(plain2, babel.index + babel[0].length);
3771
4222
  return found2 === null ? plain2 : found2.parts.join(",");
3772
4223
  })();
3773
4224
  const maps = /module-resolver|\balias\b/.exec(own2);
@@ -3781,7 +4232,7 @@ function unexplainedMaps(input) {
3781
4232
  continue;
3782
4233
  }
3783
4234
  const at = paths.index + paths[0].length;
3784
- const found = plain2[at] === "{" ? partsOf(plain2, at) : null;
4235
+ const found = plain2[at] === "{" ? partsOf2(plain2, at) : null;
3785
4236
  if (found === null) {
3786
4237
  reasons.push(`${shown(file)} names paths in a way that cannot be read as a list of aliases`);
3787
4238
  continue;
@@ -3791,7 +4242,7 @@ function unexplainedMaps(input) {
3791
4242
  reasons.push(`${shown(file)} names paths, and baseUrl is set in another file`);
3792
4243
  continue;
3793
4244
  }
3794
- const base = path6.resolve(path6.dirname(file), own?.[1] ?? ".");
4245
+ const base = path7.resolve(path7.dirname(file), own?.[1] ?? ".");
3795
4246
  for (const part of found.parts) {
3796
4247
  const entry = property(part);
3797
4248
  if (entry === null) {
@@ -3800,10 +4251,10 @@ function unexplainedMaps(input) {
3800
4251
  );
3801
4252
  continue;
3802
4253
  }
3803
- const list = entry.value.startsWith("[") ? partsOf(entry.value, 0) : null;
4254
+ const list = entry.value.startsWith("[") ? partsOf2(entry.value, 0) : null;
3804
4255
  const target = list?.parts.length === 1 ? stringValue(list.parts[0]) : null;
3805
4256
  const prefix = entry.key.endsWith("*") ? entry.key.slice(0, -1) : null;
3806
- if (prefix === null && target !== null && !target.includes("*") && path6.resolve(base, target).split(path6.sep).includes("node_modules")) {
4257
+ if (prefix === null && target !== null && !target.includes("*") && path7.resolve(base, target).split(path7.sep).includes("node_modules")) {
3807
4258
  continue;
3808
4259
  }
3809
4260
  const directory = target?.endsWith("*") ? target.slice(0, -1) : null;
@@ -3821,26 +4272,69 @@ function unexplainedMaps(input) {
3821
4272
  }
3822
4273
 
3823
4274
  // src/commands/mutation.ts
3824
- import { spawnSync as spawnSync2 } from "child_process";
4275
+ import { spawnSync as spawnSync4 } from "child_process";
4276
+ import fs10 from "fs";
4277
+ import path10 from "path";
4278
+ import { pathToFileURL } from "url";
4279
+
4280
+ // src/main-worktree.ts
4281
+ import { spawnSync as spawnSync3 } from "child_process";
3825
4282
  import fs8 from "fs";
3826
4283
  import path8 from "path";
3827
- import { pathToFileURL } from "url";
4284
+ function git2(dir, args) {
4285
+ const result = spawnSync3("git", args, {
4286
+ cwd: dir,
4287
+ env: gitEnvironment(process.env),
4288
+ encoding: "utf8"
4289
+ });
4290
+ return result.error || result.status !== 0 ? null : result.stdout.trim().split("\n");
4291
+ }
4292
+ function linkedWorktreeOf(dir) {
4293
+ const found = git2(dir, [
4294
+ "rev-parse",
4295
+ "--path-format=absolute",
4296
+ "--show-toplevel",
4297
+ "--git-dir",
4298
+ "--git-common-dir"
4299
+ ]);
4300
+ if (found === null || found.length !== 3) {
4301
+ return null;
4302
+ }
4303
+ const [top, gitDir, common] = found;
4304
+ if (gitDir === common || path8.basename(common) !== ".git") {
4305
+ return null;
4306
+ }
4307
+ try {
4308
+ const main2 = fs8.realpathSync(path8.dirname(common));
4309
+ const real = fs8.realpathSync(top);
4310
+ return main2 === real ? null : { top: real, main: main2 };
4311
+ } catch {
4312
+ return null;
4313
+ }
4314
+ }
4315
+ function inMainWorktree(linked, file) {
4316
+ const relative = path8.relative(linked.top, file);
4317
+ if (relative === "" || relative.startsWith("..") || path8.isAbsolute(relative)) {
4318
+ return null;
4319
+ }
4320
+ return path8.join(linked.main, relative);
4321
+ }
3828
4322
 
3829
4323
  // src/commands/mutation-result.ts
3830
- import fs7 from "fs";
3831
- import path7 from "path";
4324
+ import fs9 from "fs";
4325
+ import path9 from "path";
3832
4326
  function reportPathOf(manifest, override) {
3833
- return path7.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
4327
+ return path9.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
3834
4328
  }
3835
- function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0) {
4329
+ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0, retried = null) {
3836
4330
  const manifest = readManifest(dir);
3837
4331
  const gates = only.length === 0 ? manifest.gates : manifest.gates.filter((gate) => only.includes(gate.path));
3838
4332
  const caveat = only.length === 0 ? "" : gateRunCaveat(gates.length, manifest.gates.length);
3839
4333
  const reportPath = reportPathOf(manifest, reportOverride);
3840
- const shown = path7.relative(manifest.dir, reportPath) || reportPath;
4334
+ const shown = path9.relative(manifest.dir, reportPath) || reportPath;
3841
4335
  let report;
3842
4336
  try {
3843
- report = JSON.parse(fs7.readFileSync(reportPath, "utf8"));
4337
+ report = JSON.parse(fs9.readFileSync(reportPath, "utf8"));
3844
4338
  } catch (error) {
3845
4339
  io2.err(
3846
4340
  `test-gates mutation: cannot read the Stryker JSON report ${shown} (${error.message}). Did Stryker finish, with the "json" reporter writing to this path?`
@@ -3857,7 +4351,7 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3857
4351
  maxTimeouts: manifest.settings.maxTimeouts,
3858
4352
  readSource: (gatePath) => {
3859
4353
  try {
3860
- return fs7.readFileSync(path7.join(manifest.dir, gatePath), "utf8");
4354
+ return fs9.readFileSync(path9.join(manifest.dir, gatePath), "utf8");
3861
4355
  } catch {
3862
4356
  return void 0;
3863
4357
  }
@@ -3905,8 +4399,34 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3905
4399
  io2.err(` - ${formatSurvivor(mutant)}`);
3906
4400
  }
3907
4401
  io2.err("");
3908
- io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
3909
- io2.err("can time out instead. Run again with less load (-- --concurrency 1). Only for a");
4402
+ if (retried === null || retried === "failed") {
4403
+ io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
4404
+ if (retried === null) {
4405
+ io2.err(
4406
+ "can time out instead. `test-gates mutation` runs such timeouts again one at a time"
4407
+ );
4408
+ io2.err("(--concurrency 1) before it fails; these were not run again. Only for a");
4409
+ } else {
4410
+ io2.err(
4411
+ "can time out instead. They were run again one at a time (--concurrency 1), but that"
4412
+ );
4413
+ io2.err("run gave no result (see above), so these are those of the first run. Only for a");
4414
+ }
4415
+ } else if (retried.missing === 0) {
4416
+ io2.err("They timed out again when they were run one at a time (--concurrency 1), so they");
4417
+ io2.err("most likely never finish. Look at each mutant first. Only for a");
4418
+ } else {
4419
+ const again = retried.statuses["Timeout"] ?? 0;
4420
+ if (again > 0) {
4421
+ io2.err(
4422
+ `${again} of them timed out again when they were run one at a time (--concurrency 1).`
4423
+ );
4424
+ }
4425
+ io2.err(
4426
+ `${retried.missing} of them were not in the report of the run again (--concurrency 1),`
4427
+ );
4428
+ io2.err("so they keep the timeout of the first run. Look at each mutant first. Only for a");
4429
+ }
3910
4430
  io2.err("mutant that can never finish (a loop that no longer ends), add it to");
3911
4431
  io2.err('"expectedTimeouts" of the gate in test-gates.json and fill in "reason"');
3912
4432
  io2.err("(settings.mutation.maxTimeouts raises the limit for the whole project):");
@@ -3925,16 +4445,16 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3925
4445
  // src/commands/mutation.ts
3926
4446
  var CONFIG_MARK = "\0test-gates-stryker-config\0";
3927
4447
  function loadStrykerConfig(manifest, config) {
3928
- const file = path8.resolve(manifest.dir, config);
4448
+ const file = path10.resolve(manifest.dir, config);
3929
4449
  if (file.endsWith(".json")) {
3930
4450
  try {
3931
- return JSON.parse(fs8.readFileSync(file, "utf8"));
4451
+ return JSON.parse(fs10.readFileSync(file, "utf8"));
3932
4452
  } catch {
3933
4453
  return void 0;
3934
4454
  }
3935
4455
  }
3936
- const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile, checkers: config.checkers,htmlReporter: config.htmlReporter, jsonReporter: config.jsonReporter,ignorePatterns: config.ignorePatterns,}) + ${JSON.stringify(CONFIG_MARK)});`;
3937
- const result = spawnSync2(process.execPath, ["--input-type=module", "--eval", script], {
4456
+ const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile, checkers: config.checkers,htmlReporter: config.htmlReporter, jsonReporter: config.jsonReporter,ignorePatterns: config.ignorePatterns, cleanTempDir: config.cleanTempDir,}) + ${JSON.stringify(CONFIG_MARK)});`;
4457
+ const result = spawnSync4(process.execPath, ["--input-type=module", "--eval", script], {
3938
4458
  cwd: manifest.dir,
3939
4459
  encoding: "utf8",
3940
4460
  timeout: 6e4
@@ -3947,7 +4467,7 @@ function loadStrykerConfig(manifest, config) {
3947
4467
  }
3948
4468
  function modifiedAt(file) {
3949
4469
  try {
3950
- const stat = fs8.statSync(file, { bigint: true });
4470
+ const stat = fs10.statSync(file, { bigint: true });
3951
4471
  return `${stat.mtimeNs} ${stat.ino}`;
3952
4472
  } catch {
3953
4473
  return null;
@@ -3956,6 +4476,7 @@ function modifiedAt(file) {
3956
4476
  function guardRecordPath(incrementalFile) {
3957
4477
  return `${incrementalFile}.test-gates.json`;
3958
4478
  }
4479
+ var forcedLine = (reason) => `test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${reason}`;
3959
4480
  function coveringSpecs(report, gates) {
3960
4481
  const fileOf = /* @__PURE__ */ new Map();
3961
4482
  for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
@@ -3982,10 +4503,20 @@ function coveringSpecs(report, gates) {
3982
4503
  }
3983
4504
  return covering;
3984
4505
  }
3985
- function decideReuse(manifest, config, strykerArgs, part, affected) {
4506
+ function readSeed(incrementalFile, recordFile) {
4507
+ try {
4508
+ const incremental = fs10.readFileSync(incrementalFile);
4509
+ const record = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
4510
+ const named = record?.incremental;
4511
+ return named === hashOf(incremental) ? { incremental, record } : null;
4512
+ } catch {
4513
+ return null;
4514
+ }
4515
+ }
4516
+ function decideReuse(manifest, config, strykerArgs, part, affected, found = () => void 0) {
3986
4517
  const loaded = loadStrykerConfig(manifest, config);
3987
4518
  const options = incrementalOptions(loaded, strykerArgs);
3988
- const base = fs8.realpathSync(manifest.dir);
4519
+ const base = fs10.realpathSync(manifest.dir);
3989
4520
  const checkersArg = strykerArgs.some(
3990
4521
  (arg) => arg === "--checkers" || arg.startsWith("--checkers=")
3991
4522
  );
@@ -4009,35 +4540,42 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4009
4540
  last: void 0,
4010
4541
  ci: affected ? affectedRefused(process.env) : null,
4011
4542
  unchanged: null,
4012
- configs
4543
+ configs,
4544
+ temp: {
4545
+ dir: loaded === void 0 ? null : path10.resolve(base, options.tempDirName),
4546
+ keeps: loaded?.cleanTempDir === false
4547
+ },
4548
+ mainWorktree: null,
4549
+ seed: null
4013
4550
  };
4551
+ found(decision.temp);
4014
4552
  if (loaded !== void 0 && !options.incremental) {
4015
4553
  return decision;
4016
4554
  }
4017
4555
  decision.incremental = true;
4018
- const incrementalFile = path8.resolve(base, options.incrementalFile);
4556
+ const incrementalFile = path10.resolve(base, options.incrementalFile);
4019
4557
  const recordFile = guardRecordPath(incrementalFile);
4020
4558
  decision.incrementalFile = incrementalFile;
4021
4559
  decision.recordFile = recordFile;
4022
4560
  let snapshot = null;
4023
4561
  let root = base;
4024
4562
  let failure = "the Stryker config could not be loaded to see where the incremental file is";
4025
- const reports = reportFiles(loaded).map((file) => path8.resolve(base, file));
4563
+ const reports = reportFiles(loaded).map((file) => path10.resolve(base, file));
4026
4564
  const ignores = strykerIgnores(loaded);
4027
4565
  const read2 = (file) => {
4028
- const relative = path8.relative(base, file);
4029
- const inside = !relative.startsWith("..") && !path8.isAbsolute(relative);
4030
- return !reports.includes(file) && !(inside && ignores(relative.split(path8.sep).join("/")));
4566
+ const relative = path10.relative(base, file);
4567
+ const inside = !relative.startsWith("..") && !path10.isAbsolute(relative);
4568
+ return !reports.includes(file) && !(inside && ignores(relative.split(path10.sep).join("/")));
4031
4569
  };
4032
4570
  if (loaded !== void 0) {
4033
4571
  const scope = (listed2) => {
4034
4572
  try {
4035
- const found = loadedScope(manifest, configs(), listed2);
4036
- if (!("files" in found)) {
4037
- decision.scope = found;
4573
+ const found2 = loadedScope(manifest, configs(), listed2);
4574
+ if (!("files" in found2)) {
4575
+ decision.scope = found2;
4038
4576
  return null;
4039
4577
  }
4040
- const files = new Set([...found.files].filter(read2));
4578
+ const files = new Set([...found2.files].filter(read2));
4041
4579
  decision.scope = { files: files.size };
4042
4580
  return files;
4043
4581
  } catch (error) {
@@ -4053,8 +4591,8 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4053
4591
  written: [
4054
4592
  incrementalFile,
4055
4593
  recordFile,
4056
- path8.resolve(base, options.tempDirName),
4057
- path8.resolve(base, manifest.settings.strykerReportFile),
4594
+ path10.resolve(base, options.tempDirName),
4595
+ path10.resolve(base, manifest.settings.strykerReportFile),
4058
4596
  ...reports
4059
4597
  ],
4060
4598
  args: comparableArgs(strykerArgs)
@@ -4067,17 +4605,32 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4067
4605
  decision.snapshot = snapshot;
4068
4606
  decision.failure = snapshot === null ? failure : null;
4069
4607
  decision.root = root;
4070
- const incremental = hashFile(incrementalFile);
4608
+ const tempDir = path10.basename(options.tempDirName);
4609
+ if (!part && !options.force && snapshot?.mode === "git" && !path10.relative(root, base).split(path10.sep).includes(tempDir) && !fs10.existsSync(incrementalFile) && !fs10.existsSync(recordFile)) {
4610
+ const linked = linkedWorktreeOf(base);
4611
+ const mainIncremental = linked === null ? null : inMainWorktree(linked, incrementalFile);
4612
+ const mainRecord = linked === null ? null : inMainWorktree(linked, recordFile);
4613
+ if (linked !== null && mainIncremental !== null && mainRecord !== null) {
4614
+ decision.mainWorktree = linked.main;
4615
+ const seed = readSeed(mainIncremental, mainRecord);
4616
+ if (seed !== null) {
4617
+ decision.seed = seed;
4618
+ }
4619
+ }
4620
+ }
4621
+ const incremental = decision.seed === null ? hashFile(incrementalFile) : hashOf(decision.seed.incremental);
4071
4622
  decision.incrementalHash = incremental;
4072
4623
  if (part) {
4073
4624
  decision.force = incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null;
4074
4625
  return decision;
4075
4626
  }
4076
- let stored;
4077
- try {
4078
- stored = JSON.parse(fs8.readFileSync(recordFile, "utf8"));
4079
- } catch {
4080
- stored = void 0;
4627
+ let stored = decision.seed?.record;
4628
+ if (decision.seed === null) {
4629
+ try {
4630
+ stored = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
4631
+ } catch {
4632
+ stored = void 0;
4633
+ }
4081
4634
  }
4082
4635
  const ci = decision.ci;
4083
4636
  let force = null;
@@ -4091,6 +4644,9 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4091
4644
  if (force === null) {
4092
4645
  const read3 = readRecord({ record: stored, incremental, snapshot });
4093
4646
  last = "record" in read3 ? read3.record.affected : void 0;
4647
+ if (decision.seed !== null) {
4648
+ force = blindInMainRecord(manifest, { root, snapshot, configs, last });
4649
+ }
4094
4650
  }
4095
4651
  } else if (ci !== null) {
4096
4652
  force = ci;
@@ -4117,8 +4673,10 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4117
4673
  config: inputs.config,
4118
4674
  gates: manifest.gates.map((gate) => ({
4119
4675
  path: gate.path,
4120
- files: inputs.gates[gate.path]?.files ?? []
4121
- }))
4676
+ files: inputs.gates[gate.path]?.files ?? [],
4677
+ blind: inputs.gates[gate.path]?.blind
4678
+ })),
4679
+ seeded: decision.seed !== null
4122
4680
  });
4123
4681
  } catch (error) {
4124
4682
  if (!(error instanceof AffectedError)) {
@@ -4127,7 +4685,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4127
4685
  plan = { full: `what a gate depends on cannot be told: ${error.message}` };
4128
4686
  }
4129
4687
  if ("full" in plan) {
4130
- if (refuseReuse({ record: stored, incremental, snapshot }) === null) {
4688
+ if (decision.seed === null && refuseReuse({ record: stored, incremental, snapshot }) === null) {
4131
4689
  decision.unchanged = plan.full;
4132
4690
  plan = null;
4133
4691
  } else {
@@ -4144,9 +4702,45 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4144
4702
  decision.last = last;
4145
4703
  return decision;
4146
4704
  }
4147
- function guardIncremental(manifest, config, strykerArgs, part, affected) {
4148
- const nothing = { force: null, mutate: null, said: [], record: () => void 0 };
4149
- const decision = decideReuse(manifest, config, strykerArgs, part, affected);
4705
+ function blindInMainRecord(manifest, input) {
4706
+ const { last } = input;
4707
+ const unknown = last === void 0 ? "each gate" : manifest.gates.find((gate) => !Object.hasOwn(last.gates, gate.path))?.path;
4708
+ if (last === void 0 || unknown !== void 0) {
4709
+ return `the record of the main worktree does not say what ${unknown} depends on, so it may read files git does not list, which differ between worktrees`;
4710
+ }
4711
+ try {
4712
+ const inputs = affectedInputs(manifest, {
4713
+ root: input.root,
4714
+ files: input.snapshot.files,
4715
+ configs: input.configs(),
4716
+ tests: last.tests,
4717
+ covering: Object.fromEntries(
4718
+ Object.entries(last.gates).map(([gate, entry]) => [gate, entry.specs])
4719
+ )
4720
+ });
4721
+ for (const gate of manifest.gates) {
4722
+ const blind = inputs.gates[gate.path]?.blind ?? last.gates[gate.path]?.blind;
4723
+ if (blind !== void 0) {
4724
+ return `${gate.path} may read files git does not list, which differ between worktrees (${blind})`;
4725
+ }
4726
+ }
4727
+ return null;
4728
+ } catch (error) {
4729
+ if (!(error instanceof AffectedError)) {
4730
+ throw error;
4731
+ }
4732
+ return `what a gate depends on cannot be told, so it may read files git does not list, which differ between worktrees: ${error.message}`;
4733
+ }
4734
+ }
4735
+ function guardIncremental(manifest, config, strykerArgs, part, affected, found) {
4736
+ const decision = decideReuse(manifest, config, strykerArgs, part, affected, found);
4737
+ const nothing = {
4738
+ force: null,
4739
+ mutate: null,
4740
+ said: [],
4741
+ temp: decision.temp,
4742
+ record: () => void 0
4743
+ };
4150
4744
  if (!decision.incremental) {
4151
4745
  return affected ? {
4152
4746
  ...nothing,
@@ -4161,10 +4755,14 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4161
4755
  const incremental = decision.incrementalHash;
4162
4756
  const options = { force: decision.forced };
4163
4757
  if (part) {
4164
- fs8.rmSync(recordFile, { force: true });
4758
+ fs10.rmSync(recordFile, { force: true });
4165
4759
  return { ...nothing, force };
4166
4760
  }
4167
- fs8.rmSync(recordFile, { force: true });
4761
+ if (decision.seed !== null) {
4762
+ fs10.mkdirSync(path10.dirname(incrementalFile), { recursive: true });
4763
+ fs10.writeFileSync(incrementalFile, decision.seed.incremental);
4764
+ }
4765
+ fs10.rmSync(recordFile, { force: true });
4168
4766
  const before = modifiedAt(incrementalFile);
4169
4767
  const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
4170
4768
  const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
@@ -4179,7 +4777,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4179
4777
  rerun: timedOut,
4180
4778
  expected
4181
4779
  } = timeoutRanges(
4182
- JSON.parse(fs8.readFileSync(incrementalFile, "utf8")),
4780
+ JSON.parse(fs10.readFileSync(incrementalFile, "utf8")),
4183
4781
  kept
4184
4782
  ));
4185
4783
  } catch {
@@ -4210,11 +4808,20 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4210
4808
  said.push(`test-gates mutation: ${describeExpected(expected)}`);
4211
4809
  }
4212
4810
  }
4811
+ if (decision.seed !== null) {
4812
+ if (said.length === 0 && everyGate !== null) {
4813
+ said.push(forcedLine(everyGate));
4814
+ }
4815
+ said.unshift(
4816
+ `test-gates mutation: this worktree has no record of its own, so it starts from the one of the main worktree (${decision.mainWorktree})`
4817
+ );
4818
+ }
4213
4819
  return {
4214
4820
  // Every gate is run again: the same as a run of every mutant.
4215
4821
  force: everyGate,
4216
4822
  mutate: subset.length + ranges.length > 0 ? [...subset, ...ranges] : null,
4217
4823
  said,
4824
+ temp: decision.temp,
4218
4825
  record: (judged) => {
4219
4826
  const written = hashFile(incrementalFile);
4220
4827
  if (snapshot === null || written === null || modifiedAt(incrementalFile) === before) {
@@ -4225,7 +4832,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4225
4832
  record.affected = affectedRecord(judged, ran, last);
4226
4833
  }
4227
4834
  try {
4228
- fs8.writeFileSync(recordFile, `${JSON.stringify(record)}
4835
+ fs10.writeFileSync(recordFile, `${JSON.stringify(record)}
4229
4836
  `);
4230
4837
  } catch {
4231
4838
  }
@@ -4256,7 +4863,11 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4256
4863
  for (const gate of Object.keys(covering)) {
4257
4864
  const entry = inputs.gates[gate];
4258
4865
  if (entry !== void 0) {
4259
- recorded[gate] = { specs: entry.specs, files: entry.files };
4866
+ recorded[gate] = {
4867
+ specs: entry.specs,
4868
+ files: entry.files,
4869
+ ...entry.blind === void 0 ? {} : { blind: entry.blind }
4870
+ };
4260
4871
  }
4261
4872
  }
4262
4873
  return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
@@ -4307,53 +4918,120 @@ function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
4307
4918
  );
4308
4919
  }
4309
4920
  const reportFile = reportPathOf(manifest, null);
4310
- fs8.rmSync(reportFile, { force: true });
4311
- const guard = guardIncremental(manifest, config, strykerArgs, part, affected);
4921
+ fs10.rmSync(reportFile, { force: true });
4922
+ let removed = [];
4923
+ const guard = guardIncremental(manifest, config, strykerArgs, part, affected, (temp) => {
4924
+ removed = removeStaleSandboxes(manifest.dir, temp);
4925
+ });
4312
4926
  if (guard.said.length > 0) {
4313
4927
  guard.said.forEach((line) => io2.out(line));
4314
4928
  } else if (guard.force !== null) {
4315
- io2.out(
4316
- `test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${guard.force}`
4317
- );
4929
+ io2.out(forcedLine(guard.force));
4318
4930
  }
4931
+ removed.forEach((line) => io2.out(line));
4319
4932
  const guardArgs = guard.force === null && guard.mutate === null ? [] : ["--force"];
4320
4933
  const partArgs = part ? ["--mutate", chosen.selected.join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
4321
- const result = spawnSync2(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
4322
- cwd: manifest.dir,
4323
- stdio: "inherit",
4324
- shell: process.platform === "win32"
4325
- });
4326
- if (result.error) {
4327
- io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
4328
- return 1;
4329
- }
4330
- if (result.status !== 0) {
4331
- io2.err(
4332
- `test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
4934
+ const unmark = markRun(manifest.dir, guard.temp);
4935
+ let retried;
4936
+ try {
4937
+ const result = spawnSync4(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
4938
+ cwd: manifest.dir,
4939
+ stdio: "inherit",
4940
+ shell: process.platform === "win32"
4941
+ });
4942
+ if (result.error) {
4943
+ io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
4944
+ return 1;
4945
+ }
4946
+ if (result.status !== 0) {
4947
+ io2.err(
4948
+ `test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
4949
+ );
4950
+ return 1;
4951
+ }
4952
+ retried = retryTimeouts(
4953
+ stryker,
4954
+ [config, ...strykerArgs],
4955
+ manifest,
4956
+ reportFile,
4957
+ part ? manifest.gates.filter((gate) => chosen.selected.includes(gate.path)) : manifest.gates,
4958
+ io2
4333
4959
  );
4334
- return 1;
4960
+ } finally {
4961
+ unmark();
4335
4962
  }
4336
4963
  let judged = null;
4337
- const status = runMutationResult(dir, null, io2, part ? chosen.selected : [], (seen) => {
4338
- judged = seen;
4339
- });
4964
+ const status = runMutationResult(
4965
+ dir,
4966
+ null,
4967
+ io2,
4968
+ part ? chosen.selected : [],
4969
+ (seen) => {
4970
+ judged = seen;
4971
+ },
4972
+ retried
4973
+ );
4340
4974
  guard.record(judged);
4341
4975
  if (part) {
4342
- fs8.rmSync(reportFile, { force: true });
4976
+ fs10.rmSync(reportFile, { force: true });
4343
4977
  }
4344
4978
  return status;
4345
4979
  }
4980
+ function retryTimeouts(stryker, args, manifest, reportFile, gates, io2) {
4981
+ let first;
4982
+ try {
4983
+ first = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
4984
+ } catch {
4985
+ return null;
4986
+ }
4987
+ if (typeof first !== "object" || first === null) {
4988
+ return null;
4989
+ }
4990
+ const { ranges, rerun } = timeoutRanges(first, gates);
4991
+ if (ranges.length === 0 || rerun <= manifest.settings.maxTimeouts) {
4992
+ return null;
4993
+ }
4994
+ io2.out(
4995
+ `test-gates mutation: ${rerun} mutant(s) timed out outside expectedTimeouts, running them again one at a time (--concurrency 1)`
4996
+ );
4997
+ fs10.rmSync(reportFile, { force: true });
4998
+ const result = spawnSync4(
4999
+ stryker,
5000
+ ["run", ...args, "--mutate", ranges.join(","), "--concurrency", "1", "--force"],
5001
+ { cwd: manifest.dir, stdio: "inherit", shell: process.platform === "win32" }
5002
+ );
5003
+ let retry = null;
5004
+ if (!result.error && result.status === 0) {
5005
+ try {
5006
+ retry = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
5007
+ } catch {
5008
+ retry = null;
5009
+ }
5010
+ }
5011
+ if (retry === null || typeof retry !== "object") {
5012
+ const why = result.error ? result.error.message : result.signal ? `signal ${result.signal}` : result.status === 0 ? "no report" : `exit ${result.status}`;
5013
+ io2.err(
5014
+ `test-gates mutation: running the timed-out mutants again failed (${why}). The first run is judged`
5015
+ );
5016
+ fs10.writeFileSync(reportFile, JSON.stringify(first));
5017
+ return "failed";
5018
+ }
5019
+ const merged = mergeRetry(first, retry, gates);
5020
+ fs10.writeFileSync(reportFile, JSON.stringify(merged.report));
5021
+ io2.out(formatRetry(merged.outcome));
5022
+ return merged.outcome;
5023
+ }
4346
5024
 
4347
5025
  // src/commands/doctor.ts
4348
5026
  function read(file) {
4349
5027
  try {
4350
- return fs9.readFileSync(file, "utf8");
5028
+ return fs11.readFileSync(file, "utf8");
4351
5029
  } catch {
4352
5030
  return null;
4353
5031
  }
4354
5032
  }
4355
- function git2(dir, args) {
4356
- const result = spawnSync3("git", args, {
5033
+ function git3(dir, args) {
5034
+ const result = spawnSync5("git", args, {
4357
5035
  cwd: dir,
4358
5036
  env: gitEnvironment(process.env),
4359
5037
  encoding: "utf8"
@@ -4361,20 +5039,20 @@ function git2(dir, args) {
4361
5039
  return result.status === 0 ? result.stdout.trim() : null;
4362
5040
  }
4363
5041
  function hooksOf(dir) {
4364
- const root = git2(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
4365
- const hooksDir = git2(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
5042
+ const root = git3(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
5043
+ const hooksDir = git3(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
4366
5044
  const candidates = [
4367
- ...["pre-push", "pre-commit"].map((name) => path9.join(root, ".husky", name)),
4368
- ...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path9.join(hooksDir, name)),
4369
- ...["lefthook.yml", ".lefthook.yml"].map((name) => path9.join(root, name))
5045
+ ...["pre-push", "pre-commit"].map((name) => path11.join(root, ".husky", name)),
5046
+ ...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path11.join(hooksDir, name)),
5047
+ ...["lefthook.yml", ".lefthook.yml"].map((name) => path11.join(root, name))
4370
5048
  ];
4371
5049
  let script = "";
4372
5050
  try {
4373
- const manifest = JSON.parse(read(path9.join(dir, "package.json")) ?? "{}");
5051
+ const manifest = JSON.parse(read(path11.join(dir, "package.json")) ?? "{}");
4374
5052
  script = manifest.scripts?.["test:gates:mutation"] ?? "";
4375
5053
  } catch {
4376
5054
  }
4377
- const subproject = path9.relative(fs9.realpathSync(root), dir).split(path9.sep).join("/");
5055
+ const subproject = path11.relative(fs11.realpathSync(root), dir).split(path11.sep).join("/");
4378
5056
  const seen = /* @__PURE__ */ new Set();
4379
5057
  const uses = [];
4380
5058
  for (const file of candidates) {
@@ -4382,12 +5060,12 @@ function hooksOf(dir) {
4382
5060
  if (text === null) {
4383
5061
  continue;
4384
5062
  }
4385
- const real = fs9.realpathSync(file);
5063
+ const real = fs11.realpathSync(file);
4386
5064
  if (seen.has(real)) {
4387
5065
  continue;
4388
5066
  }
4389
5067
  seen.add(real);
4390
- const shown = path9.relative(root, file).split(path9.sep).join("/");
5068
+ const shown = path11.relative(root, file).split(path11.sep).join("/");
4391
5069
  uses.push(...hookUses({ file: shown, text, subproject, script }));
4392
5070
  }
4393
5071
  return uses;
@@ -4404,8 +5082,8 @@ function runDoctor(dir, io2) {
4404
5082
  const affected = decideReuse(manifest, config, [], false, true);
4405
5083
  io2.out(`test-gates doctor: ${manifest.dir} (${config})`);
4406
5084
  if (plain2.snapshot !== null) {
4407
- const subproject = path9.relative(plain2.root, fs9.realpathSync(manifest.dir));
4408
- const prefix = subproject === "" ? "" : `${subproject.split(path9.sep).join("/")}/`;
5085
+ const subproject = path11.relative(plain2.root, fs11.realpathSync(manifest.dir));
5086
+ const prefix = subproject === "" ? "" : `${subproject.split(path11.sep).join("/")}/`;
4409
5087
  const keys = Object.keys(plain2.snapshot.files);
4410
5088
  const outside = keys.filter((key) => !key.startsWith(prefix)).length;
4411
5089
  const followed = plain2.scope === null ? "" : "files" in plain2.scope ? `, paths followed in the ${plain2.scope.files} files the run loads` : ", paths followed in every file";
@@ -4416,6 +5094,15 @@ function runDoctor(dir, io2) {
4416
5094
  io2.out(
4417
5095
  reuse === null ? " next run: reuses the results of the last run" : ` next run: every mutant, ${reuse}`
4418
5096
  );
5097
+ if (plain2.seed !== null) {
5098
+ io2.out(
5099
+ ` this worktree has no record of its own: the next run starts from the one of the main worktree (${plain2.mainWorktree})`
5100
+ );
5101
+ } else if (plain2.mainWorktree !== null) {
5102
+ io2.out(
5103
+ ` this worktree has no record of its own, and the main worktree (${plain2.mainWorktree}) has none this worktree can start from`
5104
+ );
5105
+ }
4419
5106
  if (affected.ci !== null) {
4420
5107
  io2.out(` next run with --affected: every mutant, ${affected.ci}`);
4421
5108
  } else if (affected.plan !== null && "rerun" in affected.plan) {
@@ -4432,15 +5119,20 @@ function runDoctor(dir, io2) {
4432
5119
  }
4433
5120
  }
4434
5121
  let blind = null;
5122
+ let blindGates = [];
4435
5123
  if (plain2.snapshot !== null) {
4436
5124
  try {
4437
- affectedInputs(manifest, {
5125
+ const inputs = affectedInputs(manifest, {
4438
5126
  root: plain2.root,
4439
5127
  files: plain2.snapshot.files,
4440
5128
  configs: plain2.configs(),
4441
5129
  tests: [],
4442
5130
  covering: {}
4443
5131
  });
5132
+ blindGates = manifest.gates.flatMap((gate) => {
5133
+ const reason = inputs.gates[gate.path]?.blind;
5134
+ return reason === void 0 ? [] : [{ gate: gate.path, reason }];
5135
+ });
4444
5136
  } catch (error) {
4445
5137
  if (!(error instanceof AffectedError)) {
4446
5138
  throw error;
@@ -4448,6 +5140,8 @@ function runDoctor(dir, io2) {
4448
5140
  blind = error.message;
4449
5141
  }
4450
5142
  }
5143
+ const left = sandboxPlan(manifest.dir, plain2.temp);
5144
+ const count = "kept" in left.plan ? left.plan.kept.length : left.plan.remove.length;
4451
5145
  const { lines, status } = formatFindings(
4452
5146
  diagnose({
4453
5147
  incremental: plain2.incremental,
@@ -4455,8 +5149,16 @@ function runDoctor(dir, io2) {
4455
5149
  failure: plain2.failure,
4456
5150
  widened: plain2.widened,
4457
5151
  scope: plain2.scope,
4458
- hooks: hooksOf(fs9.realpathSync(manifest.dir)),
4459
- blind
5152
+ hooks: hooksOf(fs11.realpathSync(manifest.dir)),
5153
+ blind,
5154
+ blindGates,
5155
+ gates: manifest.gates.length,
5156
+ sandboxes: count === 0 ? null : {
5157
+ tempDir: left.shown,
5158
+ count,
5159
+ kept: "kept" in left.plan ? left.plan.why : null,
5160
+ running: "kept" in left.plan ? left.plan.running : null
5161
+ }
4460
5162
  })
4461
5163
  );
4462
5164
  lines.forEach((line) => io2.out(line));
@@ -4464,8 +5166,8 @@ function runDoctor(dir, io2) {
4464
5166
  }
4465
5167
 
4466
5168
  // src/commands/lcov.ts
4467
- import fs10 from "fs";
4468
- import path10 from "path";
5169
+ import fs12 from "fs";
5170
+ import path12 from "path";
4469
5171
 
4470
5172
  // src/core/lcov.ts
4471
5173
  function normalizePath2(file, rootDir) {
@@ -4492,9 +5194,9 @@ function parseLcov(text, rootDir) {
4492
5194
  const line = rawLine.trim();
4493
5195
  if (line.startsWith("SF:")) {
4494
5196
  closeRecord();
4495
- const path13 = normalizePath2(line.slice(3), rootDir);
4496
- const file = files.get(path13) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
4497
- files.set(path13, file);
5197
+ const path15 = normalizePath2(line.slice(3), rootDir);
5198
+ const file = files.get(path15) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
5199
+ files.set(path15, file);
4498
5200
  record = { file, hasDa: false, found: 0, hit: 0 };
4499
5201
  } else if (line === "end_of_record") {
4500
5202
  closeRecord();
@@ -4578,10 +5280,10 @@ function runLcov(dir, fileOption, io2) {
4578
5280
  "no lcov file given. Pass --file <lcov.info> or set settings.lcov.file"
4579
5281
  );
4580
5282
  }
4581
- const lcovPath = path10.resolve(manifest.dir, file);
5283
+ const lcovPath = path12.resolve(manifest.dir, file);
4582
5284
  let text;
4583
5285
  try {
4584
- text = fs10.readFileSync(lcovPath, "utf8");
5286
+ text = fs12.readFileSync(lcovPath, "utf8");
4585
5287
  } catch (error) {
4586
5288
  io2.err(
4587
5289
  `test-gates lcov: cannot read ${file} (${error.message}). Run the tests with coverage first`
@@ -4624,10 +5326,10 @@ function runLcov(dir, fileOption, io2) {
4624
5326
  }
4625
5327
 
4626
5328
  // src/commands/selfcheck.ts
4627
- import { spawn, spawnSync as spawnSync4 } from "child_process";
4628
- import fs12 from "fs";
5329
+ import { spawn, spawnSync as spawnSync6 } from "child_process";
5330
+ import fs14 from "fs";
4629
5331
  import os2 from "os";
4630
- import path12 from "path";
5332
+ import path14 from "path";
4631
5333
 
4632
5334
  // src/core/jobs.ts
4633
5335
  var SWITCHES = {
@@ -4667,6 +5369,34 @@ function knownCommand(program, args) {
4667
5369
  function jobFlags(runner, dir) {
4668
5370
  return runner === "jest" ? [`--coverageDirectory=${dir}/coverage`, `--cacheDirectory=${dir}/cache`] : [`--coverage.reportsDirectory=${dir}/coverage`, "--no-cache"];
4669
5371
  }
5372
+ var LIMITS = {
5373
+ jest: ["--runInBand", "-i", "--maxWorkers", "-w"],
5374
+ vitest: ["--no-file-parallelism", "--fileParallelism", "--maxWorkers", "--minWorkers"]
5375
+ };
5376
+ var CONFIG_LIMITS = [
5377
+ "maxWorkers",
5378
+ "minWorkers",
5379
+ "maxThreads",
5380
+ "minThreads",
5381
+ "maxForks",
5382
+ "minForks",
5383
+ "fileParallelism",
5384
+ "singleThread",
5385
+ "singleFork"
5386
+ ];
5387
+ function workerFlags(input) {
5388
+ if (input.lines <= 1) {
5389
+ return [];
5390
+ }
5391
+ const given = input.args.some(
5392
+ (arg) => LIMITS[input.runner].includes(arg.split("=")[0])
5393
+ );
5394
+ const configured = CONFIG_LIMITS.some((word) => input.configSource?.includes(word) === true);
5395
+ if (given || configured) {
5396
+ return [];
5397
+ }
5398
+ return [`--maxWorkers=${Math.max(1, Math.floor(input.cores / input.lines))}`];
5399
+ }
4670
5400
  var REACHES = [
4671
5401
  "process.",
4672
5402
  "process?.",
@@ -4698,10 +5428,22 @@ var REACHES = [
4698
5428
  ];
4699
5429
  var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])|\bimport\.meta\.glob\b/;
4700
5430
  var COMPUTED = "loads a module whose name is computed";
5431
+ var STRING = /(^|[:,([={?+&|;])(\s*)(['"])((?:\\.|(?!\3)[^\\\n])*)\3(?=\s*(?:[,}\]);:+?&|]|$))/gm;
5432
+ function withoutStrings(source) {
5433
+ return source.replace(
5434
+ STRING,
5435
+ (all, before, space, quote, inside) => (
5436
+ // In a template (`{"home": "${home()}"}`) the quotes are text, and what is
5437
+ // between them may be code.
5438
+ inside.includes("${") ? all : `${before}${space}${quote}${" ".repeat(inside.length)}${quote}`
5439
+ )
5440
+ );
5441
+ }
4701
5442
  var RUNNER_PACKAGES = ["vitest", "@jest/globals"];
4702
5443
  function reachesOutside(file, trusted) {
4703
5444
  const dense = file.source.replace(/\s/g, "");
4704
- const mark = REACHES.find((text) => dense.includes(text));
5445
+ const code = withoutStrings(file.source).replace(/\s/g, "");
5446
+ const mark = REACHES.find((text) => code.includes(text));
4705
5447
  if (mark !== void 0) {
4706
5448
  return `uses ${mark.replace(/[.?[(:]+$/, "")}`;
4707
5449
  }
@@ -4844,8 +5586,8 @@ function cleanHold(input) {
4844
5586
 
4845
5587
  // src/touch.ts
4846
5588
  import { createHash as createHash2 } from "crypto";
4847
- import fs11 from "fs";
4848
- import path11 from "path";
5589
+ import fs13 from "fs";
5590
+ import path13 from "path";
4849
5591
  import { fileURLToPath } from "url";
4850
5592
  var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
4851
5593
  function isObject(value) {
@@ -4879,7 +5621,7 @@ function isCounters(value) {
4879
5621
  function readNotes(directory) {
4880
5622
  const read2 = (file) => {
4881
5623
  try {
4882
- return JSON.parse(fs11.readFileSync(path11.join(directory, file), "utf8"));
5624
+ return JSON.parse(fs13.readFileSync(path13.join(directory, file), "utf8"));
4883
5625
  } catch {
4884
5626
  return null;
4885
5627
  }
@@ -4887,8 +5629,8 @@ function readNotes(directory) {
4887
5629
  const run = read2("run.json");
4888
5630
  let notes = [];
4889
5631
  try {
4890
- for (const name of fs11.readdirSync(path11.join(directory, "specs"))) {
4891
- const note = read2(path11.join("specs", name));
5632
+ for (const name of fs13.readdirSync(path13.join(directory, "specs"))) {
5633
+ const note = read2(path13.join("specs", name));
4892
5634
  if (!isObject(note) || typeof note.spec !== "string" || !isObject(note.files) || !Object.values(note.files).every(isCounters)) {
4893
5635
  throw new Error("not a note");
4894
5636
  }
@@ -4900,7 +5642,7 @@ function readNotes(directory) {
4900
5642
  return {
4901
5643
  run: isObject(run) && typeof run.runner === "string" && typeof run.version === "string" && typeof run.provider === "string" && typeof run.pool === "string" && typeof run.isolate === "boolean" && typeof run.projects === "number" && (run.problem === null || typeof run.problem === "string") ? run : null,
4902
5644
  notes,
4903
- totals: countersOfAll(read2(path11.join("coverage", "coverage-final.json")))
5645
+ totals: countersOfAll(read2(path13.join("coverage", "coverage-final.json")))
4904
5646
  };
4905
5647
  }
4906
5648
 
@@ -4915,7 +5657,7 @@ function resolveGateCommand(manifest) {
4915
5657
  if (manifest.settings.gateCommand !== null) {
4916
5658
  return manifest.settings.gateCommand;
4917
5659
  }
4918
- const exists = (name) => fs12.existsSync(path12.join(manifest.dir, name));
5660
+ const exists = (name) => fs14.existsSync(path14.join(manifest.dir, name));
4919
5661
  const jest = JEST_CONFIGS.find(exists);
4920
5662
  const vitest = VITEST_CONFIGS.find(exists);
4921
5663
  if (jest && vitest) {
@@ -4936,7 +5678,7 @@ function resolveGateCommand(manifest) {
4936
5678
  var TOUCH_PREFIX = ".test-gates-touch-";
4937
5679
  function realPath(file) {
4938
5680
  try {
4939
- return fs12.realpathSync(file);
5681
+ return fs14.realpathSync(file);
4940
5682
  } catch {
4941
5683
  return file;
4942
5684
  }
@@ -4944,13 +5686,16 @@ function realPath(file) {
4944
5686
  function shareOneRun(manifest, targets, runGate) {
4945
5687
  let touchDir;
4946
5688
  try {
4947
- for (const name of fs12.readdirSync(manifest.dir)) {
4948
- const left = path12.join(manifest.dir, name);
4949
- if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
4950
- fs12.rmSync(left, { recursive: true, force: true });
5689
+ for (const name of fs14.readdirSync(manifest.dir)) {
5690
+ const left = path14.join(manifest.dir, name);
5691
+ try {
5692
+ if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
5693
+ fs14.rmSync(left, { recursive: true, force: true });
5694
+ }
5695
+ } catch {
4951
5696
  }
4952
5697
  }
4953
- touchDir = fs12.mkdtempSync(path12.join(manifest.dir, TOUCH_PREFIX));
5698
+ touchDir = fs14.mkdtempSync(path14.join(manifest.dir, TOUCH_PREFIX));
4954
5699
  } catch {
4955
5700
  return { reason: "the subproject cannot be written to" };
4956
5701
  }
@@ -4961,14 +5706,14 @@ function shareOneRun(manifest, targets, runGate) {
4961
5706
  return { reason: "the gate run with every spec does not pass" };
4962
5707
  }
4963
5708
  const base = realPath(manifest.dir);
4964
- const absolute = (file) => realPath(path12.resolve(base, file));
5709
+ const absolute = (file) => realPath(path14.resolve(base, file));
4965
5710
  const rekeyed = (files) => Object.fromEntries(Object.entries(files).map(([file, value]) => [absolute(file), value]));
4966
5711
  const left = readNotes(touchDir);
4967
5712
  plan = planControls({
4968
5713
  all: manifest.gates.map((gate) => ({
4969
5714
  path: gate.path,
4970
- file: path12.join(base, gate.path),
4971
- specFile: path12.join(base, gate.spec ?? "")
5715
+ file: path14.join(base, gate.path),
5716
+ specFile: path14.join(base, gate.spec ?? "")
4972
5717
  })),
4973
5718
  targets: targets.map((gate) => gate.path),
4974
5719
  run: left.run,
@@ -4977,7 +5722,7 @@ function shareOneRun(manifest, targets, runGate) {
4977
5722
  failurePattern: manifest.settings.selfcheckFailurePattern
4978
5723
  });
4979
5724
  } finally {
4980
- fs12.rmSync(touchDir, { recursive: true, force: true });
5725
+ fs14.rmSync(touchDir, { recursive: true, force: true });
4981
5726
  }
4982
5727
  if (plan.reason !== null) {
4983
5728
  return { reason: plan.reason };
@@ -4987,15 +5732,15 @@ function shareOneRun(manifest, targets, runGate) {
4987
5732
  return { gates, result: runGate({ excludeAll: gates.map((gate) => gate.spec) }) };
4988
5733
  }
4989
5734
  function installedVersion2(start, name) {
4990
- let current = path12.resolve(start);
5735
+ let current = path14.resolve(start);
4991
5736
  for (; ; ) {
4992
5737
  try {
4993
5738
  const manifest = JSON.parse(
4994
- fs12.readFileSync(path12.join(current, "node_modules", name, "package.json"), "utf8")
5739
+ fs14.readFileSync(path14.join(current, "node_modules", name, "package.json"), "utf8")
4995
5740
  );
4996
5741
  return typeof manifest.version === "string" ? manifest.version : null;
4997
5742
  } catch {
4998
- const parent = path12.dirname(current);
5743
+ const parent = path14.dirname(current);
4999
5744
  if (parent === current) {
5000
5745
  return null;
5001
5746
  }
@@ -5024,24 +5769,39 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
5024
5769
  const results = /* @__PURE__ */ new Map();
5025
5770
  let root;
5026
5771
  try {
5027
- for (const name of fs12.readdirSync(os2.tmpdir())) {
5028
- const left = path12.join(os2.tmpdir(), name);
5029
- if (name.startsWith(JOBS_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
5030
- fs12.rmSync(left, { recursive: true, force: true });
5772
+ root = fs14.realpathSync(fs14.mkdtempSync(path14.join(os2.tmpdir(), JOBS_PREFIX)));
5773
+ } catch {
5774
+ return results;
5775
+ }
5776
+ try {
5777
+ for (const name of fs14.readdirSync(os2.tmpdir())) {
5778
+ const left = path14.join(os2.tmpdir(), name);
5779
+ try {
5780
+ if (name.startsWith(JOBS_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
5781
+ fs14.rmSync(left, { recursive: true, force: true });
5782
+ }
5783
+ } catch {
5031
5784
  }
5032
5785
  }
5033
- root = fs12.realpathSync(fs12.mkdtempSync(path12.join(os2.tmpdir(), JOBS_PREFIX)));
5034
5786
  } catch {
5035
- return results;
5036
5787
  }
5037
5788
  const queue = [...gates];
5789
+ const lines = Math.min(jobs, gates.length);
5790
+ const workers = workerFlags({
5791
+ runner,
5792
+ args,
5793
+ configSource: gateConfigSource(manifest, args),
5794
+ lines,
5795
+ // (os.availableParallelism is in Node 18.14 and later.)
5796
+ cores: os2.availableParallelism?.() ?? os2.cpus().length
5797
+ });
5038
5798
  const line = async (dir) => {
5039
5799
  for (let gate = queue.shift(); gate !== void 0; gate = queue.shift()) {
5040
5800
  const spec = gate.spec;
5041
5801
  results.set(
5042
5802
  gate,
5043
5803
  await new Promise((resolve) => {
5044
- const child = spawn(executable, [...args, ...jobFlags(runner, dir)], {
5804
+ const child = spawn(executable, [...args, ...jobFlags(runner, dir), ...workers], {
5045
5805
  cwd: manifest.dir,
5046
5806
  env: { ...process.env, [EXCLUDE_ENV]: spec, [EXCLUDE_ALL_ENV]: "", [TOUCH_ENV]: "" },
5047
5807
  shell: process.platform === "win32"
@@ -5060,13 +5820,10 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
5060
5820
  };
5061
5821
  try {
5062
5822
  await Promise.all(
5063
- Array.from(
5064
- { length: Math.min(jobs, gates.length) },
5065
- (_unused, index) => line(path12.join(root, `line-${index}`))
5066
- )
5823
+ Array.from({ length: lines }, (_unused, index) => line(path14.join(root, `line-${index}`)))
5067
5824
  );
5068
5825
  } finally {
5069
- fs12.rmSync(root, { recursive: true, force: true });
5826
+ fs14.rmSync(root, { recursive: true, force: true });
5070
5827
  }
5071
5828
  return results;
5072
5829
  }
@@ -5095,7 +5852,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
5095
5852
  const command = commandOverride.length > 0 ? commandOverride : resolveGateCommand(manifest);
5096
5853
  const [program, ...args] = command;
5097
5854
  const executable = findLocalBin(manifest.dir, program) ?? program;
5098
- const runGate = (env) => spawnSync4(executable, args, {
5855
+ const runGate = (env) => spawnSync6(executable, args, {
5099
5856
  cwd: manifest.dir,
5100
5857
  env: {
5101
5858
  ...process.env,
@@ -5173,6 +5930,11 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
5173
5930
  ...manifest.gates.flatMap((gate) => [gate.path, gate.spec])
5174
5931
  ].sort((left, right) => right.length - left.length);
5175
5932
  const results = await runAtOnce(manifest, atOnce, jobs, known.runner, executable, args);
5933
+ if (results.size === 0) {
5934
+ io2.out(
5935
+ `test-gates selfcheck: one negative control at a time (no directory for the runs next to others could be made in ${os2.tmpdir()})`
5936
+ );
5937
+ }
5176
5938
  for (const [gate, result] of results) {
5177
5939
  const output = `${result.stdout}${result.stderr}`;
5178
5940
  const clean = cleanHold({
@@ -5190,7 +5952,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
5190
5952
  }
5191
5953
  }
5192
5954
  const again = atOnce.length - heldAtOnce.size;
5193
- if (again > 0) {
5955
+ if (again > 0 && results.size > 0) {
5194
5956
  io2.out(
5195
5957
  `test-gates selfcheck: ${again} of ${atOnce.length} negative control(s) did not hold cleanly next to others and run again on their own`
5196
5958
  );