supercov 2.0.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +19 -22
  2. package/docs/agent-loop.md +8 -13
  3. package/docs/assertions.md +161 -75
  4. package/docs/cli.md +111 -59
  5. package/docs/coverage-model.md +5 -6
  6. package/docs/evidence.md +0 -11
  7. package/docs/getting-started.md +1 -1
  8. package/docs/performance.md +6 -14
  9. package/docs/quality.md +1 -1
  10. package/docs/security.md +43 -12
  11. package/docs/supported-suites.md +44 -48
  12. package/docs/troubleshooting.md +12 -5
  13. package/docs/workspace-isolation.md +23 -2
  14. package/package.json +33 -21
  15. package/runtime/javascript/atomic.mjs +46 -2
  16. package/runtime/javascript/ava.mjs +82 -0
  17. package/runtime/javascript/bootstrap.cjs +43 -0
  18. package/runtime/javascript/jest.cjs +52 -15
  19. package/runtime/javascript/jest.config.mjs +9 -1
  20. package/runtime/javascript/jestReporter.mjs +5 -6
  21. package/runtime/javascript/launchSupervisor.mjs +127 -15
  22. package/runtime/javascript/mocha.mjs +180 -0
  23. package/runtime/javascript/nodeTest.mjs +57 -3
  24. package/runtime/javascript/playwright.mjs +241 -32
  25. package/runtime/javascript/provenance.mjs +1 -1
  26. package/runtime/javascript/register.mjs +290 -2
  27. package/runtime/javascript/resolve-loader.mjs +40 -1
  28. package/runtime/javascript/runnerEvidence.mjs +3 -5
  29. package/runtime/javascript/runtime.mjs +551 -61
  30. package/runtime/javascript/tap.mjs +130 -0
  31. package/runtime/javascript/transport.mjs +7 -2
  32. package/runtime/javascript/vitest.mjs +5 -8
  33. package/runtime/javascript/vitestReporter.mjs +3 -10
  34. package/docs/assertion-agent.md +0 -150
  35. package/docs/assertion-evidence.md +0 -103
  36. package/docs/assertion-maps.md +0 -316
  37. package/schemas/assertions.schema.json +0 -276
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import * as native from "node:test";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import vm from "node:vm";
@@ -86,6 +87,43 @@ function restoreUserError(error, depth = 0) {
86
87
  }
87
88
  return error;
88
89
  }
90
+ // The describe/suite blocks a test is registered in, outermost first. A test
91
+ // is named by them as Vitest, Jest and Playwright name theirs, so two tests
92
+ // with one name in different suites stay apart, and `tests affected --names`
93
+ // gives what --test-name-pattern matches. A store rather than a stack: an
94
+ // async suite registers tests after it awaits.
95
+ const suitePath = new AsyncLocalStorage();
96
+ function suiteName(args, callback) {
97
+ return typeof args[0] === "string" ? args[0] : callback.name || "<anonymous>";
98
+ }
99
+ function wrappedSuite(original) {
100
+ const wrapped = function supercovNodeSuite(...args) {
101
+ const index = callbackIndex(args);
102
+ if (index < 0)
103
+ return Reflect.apply(original, this, args);
104
+ const callback = args[index];
105
+ const path = [...(suitePath.getStore() ?? []), suiteName(args, callback)];
106
+ const next = [...args];
107
+ next[index] = function supercovNodeSuiteCallback(...callbackArgs) {
108
+ return suitePath.run(path, () => Reflect.apply(callback, this, callbackArgs));
109
+ };
110
+ // node:test reports a suite at its direct caller, as it does a test.
111
+ const registration = registrationAt(callerLocation(supercovNodeSuite));
112
+ if (registration)
113
+ return registration(this === undefined ? original : original.bind(this), next);
114
+ return Reflect.apply(original, this, next);
115
+ };
116
+ for (const property of ["skip", "todo", "only"]) {
117
+ const member = original[property];
118
+ if (typeof member === "function")
119
+ Object.defineProperty(wrapped, property, {
120
+ configurable: true,
121
+ enumerable: true,
122
+ value: wrappedSuite(member),
123
+ });
124
+ }
125
+ return wrapped;
126
+ }
89
127
  const registrationCounts = new Map();
90
128
  function wrappedRegistration(original, parentTestId, forcedStatus) {
91
129
  const wrapped = function supercovNodeTest(...args) {
@@ -96,7 +134,9 @@ function wrappedRegistration(original, parentTestId, forcedStatus) {
96
134
  const location = callerLocation(supercovNodeTest);
97
135
  const identity = {
98
136
  runner: "node:test",
99
- name: testName(args, callback),
137
+ name: parentTestId
138
+ ? testName(args, callback)
139
+ : [...(suitePath.getStore() ?? []), testName(args, callback)].join(" > "),
100
140
  ...(parentTestId ? { parentTestId } : {}),
101
141
  ...location,
102
142
  // Source-map producers disagree about whether a call expression maps to
@@ -260,8 +300,8 @@ function restoringHook(original, scope, hookName = "hook") {
260
300
  }
261
301
  export const test = wrappedRegistration(native.test);
262
302
  export const it = wrappedRegistration(native.it);
263
- export const suite = native.suite;
264
- export const describe = native.describe;
303
+ export const suite = wrappedSuite(native.suite);
304
+ export const describe = wrappedSuite(native.describe);
265
305
  export const before = restoringHook(native.before, undefined, "before");
266
306
  export const after = restoringHook(native.after, undefined, "after");
267
307
  export const beforeEach = restoringHook(native.beforeEach, undefined, "beforeEach");
@@ -270,4 +310,18 @@ export const mock = native.mock;
270
310
  export const snapshot = native.snapshot;
271
311
  export const run = native.run;
272
312
  export const assert = native.assert;
313
+ // node:test hangs its whole API off `test` (test.mock, test.describe,
314
+ // test.expectFailure, test.getTestContext...). What the adapter does not wrap
315
+ // stays the runtime's own.
316
+ const adapted = { it, describe, suite, before, after, beforeEach, afterEach, test };
317
+ for (const property of Object.keys(native.test)) {
318
+ if (property in test && !(property in adapted))
319
+ continue;
320
+ Object.defineProperty(test, property, {
321
+ configurable: true,
322
+ enumerable: true,
323
+ writable: true,
324
+ value: adapted[property] ?? native.test[property],
325
+ });
326
+ }
273
327
  export default test;
@@ -31,6 +31,35 @@ const targetTestExport = process.env["SUPERCOV_PLAYWRIGHT_TEST_EXPORT"] ??
31
31
  const adapter = (targetModule === "@playwright/test"
32
32
  ? standardPlaywright
33
33
  : await import(__rewriteRelativeImportExtension(targetModule)));
34
+ // A page that loads the application as plain `<script>` files, from a static
35
+ // server, has no bundler to import the runtime the way a module graph would,
36
+ // and instrumented classic scripts read it from a global. Install the same
37
+ // self-contained runtime in every document before any of the page's scripts
38
+ // run. Every copy keys its state on the page's global, so this copy and one a
39
+ // bundler imports record into the same place. Its locals stay inside a
40
+ // function, out of the page's global scope.
41
+ const browserRuntimeSource = readFileSync(new URL("./runtime.mjs", import.meta.url), "utf8");
42
+ const browserRuntimeExport = browserRuntimeSource.lastIndexOf("\nexport {");
43
+ if (/^import\s/m.test(browserRuntimeSource) || browserRuntimeExport < 0)
44
+ throw new Error("Supercov's browser runtime must stay self-contained");
45
+ const browserRuntimePrelude = `(() => { if (globalThis.__SUPERCOV_DIRECT_RUNTIME__) return;\n${browserRuntimeSource.slice(0, browserRuntimeExport)}\n})();`;
46
+ // A worker blocked in `Atomics.wait` runs no evaluation until the page wakes
47
+ // it, and the page action that wakes it comes after Supercov's own call: an
48
+ // awaited evaluation deadlocked Actual's suite. Every worker evaluation is
49
+ // bounded; one that does not answer in time is left queued, and a test whose
50
+ // worker evidence could not be read is recorded as partial, never as complete.
51
+ const WORKER_ACTIVATION_WAIT_MS = 250;
52
+ const WORKER_SNAPSHOT_WAIT_MS = 5000;
53
+ const WORKER_TIMED_OUT = Symbol("worker did not answer");
54
+ function bounded(promise, milliseconds) {
55
+ let timer;
56
+ return Promise.race([
57
+ promise,
58
+ new Promise((resolve) => {
59
+ timer = setTimeout(() => resolve(WORKER_TIMED_OUT), milliseconds);
60
+ }),
61
+ ]).finally(() => clearTimeout(timer));
62
+ }
34
63
  const base = (adapter[targetTestExport] ?? adapter.test);
35
64
  const baseExpect = adapter.expect;
36
65
  const GENERATED_EVIDENCE_DIRECTORY = "__SUPERCOV_EVIDENCE_DIRECTORY__";
@@ -187,6 +216,21 @@ class CoveragePhaseController {
187
216
  // closed page cannot be evaluated, so the evidence is taken on the way out.
188
217
  earlySnapshots = [];
189
218
  snapshottedPages = new WeakSet();
219
+ snapshottedWorkers = new WeakSet();
220
+ // Workers that did not answer an evaluation in time. They are not waited
221
+ // for again until one answers.
222
+ unresponsiveWorkers = new WeakSet();
223
+ // Each worker's token, under which it pushes its evidence to its pages
224
+ // before it blocks in Atomics.wait (see runtime.mjs).
225
+ workerTokens = new WeakMap();
226
+ // Workers that answered their registration without Supercov's runtime: a
227
+ // worker built from a string (a `blob:` URL), or from a script nobody
228
+ // instrumented. There is no phase to set in them and no evidence to read,
229
+ // and Actual's blocked ones were most of what each step waited for.
230
+ workersWithoutRuntime = new WeakSet();
231
+ // Set when a worker's evidence could not be read: the test's coverage is
232
+ // then a lower bound.
233
+ workerEvidenceMissing = false;
190
234
  disposed = false;
191
235
  // Parameter properties are stateful despite the base ESLint rule treating
192
236
  // this as an empty constructor.
@@ -216,21 +260,79 @@ class CoveragePhaseController {
216
260
  snapshots.push(...(await this.snapshotPage(page)));
217
261
  }
218
262
  for (const worker of this.allWorkers()) {
219
- const snapshot = await worker
220
- .evaluate(() => {
221
- const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
222
- return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
223
- })
224
- .catch(() => ({
225
- decisions: [],
226
- hits: [],
227
- events: [],
228
- }));
229
- snapshots.push(snapshot);
263
+ if (this.snapshottedWorkers.has(worker))
264
+ continue;
265
+ const snapshot = await this.snapshotWorker(worker, this.allPages());
266
+ if (snapshot)
267
+ snapshots.push(snapshot);
230
268
  }
231
269
  this.runtimeSnapshots = snapshots;
232
270
  return snapshots;
233
271
  }
272
+ /**
273
+ * A worker's evidence, or nothing when it cannot be read. A worker that
274
+ * closed with its page has nothing left to read; one that did not answer
275
+ * in time is still running and its evidence is missing from this test.
276
+ */
277
+ async snapshotWorker(worker, pages) {
278
+ if (this.workersWithoutRuntime.has(worker)) {
279
+ // Asked once more, without waiting: it may have loaded an
280
+ // instrumented script since it was registered.
281
+ const answer = await bounded(worker
282
+ .evaluate(() => globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__?.())
283
+ .catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
284
+ if (answer && answer !== WORKER_TIMED_OUT) {
285
+ this.snapshottedWorkers.add(worker);
286
+ return answer;
287
+ }
288
+ return undefined;
289
+ }
290
+ const answer = await bounded(worker
291
+ .evaluate(() => {
292
+ const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
293
+ return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
294
+ })
295
+ .catch(() => undefined), WORKER_SNAPSHOT_WAIT_MS);
296
+ if (answer === WORKER_TIMED_OUT) {
297
+ this.unresponsiveWorkers.add(worker);
298
+ // Blocked in Atomics.wait, it pushed what it recorded before it
299
+ // blocked; that is all of it while the push is current.
300
+ const pushed = await this.pushedWorkerEvidence(worker, pages);
301
+ if (pushed?.current) {
302
+ this.snapshottedWorkers.add(worker);
303
+ return pushed.snapshot;
304
+ }
305
+ this.workerEvidenceMissing = true;
306
+ return pushed?.snapshot;
307
+ }
308
+ this.snapshottedWorkers.add(worker);
309
+ return answer;
310
+ }
311
+ /**
312
+ * A worker's latest push, found by the token it answered with when it was
313
+ * registered, or else by its URL when no other worker of the page pushed
314
+ * from the same one: a worker that blocks on its first message may never
315
+ * have answered at all.
316
+ */
317
+ async pushedWorkerEvidence(worker, pages) {
318
+ const token = this.workerTokens.get(worker);
319
+ const url = worker.url();
320
+ for (const page of pages) {
321
+ const pushed = await bounded(page
322
+ .evaluate(({ token, url }) => {
323
+ const entries = globalThis.__SUPERCOV_WORKER_EVIDENCE__?.() ?? [];
324
+ const byToken = token ? entries.find((entry) => entry.token === token) : undefined;
325
+ if (byToken)
326
+ return byToken;
327
+ const byUrl = entries.filter((entry) => entry.url === url);
328
+ return byUrl.length === 1 ? byUrl[0] : undefined;
329
+ }, { token, url })
330
+ .catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
331
+ if (pushed && pushed !== WORKER_TIMED_OUT)
332
+ return pushed;
333
+ }
334
+ return undefined;
335
+ }
234
336
  async snapshotPage(page) {
235
337
  const snapshots = [];
236
338
  for (const frame of page.frames()) {
@@ -253,6 +355,16 @@ class CoveragePhaseController {
253
355
  return;
254
356
  this.snapshottedPages.add(page);
255
357
  this.earlySnapshots.push(...(await this.snapshotPage(page)));
358
+ // A dedicated worker ends with the page that owns it, so it is read
359
+ // now; read after the page closed, a worker that ran the test's code
360
+ // reported nothing.
361
+ for (const worker of page.workers()) {
362
+ if (this.snapshottedWorkers.has(worker))
363
+ continue;
364
+ const snapshot = await this.snapshotWorker(worker, [page]);
365
+ if (snapshot)
366
+ this.earlySnapshots.push(snapshot);
367
+ }
256
368
  }
257
369
  /**
258
370
  * Register every live context the process created outside the wrapped
@@ -299,6 +411,7 @@ class CoveragePhaseController {
299
411
  this.contextConfiguredHeaders.set(context, configuredHeaders);
300
412
  if (adopted)
301
413
  this.adoptedContexts.add(context);
414
+ await context.addInitScript({ content: browserRuntimePrelude }).catch(() => undefined);
302
415
  // A context that outlives its test (a worker-scoped browser fixture)
303
416
  // is registered again by every later test, so this script accumulates
304
417
  // on it and each new document runs every copy in registration order.
@@ -359,15 +472,19 @@ class CoveragePhaseController {
359
472
  return;
360
473
  this.workers.add(worker);
361
474
  const phaseId = this.requestPhaseId();
362
- await worker
475
+ const registration = await worker
363
476
  .evaluate(({ attemptId, scopeHeader, scopeValue, phaseHeader, phase }) => {
364
477
  globalThis.__SUPERCOV_MCDC_TEST_ID__ = attemptId;
365
478
  if (phase)
366
479
  globalThis.__SUPERCOV_PHASE_ID__ = phase;
367
480
  globalThis.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(attemptId, phase);
481
+ const answer = {
482
+ token: globalThis.__SUPERCOV_WORKER_TOKEN__,
483
+ runtime: typeof globalThis.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__ === "function",
484
+ };
368
485
  const originalFetch = globalThis.fetch?.bind(globalThis);
369
486
  if (!originalFetch)
370
- return;
487
+ return answer;
371
488
  globalThis.fetch = ((input, init) => {
372
489
  const headers = new Headers(init?.headers ??
373
490
  (input instanceof Request ? input.headers : undefined));
@@ -376,6 +493,7 @@ class CoveragePhaseController {
376
493
  headers.set(phaseHeader, phase);
377
494
  return originalFetch(input, { ...init, headers });
378
495
  });
496
+ return answer;
379
497
  }, {
380
498
  attemptId: this.scope.attemptId,
381
499
  scopeHeader: COVERAGE_SCOPE_HEADER,
@@ -384,6 +502,10 @@ class CoveragePhaseController {
384
502
  phase: phaseId,
385
503
  })
386
504
  .catch(() => undefined);
505
+ if (typeof registration?.token === "string")
506
+ this.workerTokens.set(worker, registration.token);
507
+ if (registration?.runtime === false)
508
+ this.workersWithoutRuntime.add(worker);
387
509
  }
388
510
  async beginAction(operation) {
389
511
  timingCount("beginAction");
@@ -565,10 +687,25 @@ class CoveragePhaseController {
565
687
  };
566
688
  return scoped;
567
689
  }
690
+ /**
691
+ * Tell every context, frame, worker and future document of the test about
692
+ * a new phase. The four go to different targets and none waits on another,
693
+ * so they run together: an action waits for the slowest instead of their
694
+ * sum, which was most of what Actual's suite spent under Supercov (47 of
695
+ * the 43 extra seconds, with the database worker's bounded wait the
696
+ * largest part).
697
+ */
568
698
  async activateInBrowser(phaseId) {
569
- await timed("activate.contextHeaders", () => Promise.all([...this.contexts]
570
- .filter((context) => !this.adoptedContexts.has(context))
571
- .map((context) => this.updateContextHeaders(context, phaseId))));
699
+ await Promise.all([
700
+ timed("activate.contextHeaders", () => Promise.all([...this.contexts]
701
+ .filter((context) => !this.adoptedContexts.has(context))
702
+ .map((context) => this.updateContextHeaders(context, phaseId)))),
703
+ this.activateFrames(phaseId),
704
+ this.activateWorkers(phaseId),
705
+ timed("activate.pageScript", () => Promise.all([...this.pages].map((page) => this.activatePage(page, phaseId)))),
706
+ ]);
707
+ }
708
+ async activateFrames(phaseId) {
572
709
  await timed("activate.frameEvaluate", () => Promise.all([...this.pages].flatMap((page) => page.frames()).map((frame) => frame
573
710
  .evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie }) => {
574
711
  globalThis.__SUPERCOV_PHASE_ID__ = id;
@@ -595,14 +732,25 @@ class CoveragePhaseController {
595
732
  phaseCookie: COVERAGE_PHASE_COOKIE,
596
733
  })
597
734
  .catch(() => undefined))));
598
- await timed("activate.workerEvaluate", () => Promise.all([...this.workers].map((worker) => worker
599
- .evaluate((id) => {
600
- const coverageGlobal = globalThis;
601
- coverageGlobal.__SUPERCOV_PHASE_ID__ = id;
602
- coverageGlobal.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ ?? "unscoped", id);
603
- }, phaseId)
604
- .catch(() => undefined))));
605
- await timed("activate.pageScript", () => Promise.all([...this.pages].map((page) => this.activatePage(page, phaseId))));
735
+ }
736
+ async activateWorkers(phaseId) {
737
+ await timed("activate.workerEvaluate", () => Promise.all([...this.workers].map(async (worker) => {
738
+ const activation = worker
739
+ .evaluate((id) => {
740
+ const coverageGlobal = globalThis;
741
+ coverageGlobal.__SUPERCOV_PHASE_ID__ = id;
742
+ coverageGlobal.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ ?? "unscoped", id);
743
+ }, phaseId)
744
+ .then(() => this.unresponsiveWorkers.delete(worker))
745
+ .catch(() => undefined);
746
+ // Evaluations run in order, so one left queued still applies this
747
+ // phase before any later one; it is only not waited for. One with
748
+ // no runtime has nothing to apply unless it loads some later.
749
+ if (this.unresponsiveWorkers.has(worker) || this.workersWithoutRuntime.has(worker))
750
+ return;
751
+ if ((await bounded(activation, WORKER_ACTIVATION_WAIT_MS)) === WORKER_TIMED_OUT)
752
+ this.unresponsiveWorkers.add(worker);
753
+ })));
606
754
  }
607
755
  async updateContextHeaders(context, phaseId) {
608
756
  await context
@@ -804,6 +952,30 @@ globalThis.__SUPERCOV_ASSERTION_PHASE_BRIDGE__ = (operation, source, callback) =
804
952
  throw error;
805
953
  }
806
954
  };
955
+ // The same bridge for an assertion that runs in place: the runtime enters the
956
+ // carrier and calls release when the call returns, finish when it settles.
957
+ globalThis.__SUPERCOV_ASSERTION_PHASE_OPENER__ = (operation, source) => {
958
+ const controller = activeController;
959
+ if (!controller || !directRuntime())
960
+ return undefined;
961
+ const phase = controller.beginAssertion(operation, source);
962
+ bridgedAssertionDepth += 1;
963
+ let held = true;
964
+ const release = () => {
965
+ if (held) {
966
+ held = false;
967
+ bridgedAssertionDepth -= 1;
968
+ }
969
+ };
970
+ return {
971
+ carrier: { version: 1, scope: controller.scope, phaseId: phase.id },
972
+ release,
973
+ finish: (error) => {
974
+ release();
975
+ controller.finish(phase, error);
976
+ },
977
+ };
978
+ };
807
979
  function activeCoverageHeaders() {
808
980
  const controller = activeController;
809
981
  if (!controller)
@@ -890,9 +1062,10 @@ function scopedChildOptions(args, optionIndex) {
890
1062
  : {};
891
1063
  const options = {
892
1064
  ...existing,
1065
+ // An env the caller passed is the child's whole environment; only
1066
+ // Supercov's own variables join it. Without one, it inherits ours.
893
1067
  env: {
894
- ...process.env,
895
- ...(existing.env ?? {}),
1068
+ ...(existing.env ?? process.env),
896
1069
  SUPERCOV_RUN_ID: controller.scope.runId,
897
1070
  [COVERAGE_CARRIER_ENV]: encodeCoverageCarrier({
898
1071
  version: 1,
@@ -909,13 +1082,48 @@ function scopedChildOptions(args, optionIndex) {
909
1082
  scoped[optionIndex] = options;
910
1083
  return scoped;
911
1084
  }
1085
+ // spawn, fork and execFile take (file, args?, options?, callback?): options
1086
+ // follow an argument list, or an explicit null or undefined in its place.
912
1087
  function childOptionIndex(method, args) {
913
- if (method === "spawn" || method === "spawnSync" || method === "fork")
914
- return Array.isArray(args[1]) ? 2 : 1;
915
- if (method === "execFile" || method === "execFileSync")
916
- return Array.isArray(args[1]) ? 2 : 1;
1088
+ if (method === "spawn" ||
1089
+ method === "spawnSync" ||
1090
+ method === "fork" ||
1091
+ method === "execFile" ||
1092
+ method === "execFileSync")
1093
+ return Array.isArray(args[1]) || (args[1] == null && args.length > 2) ? 2 : 1;
917
1094
  return 1;
918
1095
  }
1096
+ // util.promisify(exec) and util.promisify(execFile) resolve { stdout, stderr }
1097
+ // through a function Node keeps on the original, and that function calls the
1098
+ // original. A wrapper carries its own, which calls the wrapper, or a promisified
1099
+ // call resolves the bare stdout string and skips the wrapper.
1100
+ const promisifyCustom = Symbol.for("nodejs.util.promisify.custom");
1101
+ function keepPromisifiedShape(original, wrapper) {
1102
+ if (typeof original[promisifyCustom] !== "function")
1103
+ return wrapper;
1104
+ Object.defineProperty(wrapper, promisifyCustom, {
1105
+ configurable: true,
1106
+ value: (...args) => {
1107
+ let settle;
1108
+ let reject;
1109
+ const promise = new Promise((ok, fail) => {
1110
+ settle = ok;
1111
+ reject = fail;
1112
+ });
1113
+ promise.child = wrapper(...args, (error, stdout, stderr) => {
1114
+ if (error !== null) {
1115
+ error.stdout = stdout;
1116
+ error.stderr = stderr;
1117
+ reject(error);
1118
+ }
1119
+ else
1120
+ settle({ stdout, stderr });
1121
+ });
1122
+ return promise;
1123
+ },
1124
+ });
1125
+ return wrapper;
1126
+ }
919
1127
  function installChildProcessScopePropagation() {
920
1128
  for (const method of [
921
1129
  "exec",
@@ -927,9 +1135,9 @@ function installChildProcessScopePropagation() {
927
1135
  "spawnSync",
928
1136
  ]) {
929
1137
  const original = childProcess[method];
930
- childProcess[method] = function (...args) {
1138
+ childProcess[method] = keepPromisifiedShape(original, function (...args) {
931
1139
  return Reflect.apply(original, childProcess, scopedChildOptions(args, childOptionIndex(method, args)));
932
- };
1140
+ });
933
1141
  }
934
1142
  syncBuiltinESMExports();
935
1143
  }
@@ -1154,6 +1362,7 @@ const instrumentedFixtures = {
1154
1362
  explicitKind: process.env["SUPERCOV_TEST_KIND"],
1155
1363
  }),
1156
1364
  phases: mergeCollectedPhases(controller.phases, directRuntime()?.takeNodeAssertionPhases(scope) ?? []),
1365
+ ...(controller.workerEvidenceMissing ? { attribution: "partial" } : {}),
1157
1366
  browser,
1158
1367
  server,
1159
1368
  };
@@ -100,7 +100,7 @@ export function inferTestProvenance({ runner, file, project, explicitKind, }) {
100
100
  source: "path",
101
101
  };
102
102
  }
103
- const defaultKind = runner === "vitest" || runner === "jest" || runner === "node:test"
103
+ const defaultKind = runner === "vitest" || runner === "jest" || runner === "node:test" || runner === "mocha" || runner === "ava" || runner === "tap"
104
104
  ? "unit"
105
105
  : "unknown";
106
106
  return {