supercov 0.0.43 → 0.0.44

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.
@@ -3,7 +3,7 @@
3
3
  Start with the run summary. It usually tells you whether the problem is the test
4
4
  command, source discovery, runner attribution, or a measurement boundary.
5
5
 
6
- ```sh
6
+ ```sh supercov
7
7
  npx supercov runs latest
8
8
  npx supercov runs latest scope
9
9
  npx supercov runs latest runners
@@ -28,13 +28,13 @@ would for another npm package.
28
28
  Run the same command from the repository root. If first-party code lives in an
29
29
  unusual directory, declare the source roots explicitly:
30
30
 
31
- ```sh
31
+ ```sh supercov
32
32
  SUPERCOV_SOURCE_ROOTS=src,app npx supercov -- npm test
33
33
  ```
34
34
 
35
35
  Then inspect what Supercov included and excluded:
36
36
 
37
- ```sh
37
+ ```sh supercov
38
38
  npx supercov runs latest scope
39
39
  ```
40
40
 
@@ -45,7 +45,7 @@ warning. The goal is an honest boundary around code the repository owns.
45
45
 
46
46
  First check runner and source scope:
47
47
 
48
- ```sh
48
+ ```sh supercov
49
49
  npx supercov runs latest runners
50
50
  npx supercov runs latest scope
51
51
  npx supercov runs latest gaps
@@ -67,7 +67,7 @@ file that cannot be compiled with them is measured through Ruby's `Coverage`
67
67
  module alone rather than failing the run. To put a file on that path
68
68
  deliberately, name a fragment of its path:
69
69
 
70
- ```sh
70
+ ```sh supercov
71
71
  SUPERCOV_RUBY_SKIP_PROBES=app/models/order.rb npx supercov -- bundle exec rspec
72
72
  ```
73
73
 
@@ -82,7 +82,7 @@ workspace after relevant source, tests, dependencies, configuration, or
82
82
  toolchain inputs change. Rerun the same complete command to create a current
83
83
  baseline:
84
84
 
85
- ```sh
85
+ ```sh supercov
86
86
  npx supercov -- npm test
87
87
  ```
88
88
 
@@ -133,7 +133,7 @@ mode still match.
133
133
 
134
134
  Inspect the recorded phases with:
135
135
 
136
- ```sh
136
+ ```sh supercov
137
137
  npx supercov runs latest
138
138
  ```
139
139
 
@@ -143,7 +143,7 @@ See [Speed and storage](performance.md) for practical ways to shorten a loop.
143
143
 
144
144
  Preview cleanup, then choose how much history to keep:
145
145
 
146
- ```sh
146
+ ```sh supercov
147
147
  npx supercov clean --dry-run
148
148
  npx supercov clean --keep 20
149
149
  npx supercov clean
@@ -16,7 +16,7 @@ Before merging, check three things:
16
16
  3. **The test protects behavior.** It should make a meaningful assertion without
17
17
  weakening existing assertions or changing application code for the metric.
18
18
 
19
- ```sh
19
+ ```sh supercov
20
20
  npx supercov diff <baseline-run> <new-run>
21
21
  npx supercov runs <new-run> test "new test name"
22
22
  ```
@@ -34,7 +34,7 @@ Supercov separates four states:
34
34
  - **stale** — the run is valid history but no longer describes the current
35
35
  workspace.
36
36
 
37
- ```sh
37
+ ```sh supercov
38
38
  npx supercov runs latest
39
39
  npx supercov runs latest scope
40
40
  npx supercov runs latest gaps
@@ -74,7 +74,7 @@ staging state. Completed runs remain immutable.
74
74
 
75
75
  Preview cleanup before removing anything:
76
76
 
77
- ```sh
77
+ ```sh supercov
78
78
  npx supercov clean --dry-run
79
79
  npx supercov clean --keep 20
80
80
  npx supercov clean
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "0.0.43",
3
+ "version": "0.0.44",
4
4
  "description": "Coverage for coding agents and software factories 🌙",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,7 +18,9 @@
18
18
  "docs",
19
19
  "analyzers/typescript/bin",
20
20
  "analyzers/typescript/dist/analyze.js",
21
+ "analyzers/typescript/dist/mock-counts.js",
21
22
  "analyzers/typescript/dist/archive.js",
23
+ "analyzers/typescript/dist/awaited-observations.js",
22
24
  "analyzers/typescript/dist/compiler.js",
23
25
  "analyzers/typescript/dist/frontend.js",
24
26
  "analyzers/typescript/dist/native-frontend.js",
@@ -92,14 +94,14 @@
92
94
  "test:rust-public-cargo": "cargo build -p supercov && node scripts/rust-public-cargo-integration.mjs"
93
95
  },
94
96
  "optionalDependencies": {
95
- "@supercov/cli-darwin-arm64": "0.0.43",
96
- "@supercov/cli-darwin-x64": "0.0.43",
97
- "@supercov/cli-linux-arm64-gnu": "0.0.43",
98
- "@supercov/cli-linux-arm64-musl": "0.0.43",
99
- "@supercov/cli-linux-x64-gnu": "0.0.43",
100
- "@supercov/cli-linux-x64-musl": "0.0.43",
101
- "@supercov/cli-win32-arm64": "0.0.43",
102
- "@supercov/cli-win32-x64": "0.0.43"
97
+ "@supercov/cli-darwin-arm64": "0.0.44",
98
+ "@supercov/cli-darwin-x64": "0.0.44",
99
+ "@supercov/cli-linux-arm64-gnu": "0.0.44",
100
+ "@supercov/cli-linux-arm64-musl": "0.0.44",
101
+ "@supercov/cli-linux-x64-gnu": "0.0.44",
102
+ "@supercov/cli-linux-x64-musl": "0.0.44",
103
+ "@supercov/cli-win32-arm64": "0.0.44",
104
+ "@supercov/cli-win32-x64": "0.0.44"
103
105
  },
104
106
  "peerDependencies": {
105
107
  "@playwright/test": ">=1.55.0",
@@ -1,4 +1,6 @@
1
1
  import { withNodeAssertionPhase } from "./runtime.mjs";
2
+ import { relative, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  const ASSERTION_METHODS = new Set([
3
5
  "deepEqual",
4
6
  "deepStrictEqual",
@@ -19,18 +21,40 @@ const ASSERTION_METHODS = new Set([
19
21
  "strictEqual",
20
22
  "throws",
21
23
  ]);
22
- function assertionSource() {
23
- const lines = new Error().stack?.split("\n").slice(2) ?? [];
24
- const entry = lines.find((line) => !/(?:nodeAssert|nodeAssertion|runtime)\.[cm]?[jt]s/.test(line) &&
25
- !line.includes("node:internal"));
26
- if (!entry)
24
+ function assertionSource(boundary) {
25
+ // Omit our own frames by function identity, not basename: a user's helper
26
+ // may itself be called runtime.mjs or nodeAssertAdapter.mjs. Never search
27
+ // deeper for a plausible-looking caller when the immediate frame is opaque.
28
+ // A loader may have transformed this file without composing source maps.
29
+ // Qualify stack coordinates so they cannot masquerade as the original
30
+ // source positions emitted by lexical probes and used for oracle matching.
31
+ try {
32
+ const error = {};
33
+ Error.captureStackTrace(error, boundary);
34
+ if (typeof error.stack !== "string")
35
+ return undefined;
36
+ const entry = error.stack.split("\n")[1]?.trim();
37
+ const match = entry && /(?:^at (?:async )?| \()((?:file:\/\/\/|\/|[A-Za-z]:[\\/]).*):(\d+):(\d+)\)?$/.exec(entry);
38
+ if (!match)
39
+ return undefined;
40
+ const file = match[1].startsWith("file:") ? fileURLToPath(match[1]) : match[1];
41
+ const roots = [process.env.SUPERCOV_PROJECT_ROOT, process.env.SUPERCOV_SOURCE_PROJECT_ROOT, process.cwd()].filter(Boolean);
42
+ for (const root of roots) {
43
+ const path = relative(root, file);
44
+ if (path !== ".." && !path.startsWith(`..${sep}`) && !/^(?:[A-Za-z]:|[\\/])/.test(path))
45
+ return `runtime-stack:${path.split(sep).join("/")}:${match[2]}:${match[3]}`;
46
+ }
47
+ return `runtime-stack:${file.split(sep).join("/")}:${match[2]}:${match[3]}`;
48
+ }
49
+ catch {
50
+ // Custom stack formatters must not change the user's assertion outcome.
27
51
  return undefined;
28
- return entry.trim().replace(/^at\s+/, "");
52
+ }
29
53
  }
30
54
  function wrapAssertion(original, operation) {
31
55
  return new Proxy(original, {
32
- apply(target, thisArgument, argumentsList) {
33
- return withNodeAssertionPhase(operation, assertionSource(), () => Reflect.apply(target, thisArgument, argumentsList));
56
+ apply: function assertionApply(target, thisArgument, argumentsList) {
57
+ return withNodeAssertionPhase(operation, () => assertionSource(assertionApply), () => Reflect.apply(target, thisArgument, argumentsList));
34
58
  },
35
59
  });
36
60
  }
@@ -2,7 +2,7 @@ import * as native from "node:test";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import vm from "node:vm";
4
4
  import { beginBufferedServerEvidence, flushBufferedServerEvidence, takeNodeAssertionPhases, withCoverageCarrier, } from "./runtime.mjs";
5
- import { callerLocation, runnerExecutionScope, writeRunnerEvidence, } from "./runnerEvidence.mjs";
5
+ import { callerLocation, runnerExecutionScope, runnerTestId, writeRunnerEvidence, } from "./runnerEvidence.mjs";
6
6
  function callbackIndex(args) {
7
7
  for (let index = args.length - 1; index >= 0; index -= 1)
8
8
  if (typeof args[index] === "function")
@@ -86,16 +86,18 @@ function restoreUserError(error, depth = 0) {
86
86
  }
87
87
  return error;
88
88
  }
89
- function wrappedRegistration(original) {
89
+ const registrationCounts = new Map();
90
+ function wrappedRegistration(original, parentTestId) {
90
91
  const wrapped = function supercovNodeTest(...args) {
91
92
  const index = callbackIndex(args);
92
93
  if (index < 0)
93
94
  return Reflect.apply(original, this, args);
94
95
  const callback = args[index];
95
- const location = callerLocation(/(?:nodeTest|runnerEvidence)\.[cm]?[jt]s/);
96
+ const location = callerLocation(supercovNodeTest);
96
97
  const identity = {
97
98
  runner: "node:test",
98
99
  name: testName(args, callback),
100
+ ...(parentTestId ? { parentTestId } : {}),
99
101
  ...location,
100
102
  // Source-map producers disagree about whether a call expression maps to
101
103
  // its first token or the first token on its source line. The line and
@@ -104,6 +106,12 @@ function wrappedRegistration(original) {
104
106
  // keeps one identity when esbuild, Babel, SWC, or TypeScript rewrites it.
105
107
  ...(location.line === undefined ? {} : { column: 1 }),
106
108
  };
109
+ // Allocate at registration, never callback completion: concurrent tests
110
+ // can finish in any order. Count each source/name within its parent.
111
+ const registrationKey = runnerTestId(identity);
112
+ const registrationOrdinal = registrationCounts.get(registrationKey) ?? 0;
113
+ registrationCounts.set(registrationKey, registrationOrdinal + 1);
114
+ identity.registrationOrdinal = registrationOrdinal;
107
115
  const scope = runnerExecutionScope(identity);
108
116
  const options = testOptions(args, index);
109
117
  const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
@@ -129,7 +137,7 @@ function wrappedRegistration(original) {
129
137
  };
130
138
  }
131
139
  if (property === "test" && typeof value === "function")
132
- return wrappedRegistration(value.bind(target));
140
+ return wrappedRegistration(value.bind(target), scope.testId);
133
141
  return typeof value === "function" ? value.bind(target) : value;
134
142
  },
135
143
  });
@@ -192,7 +200,7 @@ function wrappedRegistration(original) {
192
200
  Object.defineProperty(wrapped, property, {
193
201
  configurable: true,
194
202
  enumerable: true,
195
- value: wrappedRegistration(member),
203
+ value: wrappedRegistration(member, parentTestId),
196
204
  });
197
205
  }
198
206
  return wrapped;
@@ -27,13 +27,24 @@ function localFile(file) {
27
27
  return relative(process.cwd(), absolute).split(sep).join("/");
28
28
  }
29
29
  export function runnerTestId(identity) {
30
- const key = [
30
+ const parts = [
31
31
  identity.runner,
32
32
  localFile(identity.file) ?? "unknown",
33
33
  identity.line ?? 0,
34
34
  identity.column ?? 0,
35
35
  identity.name,
36
- ].join("\0");
36
+ ];
37
+ // Preserve existing top-level, uniquely registered test IDs. Nested tests
38
+ // and repeated registrations need more than a shared source/name identity.
39
+ if (identity.parentTestId)
40
+ parts.push("parent", identity.parentTestId);
41
+ if (identity.registrationOrdinal)
42
+ parts.push("registration", identity.registrationOrdinal);
43
+ // Titles can themselves contain separator text. Domain-separate and encode
44
+ // the extended identity structurally so it cannot alias a literal title.
45
+ const key = identity.parentTestId || identity.registrationOrdinal
46
+ ? JSON.stringify(["registration-v2", ...parts])
47
+ : parts.join("\0");
37
48
  return `${identity.runner}:${createHash("sha256").update(key).digest("hex").slice(0, 24)}`;
38
49
  }
39
50
  export function runnerExecutionScope(identity) {
@@ -50,24 +61,35 @@ export function runnerExecutionScope(identity) {
50
61
  testId,
51
62
  testKey,
52
63
  retry,
53
- attemptId: `${testKey}-${retry}`,
64
+ // Different processes/worker threads can execute the same registration.
65
+ // Their files must not collide even though the stable test ID is shared.
66
+ attemptId: `${testKey}-${retry}-${processInstanceToken()}`,
54
67
  };
55
68
  }
56
- export function callerLocation(ignored) {
57
- const lines = new Error().stack?.split("\n").slice(2) ?? [];
58
- for (const entry of lines) {
59
- if (ignored.test(entry) || entry.includes("node:internal"))
60
- continue;
61
- const match = /(?:\(|at\s+)(file:\/\/[^:)]+|(?:[A-Za-z]:)?[^():]+):(\d+):(\d+)\)?$/.exec(entry.trim());
69
+ export function callerLocation(boundary = callerLocation) {
70
+ // Omit the registration wrapper by identity. A user file may have the same
71
+ // basename as a runtime adapter. Parse the location suffix, not individual
72
+ // path segments: spaces, parentheses and Unicode are valid filenames.
73
+ try {
74
+ const error = {};
75
+ Error.captureStackTrace(error, boundary);
76
+ if (typeof error.stack !== "string")
77
+ return {};
78
+ const entry = error.stack.split("\n")[1]?.trim();
79
+ const match = entry && /(?:^at (?:async )?| \()((?:file:\/\/|\/|[A-Za-z]:[\\/]|\\\\).*):(\d+):(\d+)\)?$/.exec(entry);
62
80
  if (!match)
63
- continue;
81
+ return {};
64
82
  return {
65
83
  file: match[1],
66
84
  line: Number(match[2]),
67
85
  column: Number(match[3]),
68
86
  };
69
87
  }
70
- return {};
88
+ catch {
89
+ // Custom formatters must not prevent registration. Missing provenance
90
+ // is explicit, never replaced with an unrelated deeper caller.
91
+ return {};
92
+ }
71
93
  }
72
94
  export function readScopedServerEvidence(scope, evidencePath = serverEvidencePath(scope)) {
73
95
  try {
@@ -721,9 +721,11 @@ function withCoverageCarrier(carrier, callback) {
721
721
  function assertionPhaseState(scope) {
722
722
  const key = attemptKey(scope);
723
723
  const existing = state.assertionPhases.get(key);
724
- if (existing)
724
+ if (existing) {
725
+ existing.phaseIds ??= new Set(existing.phases.filter(phase => phase.kind === "assertion").map(phase => phase.id));
725
726
  return existing;
726
- const created = { counter: 0, phases: [] };
727
+ }
728
+ const created = { counter: 0, phases: [], phaseIds: new Set() };
727
729
  state.assertionPhases.set(key, created);
728
730
  return created;
729
731
  }
@@ -753,9 +755,13 @@ function withNodeAssertionPhase(operation, source, callback) {
753
755
  const scope = context.scope;
754
756
  if (!scope)
755
757
  return callback();
756
- const existing = context.phaseId ? assertionPhaseState(scope).phases.find((phase2) => phase2.id === context.phaseId && phase2.kind === "assertion") : void 0;
758
+ const existing = context.phaseId && assertionPhaseState(scope).phaseIds.has(context.phaseId);
757
759
  if (existing)
758
760
  return callback();
761
+ // The adapter's stack is only a fallback. Avoid formatting it when a lexical
762
+ // probe already supplied the active phase's exact original-source identity.
763
+ if (typeof source === "function")
764
+ source = source();
759
765
  const bridged = (_a8 = runtimeGlobal.__SUPERCOV_ASSERTION_PHASE_BRIDGE__) == null ? void 0 : _a8.call(runtimeGlobal, operation, source, callback);
760
766
  if (bridged == null ? void 0 : bridged.handled)
761
767
  return bridged.value;
@@ -768,6 +774,7 @@ function withNodeAssertionPhase(operation, source, callback) {
768
774
  startedAtMs: Date.now()
769
775
  });
770
776
  attempt.phases.push(phase);
777
+ attempt.phaseIds.add(phase.id);
771
778
  try {
772
779
  const result = withCoverageCarrier({ version: 1, scope, phaseId: phase.id }, callback);
773
780
  if (result && typeof result.then === "function")
@@ -785,6 +792,16 @@ function withNodeAssertionPhase(operation, source, callback) {
785
792
  throw cleanInstrumentationStack(error);
786
793
  }
787
794
  }
795
+ // Resolve the target exactly once, before argument evaluation, just like a
796
+ // native call reference. The closure carries only source identity and receiver;
797
+ // it never captures an assertion phase or mutable current-statement state across
798
+ // await. Rejected arguments therefore create no phase, and awaited producer work
799
+ // is not retroactively attributed to the assertion invocation.
800
+ function bindNodeAssertion(operation, source, receiver, method) {
801
+ const target = method === void 0 ? receiver : receiver[method];
802
+ const thisArgument = method === void 0 ? void 0 : receiver;
803
+ return (...args) => withNodeAssertionPhase(operation, source, () => Reflect.apply(target, thisArgument, args));
804
+ }
788
805
  function takeNodeAssertionPhases(scope) {
789
806
  var _a8, _b;
790
807
  const key = attemptKey(scope);
@@ -1293,6 +1310,7 @@ const directRuntimeApi = {
1293
1310
  selectionEnd,
1294
1311
  selectionRight,
1295
1312
  takeNodeAssertionPhases,
1313
+ bindNodeAssertion,
1296
1314
  testStatement,
1297
1315
  tryBegin,
1298
1316
  tryCatch,
@@ -1343,6 +1361,7 @@ export {
1343
1361
  selectionEnd,
1344
1362
  selectionRight,
1345
1363
  takeNodeAssertionPhases,
1364
+ bindNodeAssertion,
1346
1365
  testStatement,
1347
1366
  tryBegin,
1348
1367
  tryCatch,