supercov 0.0.42 → 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.
Files changed (47) hide show
  1. package/README.md +23 -3
  2. package/analyzers/typescript/README.md +59 -0
  3. package/analyzers/typescript/bin/compiler-identity.mjs +78 -0
  4. package/analyzers/typescript/bin/identity.mjs +71 -0
  5. package/analyzers/typescript/bin/query.mjs +29 -0
  6. package/analyzers/typescript/dist/analyze.js +5273 -0
  7. package/analyzers/typescript/dist/archive.js +337 -0
  8. package/analyzers/typescript/dist/awaited-observations.js +376 -0
  9. package/analyzers/typescript/dist/build-identity.json +1 -0
  10. package/analyzers/typescript/dist/compiler.js +32 -0
  11. package/analyzers/typescript/dist/frontend.js +75 -0
  12. package/analyzers/typescript/dist/mock-counts.js +2517 -0
  13. package/analyzers/typescript/dist/native-frontend.js +271 -0
  14. package/analyzers/typescript/dist/pragmas.js +186 -0
  15. package/analyzers/typescript/dist/types.js +1 -0
  16. package/analyzers/typescript/package.json +27 -0
  17. package/analyzers/typescript/src/analyze.ts +6180 -0
  18. package/analyzers/typescript/src/archive.ts +471 -0
  19. package/analyzers/typescript/src/awaited-observations.ts +561 -0
  20. package/analyzers/typescript/src/compiler.ts +49 -0
  21. package/analyzers/typescript/src/frontend.ts +136 -0
  22. package/analyzers/typescript/src/mock-counts.ts +3219 -0
  23. package/analyzers/typescript/src/native-frontend.ts +315 -0
  24. package/analyzers/typescript/src/pragmas.ts +284 -0
  25. package/analyzers/typescript/src/types.ts +45 -0
  26. package/analyzers/typescript/tsconfig.json +12 -0
  27. package/docs/agent-loop.md +129 -31
  28. package/docs/assertion-evidence.md +694 -0
  29. package/docs/cli.md +25 -15
  30. package/docs/code-verification.md +4 -0
  31. package/docs/coverage-model.md +6 -6
  32. package/docs/evidence.md +6 -6
  33. package/docs/getting-started.md +60 -72
  34. package/docs/performance.md +4 -4
  35. package/docs/supported-suites.md +40 -9
  36. package/docs/troubleshooting.md +8 -8
  37. package/docs/verification.md +14 -2
  38. package/docs/workspace-isolation.md +1 -1
  39. package/package.json +35 -15
  40. package/runtime/javascript/jest.cjs +134 -0
  41. package/runtime/javascript/jest.config.mjs +39 -0
  42. package/runtime/javascript/jestReporter.mjs +77 -0
  43. package/runtime/javascript/nodeAssertAdapter.mjs +32 -8
  44. package/runtime/javascript/nodeTest.mjs +13 -5
  45. package/runtime/javascript/register.mjs +22 -4
  46. package/runtime/javascript/runnerEvidence.mjs +33 -11
  47. package/runtime/javascript/runtime.mjs +73 -16
@@ -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
@@ -78,6 +78,18 @@ self-consistent calculation error does not pass unnoticed. Python and Ruby
78
78
  gates run real suites through their supported runners and assert the resulting
79
79
  coverage totals, test identity, and measurement limits.
80
80
 
81
+ Rust numbers are also checked against an independent LLVM line-coverage
82
+ oracle over real crates. `npm run oracle:rust` runs the same tests plain, under
83
+ the oracle, and under Supercov on twenty-six crates from bytes to tokio,
84
+ compares line coverage file by file, and fails if Supercov ran any suite
85
+ differently from plain Cargo. The two tools count differently — the oracle
86
+ counts every executable region after monomorphisation, Supercov the lines its
87
+ own obligations sit on — so the per-file comparison is read by a person: a file
88
+ at 0% in one and 70% in the other means lost evidence or a wrong denominator,
89
+ and this check found five such defects before 0.0.42. It runs locally and
90
+ costs no hosted minutes; `scripts/rust-coverage-oracle.mjs` documents what
91
+ each flag means.
92
+
81
93
  These checks reduce risk; they do not replace reviewing the assertions and
82
94
  behavior protected by a new test.
83
95
 
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "0.0.42",
4
- "description": "Zero-edit, runner-aware coverage completeness for JavaScript, TypeScript, Rust, Python, and Ruby test suites",
3
+ "version": "0.0.44",
4
+ "description": "Coverage for coding agents and software factories 🌙",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -16,6 +16,21 @@
16
16
  "bin",
17
17
  "runtime/javascript",
18
18
  "docs",
19
+ "analyzers/typescript/bin",
20
+ "analyzers/typescript/dist/analyze.js",
21
+ "analyzers/typescript/dist/mock-counts.js",
22
+ "analyzers/typescript/dist/archive.js",
23
+ "analyzers/typescript/dist/awaited-observations.js",
24
+ "analyzers/typescript/dist/compiler.js",
25
+ "analyzers/typescript/dist/frontend.js",
26
+ "analyzers/typescript/dist/native-frontend.js",
27
+ "analyzers/typescript/dist/pragmas.js",
28
+ "analyzers/typescript/dist/types.js",
29
+ "analyzers/typescript/dist/build-identity.json",
30
+ "analyzers/typescript/src",
31
+ "analyzers/typescript/package.json",
32
+ "analyzers/typescript/tsconfig.json",
33
+ "analyzers/typescript/README.md",
19
34
  "README.md"
20
35
  ],
21
36
  "engines": {
@@ -34,6 +49,7 @@
34
49
  },
35
50
  "scripts": {
36
51
  "build": "cargo build -p supercov",
52
+ "build:asserted-typescript": "npm --prefix analyzers/typescript run build",
37
53
  "sync:rust-assets": "node scripts/sync-rust-package-assets.mjs",
38
54
  "test:rust-assets": "node scripts/sync-rust-package-assets.mjs --check",
39
55
  "check:rustc-backend-spike": "(cd spikes/rustc-backend && RUSTC_BOOTSTRAP=1 RUSTUP_TOOLCHAIN=1.95.0 cargo fmt --check && RUSTC_BOOTSTRAP=1 RUSTUP_TOOLCHAIN=1.95.0 cargo clippy --all-targets -- -D warnings)",
@@ -41,16 +57,19 @@
41
57
  "test:rust-compiler-spikes": "cargo build -p supercov && node scripts/rust-libtest-companion-spike.mjs && node scripts/rust-async-attribution-spike.mjs && node scripts/rust-subprocess-attribution-spike.mjs && node scripts/rust-custom-harness-spike.mjs && node scripts/rust-libtest-builder-lifecycle-spike.mjs",
42
58
  "test": "cargo test --workspace",
43
59
  "test:runtime": "node --test tests/runtime/*.test.mjs",
60
+ "test:asserted-typescript": "npm --prefix analyzers/typescript test",
61
+ "test:asserted-integration": "cargo build -p supercov && npm --prefix analyzers/typescript run build && SUPERCOV_ASSERTED_INTEGRATION=1 node --test analyzers/typescript/tests/archive.integration.test.mjs",
62
+ "test:asserted-package": "cargo build -p supercov && npm run build:asserted-typescript && node scripts/asserted-packed-integration.mjs",
44
63
  "test:fixture": "cargo build -p supercov && node scripts/rust-fixture-matrix.mjs",
45
- "test:packed-npx": "cargo build --release -p supercov && node scripts/packed-npx-integration.mjs",
64
+ "test:packed-npx": "cargo build --release -p supercov && npm run build:asserted-typescript && node scripts/packed-npx-integration.mjs",
46
65
  "test:isolation": "cargo build -p supercov && node scripts/isolation-integration.mjs",
47
66
  "test:filesystem": "cargo test --workspace && cargo build -p supercov && node scripts/workspace-crash-integration.mjs",
48
67
  "test:watchdog": "cargo build -p supercov && node scripts/watchdog-integration.mjs",
49
68
  "test:engine-contract": "cargo build -p supercov && node scripts/engine-contract.mjs",
50
69
  "test:agent": "cargo build -p supercov && node scripts/agent-query-eval.mjs",
51
- "test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && cargo build -p supercov && npm run test:runtime && npm run test:rust-assets && node scripts/rust-process-supervision.mjs && node scripts/rust-direct-node-integration.mjs && node scripts/rust-public-run-integration.mjs && node scripts/rust-embedded-runtime-integration.mjs && node scripts/rust-direct-vitest-integration.mjs && node scripts/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/rust-generic-esbuild-integration.mjs && node scripts/rust-generic-tsc-integration.mjs && node scripts/rust-generic-build-matrix.mjs && node scripts/rust-vite-playwright-integration.mjs",
70
+ "test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && cargo build -p supercov && npm run test:runtime && npm run test:rust-assets && node scripts/rust-process-supervision.mjs && node scripts/rust-direct-node-integration.mjs && node scripts/rust-public-run-integration.mjs && node scripts/rust-embedded-runtime-integration.mjs && node scripts/rust-direct-vitest-integration.mjs && node scripts/rust-direct-jest-integration.mjs && node scripts/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/rust-generic-esbuild-integration.mjs && node scripts/rust-generic-tsc-integration.mjs && node scripts/rust-generic-build-matrix.mjs && node scripts/rust-vite-playwright-integration.mjs",
52
71
  "test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
53
- "test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
72
+ "test:native-package": "cargo build --release -p supercov && npm run build:asserted-typescript && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
54
73
  "test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
55
74
  "test:pypi-registry": "node scripts/pypi-registry-integration.mjs",
56
75
  "build:rubygem": "cargo build --release -p supercov && node scripts/build-rubygem.mjs",
@@ -61,11 +80,12 @@
61
80
  "test:test262": "cargo build --release -p supercov && node scripts/test262-equivalence.mjs",
62
81
  "benchmark:check": "cargo build --release -p supercov && node scripts/rust-transform-benchmark.mjs",
63
82
  "benchmark:python-monitoring": "cargo build -p supercov && node scripts/python-monitoring-benchmark.mjs",
64
- "check": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && npm run test && npm run test:runtime && npm run test:rust-assets && node scripts/package-preflight.mjs && node scripts/verify-binstall-metadata.mjs --offline",
83
+ "check": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && npm run test && npm run test:runtime && npm run test:rust-assets && npm run test:asserted-typescript && npm run test:asserted-integration && npm run test:asserted-package && node scripts/package-preflight.mjs && node scripts/verify-binstall-metadata.mjs --offline",
65
84
  "release:check": "cargo clean && node scripts/sweep-target.mjs && npm run check && npm run test:engine && npm run test:fixture && npm run test:watchdog && npm run test:engine-contract && npm run test:agent && npm run test:child-attribution && npm run test:host-loader && npm run test:python-monitoring && npm run test:ruby-coverage && npm run test:rust-public-cargo && npm run test:packed-npx && npm run test:clang-mcdc && npm run benchmark:check",
66
85
  "release:bump": "node scripts/bump-version.mjs",
67
86
  "sweep": "node scripts/sweep-target.mjs",
68
- "prepack": "node scripts/package-preflight.mjs",
87
+ "oracle:rust": "cargo build --release -p supercov && node scripts/rust-coverage-oracle.mjs",
88
+ "prepack": "npm run build:asserted-typescript && node scripts/package-preflight.mjs",
69
89
  "prepublishOnly": "npm run release:check",
70
90
  "test:child-attribution": "cargo build -p supercov && node scripts/rust-child-attribution-integration.mjs",
71
91
  "test:host-loader": "cargo build -p supercov && node scripts/rust-host-loader-integration.mjs",
@@ -74,14 +94,14 @@
74
94
  "test:rust-public-cargo": "cargo build -p supercov && node scripts/rust-public-cargo-integration.mjs"
75
95
  },
76
96
  "optionalDependencies": {
77
- "@supercov/cli-darwin-arm64": "0.0.42",
78
- "@supercov/cli-darwin-x64": "0.0.42",
79
- "@supercov/cli-linux-arm64-gnu": "0.0.42",
80
- "@supercov/cli-linux-arm64-musl": "0.0.42",
81
- "@supercov/cli-linux-x64-gnu": "0.0.42",
82
- "@supercov/cli-linux-x64-musl": "0.0.42",
83
- "@supercov/cli-win32-arm64": "0.0.42",
84
- "@supercov/cli-win32-x64": "0.0.42"
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"
85
105
  },
86
106
  "peerDependencies": {
87
107
  "@playwright/test": ">=1.55.0",
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ // Supercov Jest adapter. Jest loads it through `setupFilesAfterEnv` of the
3
+ // configuration the preload substitutes (jest.config.mjs), so it runs inside
4
+ // every test file's environment with Jest's globals in scope. It assigns each
5
+ // test attempt its exact scope before the test runs and writes the attempt's
6
+ // coverage and assertion phases after it. Jest tells the environment nothing
7
+ // about an outcome, so the reporter (jestReporter.mjs) records that; the two
8
+ // records share a test id and the report joins them. It computes no coverage.
9
+ //
10
+ // CommonJS on purpose: Jest's module system loads setup files without the
11
+ // ESM flag, and the runtime it needs is already on the sandbox global.
12
+ const { createHash } = require("node:crypto");
13
+ const { mkdirSync, renameSync, writeFileSync } = require("node:fs");
14
+ const { relative, resolve, sep } = require("node:path");
15
+
16
+ // jest-environment-node exposes the outer process's own globals to the sandbox
17
+ // through getters, so this is the same runtime instance the instrumented
18
+ // sources under test call into.
19
+ const runtime =
20
+ globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??
21
+ (typeof process !== "undefined" ? process.__SUPERCOV_DIRECT_RUNTIME__ : undefined);
22
+ const evidenceDirectory = process.env.SUPERCOV_EVIDENCE_DIR;
23
+
24
+ function digest(value) {
25
+ return createHash("sha256").update(value).digest("hex").slice(0, 24);
26
+ }
27
+ function localFile(path) {
28
+ return relative(process.cwd(), path).split(sep).join("/");
29
+ }
30
+ // Must match jestReporter.mjs: the reporter has the same file and full name.
31
+ function testIdentity(testFile, fullName) {
32
+ return `jest:${digest(`${testFile}\0${fullName}`)}`;
33
+ }
34
+ // Mirrors provenance.mjs, which is ESM and out of reach here.
35
+ const KINDS = [
36
+ ["unit", /(^|[/_.-])unit([/_.-]|$)/i],
37
+ ["component", /(^|[/_.-])(component|components|ct)([/_.-]|$)/i],
38
+ ["integration", /(^|[/_.-])(integration|int)([/_.-]|$)/i],
39
+ ["e2e", /(^|[/_.-])(e2e|end-to-end|offline|online)([/_.-]|$)/i],
40
+ ];
41
+ function provenance(testFile) {
42
+ const explicit = process.env.SUPERCOV_TEST_KIND?.trim();
43
+ if (explicit)
44
+ return { runner: "jest", kind: explicit.toLowerCase(), source: "explicit" };
45
+ const kind = KINDS.find(([, pattern]) => pattern.test(testFile))?.[0];
46
+ return kind
47
+ ? { runner: "jest", kind, source: "path" }
48
+ : { runner: "jest", kind: "unit", source: "runner-default" };
49
+ }
50
+ function writeEvidence(suffix, payload) {
51
+ const directory = resolve(process.cwd(), evidenceDirectory, suffix);
52
+ mkdirSync(directory, { recursive: true });
53
+ const target = resolve(directory, "mcdc.json");
54
+ const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
55
+ writeFileSync(temporary, `${JSON.stringify(payload)}\n`);
56
+ renameSync(temporary, target);
57
+ }
58
+
59
+ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof afterEach === "function") {
60
+ // One in-memory snapshot per test; the server JSONL transport would be
61
+ // redundant and unattributed, as under Vitest.
62
+ runtime.enableRuntimeSnapshotEvidence();
63
+ const attempts = new Map();
64
+ const emittedSetupFiles = new Set();
65
+ let active;
66
+ beforeEach(() => {
67
+ const state = expect.getState();
68
+ const testFile = localFile(state.testPath ?? "unknown");
69
+ if (!emittedSetupFiles.has(testFile)) {
70
+ // Code the test file ran while loading, before its first test.
71
+ emittedSetupFiles.add(testFile);
72
+ const setupSnapshot = runtime.coverageSnapshot();
73
+ if (setupSnapshot.hits.length || setupSnapshot.decisions.length) {
74
+ writeEvidence(`jest-${digest(testFile)}-setup`, {
75
+ testId: `jest:${digest(testFile)}:setup`,
76
+ test: `${testFile} > module setup`,
77
+ testFile,
78
+ title: "module setup",
79
+ retry: 0,
80
+ status: "passed",
81
+ provenance: provenance(testFile),
82
+ role: "setup",
83
+ runtime: [setupSnapshot],
84
+ browser: [],
85
+ server: [],
86
+ });
87
+ }
88
+ }
89
+ const fullName = state.currentTestName ?? "test";
90
+ const testId = testIdentity(testFile, fullName);
91
+ const retry = attempts.get(testId) ?? 0;
92
+ attempts.set(testId, retry + 1);
93
+ const testKey = digest(testId);
94
+ const scope = {
95
+ version: 1,
96
+ runId: process.env.SUPERCOV_RUN_ID ?? "unscoped",
97
+ workerId: `jest-${process.env.JEST_WORKER_ID ?? process.pid}`,
98
+ testId,
99
+ testKey,
100
+ retry,
101
+ attemptId: `${testKey}-${retry}`,
102
+ };
103
+ active = { scope, testFile, fullName, retry };
104
+ runtime.activateCoverageScope(scope);
105
+ runtime.resetCoverage(testId);
106
+ });
107
+ afterEach(() => {
108
+ const current = active;
109
+ active = undefined;
110
+ if (!current)
111
+ return;
112
+ const { scope } = current;
113
+ writeEvidence(`jest-${scope.attemptId}`, {
114
+ testId: scope.testId,
115
+ scope,
116
+ test: current.fullName,
117
+ testFile: current.testFile,
118
+ title: current.fullName,
119
+ retry: current.retry,
120
+ // The reporter's record carries the outcome.
121
+ status: "unknown",
122
+ provenance: provenance(current.testFile),
123
+ phases: runtime.takeNodeAssertionPhases(scope),
124
+ runtime: [runtime.coverageSnapshot()],
125
+ browser: [],
126
+ server: [],
127
+ });
128
+ // One runtime state serves every test file a worker runs, so what this
129
+ // test recorded must not leak into the next file's module-setup
130
+ // snapshot (Vitest isolates files; Jest does not).
131
+ runtime.resetCoverage();
132
+ runtime.activateCoverageScope();
133
+ });
134
+ }
@@ -0,0 +1,39 @@
1
+ // Supercov's Jest configuration, handed to Jest through `--config` by the
2
+ // preload (register.mjs) in place of the user's. It reads the user's own
3
+ // configuration the way Jest reads it -- a jest.config.* file in any format
4
+ // Jest accepts, or the "jest" key of package.json -- and adds the adapter
5
+ // (jest.cjs) and the reporter (jestReporter.mjs). Nothing of the user's is
6
+ // replaced: their setup files and reporters keep running.
7
+ import { createRequire } from "node:module";
8
+ import { dirname, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const here = (name) => fileURLToPath(new URL(`./${name}`, import.meta.url));
12
+ const asList = (value) => (value === undefined ? [] : Array.isArray(value) ? value : [value]);
13
+
14
+ export default async function supercovJestConfig() {
15
+ let config = { rootDir: process.cwd() };
16
+ try {
17
+ // The project's own jest-config, so version and resolution match Jest's.
18
+ const projectRequire = createRequire(resolve(process.cwd(), "package.json"));
19
+ const { readInitialOptions } = projectRequire("jest-config");
20
+ const original = process.env["SUPERCOV_ORIGINAL_JEST_CONFIG"] || undefined;
21
+ const read = await readInitialOptions(original, { skipMultipleConfigError: true });
22
+ config = read.config ?? config;
23
+ config.rootDir ??= read.configPath ? dirname(read.configPath) : process.cwd();
24
+ } catch (error) {
25
+ // jest-config before 29.3 has no readInitialOptions; the project's
26
+ // configuration is not read and Jest's defaults apply.
27
+ process.emitWarning(`[supercov] the project's Jest configuration was not read (${error?.message ?? error}); Jest's defaults apply`);
28
+ }
29
+ return {
30
+ ...config,
31
+ // Concurrent tests in one file would share the worker's scope.
32
+ maxConcurrency: 1,
33
+ setupFilesAfterEnv: [...asList(config.setupFilesAfterEnv), here("jest.cjs")],
34
+ reporters: [
35
+ ...(config.reporters === undefined ? ["default"] : asList(config.reporters)),
36
+ here("jestReporter.mjs"),
37
+ ],
38
+ };
39
+ }
@@ -0,0 +1,77 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import { relative, resolve, sep } from "node:path";
4
+ import { inferTestProvenance } from "./provenance.mjs";
5
+ import { atomicWriteFileSync } from "./atomic.mjs";
6
+ function digest(value) {
7
+ return createHash("sha256").update(value).digest("hex").slice(0, 24);
8
+ }
9
+ function localFile(path) {
10
+ return relative(process.cwd(), path).split(sep).join("/");
11
+ }
12
+ function attemptStatus(status) {
13
+ switch (status) {
14
+ case "passed":
15
+ case "failed":
16
+ return status;
17
+ case "pending":
18
+ case "skipped":
19
+ case "todo":
20
+ case "disabled":
21
+ return "skipped";
22
+ default:
23
+ return "unknown";
24
+ }
25
+ }
26
+ /**
27
+ * Records each Jest attempt's outcome, which the test environment never
28
+ * learns: a test that times out, one skipped before its hooks, one that
29
+ * failed after its last hook. The test id formula must match jest.cjs, whose
30
+ * record carries the attempt's coverage; the report joins the two.
31
+ */
32
+ export default class SupercovJestReporter {
33
+ onTestCaseResult(test, result) {
34
+ const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
35
+ if (!evidenceDirectory)
36
+ return;
37
+ const testFile = localFile(test.path);
38
+ const testId = `jest:${digest(`${testFile}\0${result.fullName}`)}`;
39
+ const retry = Math.max((result.invocations ?? 1) - 1, 0);
40
+ const status = attemptStatus(result.status);
41
+ const provenance = inferTestProvenance({
42
+ runner: "jest",
43
+ file: testFile,
44
+ explicitKind: process.env["SUPERCOV_TEST_KIND"],
45
+ });
46
+ const record = (attempt, attemptStatus, flaky) => {
47
+ const payload = {
48
+ testId,
49
+ test: result.fullName,
50
+ testFile,
51
+ title: result.title,
52
+ retry: attempt,
53
+ status: attemptStatus,
54
+ expectedStatus: "passed",
55
+ flaky,
56
+ provenance,
57
+ runtime: [],
58
+ browser: [],
59
+ server: [],
60
+ };
61
+ const directory = resolve(process.cwd(), evidenceDirectory, `jest-${digest(testId)}-${attempt}-status`);
62
+ mkdirSync(directory, { recursive: true });
63
+ atomicWriteFileSync(resolve(directory, "mcdc.json"), `${JSON.stringify(payload)}\n`);
64
+ };
65
+ // `jest.retryTimes` reports one result after the last attempt; every
66
+ // attempt before it failed, or there would have been no retry. The
67
+ // adapter wrote each attempt's coverage without an outcome.
68
+ for (let attempt = 0; attempt < retry; attempt += 1)
69
+ record(attempt, "failed", false);
70
+ record(retry, status, retry > 0 && status === "passed");
71
+ }
72
+ // Jest calls these on every reporter.
73
+ onRunComplete() { }
74
+ getLastError() {
75
+ return undefined;
76
+ }
77
+ }
@@ -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;
@@ -9,6 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import Module, { register, syncBuiltinESMExports } from "node:module";
10
10
  import { closeSync, openSync, unlinkSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
12
13
  import { installLaunchSupervisor, wrapImportedCapability } from "./launchSupervisor.mjs";
13
14
  import { __supercovBindCapabilityWrapper } from "./capability.mjs";
14
15
  installLaunchSupervisor();
@@ -115,7 +116,6 @@ syncBuiltinESMExports();
115
116
  // without editing package scripts, Vitest configs, setup files, or test imports.
116
117
  const generatedVitestConfig = process.env.SUPERCOV_GENERATED_VITEST_CONFIG;
117
118
  const generatedPlaywrightConfig = process.env.SUPERCOV_GENERATED_PLAYWRIGHT_CONFIG;
118
- const generatedJestConfig = process.env.SUPERCOV_GENERATED_JEST_CONFIG;
119
119
  const entrypoint = process.argv[1]?.replaceAll("\\", "/") ?? "";
120
120
  const playwrightTarget = process.env.SUPERCOV_PLAYWRIGHT_MODULE;
121
121
  const projectRoot = process.env.SUPERCOV_PROJECT_ROOT?.replaceAll("\\", "/").replace(/\/$/, "");
@@ -171,19 +171,37 @@ if (generatedVitestConfig && /\/vitest(?:\.m?js)?$/.test(entrypoint)) {
171
171
  });
172
172
  }
173
173
  }
174
- if (generatedJestConfig && isJestEntrypoint) {
174
+ if ((isJestEntrypoint || process.env.JEST_WORKER_ID) && process.env.SUPERCOV_EVIDENCE_DIR) {
175
+ // Jest evaluates instrumented modules in its own module system, past the
176
+ // loader hook that imports the runtime on first use elsewhere, and
177
+ // jest-environment-node exposes to each test's sandbox only the globals
178
+ // the outer process held when that class loaded. The Jest process and
179
+ // each of its workers (JEST_WORKER_ID) therefore load the runtime first;
180
+ // without this, a suite with more than one test file failed on every
181
+ // instrumented module's first line inside the workers.
182
+ globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= await import("./runtime.mjs");
183
+ process.__SUPERCOV_DIRECT_RUNTIME__ ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
184
+ }
185
+ if (isJestEntrypoint && process.env.SUPERCOV_EVIDENCE_DIR) {
186
+ // Jest reads one configuration. Ours (jest.config.mjs) reads the user's
187
+ // the way Jest would and adds the adapter and reporter; an explicit
188
+ // --config on the command line reaches it through the environment.
175
189
  for (let index = 2; index < process.argv.length; index += 1) {
176
190
  const argument = process.argv[index];
177
191
  if (argument === "--config" || argument === "-c") {
178
- process.argv.splice(index, process.argv[index + 1] ? 2 : 1);
192
+ const value = process.argv[index + 1];
193
+ if (value)
194
+ process.env.SUPERCOV_ORIGINAL_JEST_CONFIG = resolve(value);
195
+ process.argv.splice(index, value ? 2 : 1);
179
196
  index -= 1;
180
197
  }
181
198
  else if (argument?.startsWith("--config=")) {
199
+ process.env.SUPERCOV_ORIGINAL_JEST_CONFIG = resolve(argument.slice("--config=".length));
182
200
  process.argv.splice(index, 1);
183
201
  index -= 1;
184
202
  }
185
203
  }
186
- process.argv.push("--config", generatedJestConfig);
204
+ process.argv.push("--config", fileURLToPath(new URL("./jest.config.mjs", import.meta.url)));
187
205
  }
188
206
  if (generatedPlaywrightConfig &&
189
207
  isPlaywrightEntrypoint) {
@@ -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 {