supercov 0.0.28 → 0.0.30

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/README.md CHANGED
@@ -154,7 +154,7 @@ Collections accept `--limit` and `--offset` and print a copyable next-page comma
154
154
 
155
155
  ## Local, private, and zero-edit
156
156
 
157
- Run evidence stays under `.supercov/runs/<run-id>/`. Supercov also maintains a marker-protected isolated build cache under `supercov/workspace/`.
157
+ Everything Supercov writes lives under one hidden `.supercov/` directory: run evidence in `.supercov/runs/<run-id>/` and the isolated build cache in `.supercov/workspaces/`. It ignores itself in Git, so there is nothing to add to your `.gitignore`.
158
158
 
159
159
  The Supercov CLI does not contact a Supercov service during a coverage run. Package tools such as `npx` may contact the npm registry to download Supercov when it is not already cached.
160
160
 
@@ -124,5 +124,16 @@ When first-party source lives in unusual directories, declare it explicitly:
124
124
  SUPERCOV_SOURCE_ROOTS=src,app npx supercov -- npm test
125
125
  ```
126
126
 
127
+ Supercov never treats these as project source, so they are neither measured
128
+ nor reported as limitations: another checkout nested in the tree (a directory
129
+ with its own `.git`, such as an agent worktree or a vendored clone), hidden
130
+ directories at the project root (`.shopify/`, `.vercel/`, `.idea/`), directories
131
+ named `generated`, and hashed bundler output inside `assets/`, `static/`, or
132
+ `public/`. Packages the root manifest declares in `workspaces` (or
133
+ `pnpm-workspace.yaml`) are discovered wherever they live; a declared package
134
+ without a conventional source directory is measured as a whole. Functions passed
135
+ to compile-time style macros (`stylex.create(...)`) are left as written because
136
+ the bundler consumes them at build time; nothing about them runs.
137
+
127
138
  Choose roots that describe code the repository owns. Do not include dependencies
128
139
  or generated output merely to make a warning disappear.
@@ -141,3 +141,15 @@ with:
141
141
 
142
142
  Do not include secrets, private source, or raw evidence from a repository you
143
143
  cannot share.
144
+
145
+ ## "malformed evidence record(s) ... were excluded"
146
+
147
+ Application processes append coverage evidence themselves. When two of them
148
+ share one file — pool runners that restore several VMs from a single snapshot
149
+ run clones of the same server process, with the same process id — an append
150
+ can tear another's line. Supercov skips the unreadable line, counts it under
151
+ `CORRUPT_EVIDENCE_RECORDS`, and treats it as a blocking limitation so the run
152
+ never looks complete. Since 0.0.29 the background and execution-trace writers also detect a
153
+ clone sharing their file and move to a fresh one, so this should be rare; if it
154
+ persists, check whether something outside Supercov appends to
155
+ `.supercov/…/server/background/`.
@@ -32,7 +32,9 @@ synced back to the project after the run, so `supercov -- npm test -- -u`
32
32
  updates snapshots in the repository exactly as `npm test -- -u` would. Two
33
33
  exceptions are reported instead of applied: changes the command makes to
34
34
  instrumented source files (the instrumented copies must never overwrite your
35
- sources) and deletions (never propagated automatically).
35
+ sources) and deletions (never propagated automatically). Changes inside any
36
+ `node_modules` directory are neither applied nor reported: dependency trees are
37
+ not command outputs.
36
38
 
37
39
  ## Files Supercov creates
38
40
 
@@ -41,7 +43,7 @@ sources) and deletions (never propagated automatically).
41
43
  | `.supercov/runs/<run-id>/` | Completed immutable runs |
42
44
  | `.supercov/work/` | Temporary state while a run is being prepared |
43
45
  | `.supercov/locks/` | Prevents two operations from racing |
44
- | `.supercov-workspace/workspace/<project>/` | Isolated source and reusable instrumented build cache (safe to delete) |
46
+ | `.supercov/workspaces/` | Isolated source mirror and reusable instrumented build cache (safe to delete) |
45
47
 
46
48
  Managed directories include Git ignore rules so run evidence and instrumented
47
49
  builds do not become ordinary repository changes.
@@ -88,6 +90,15 @@ When a suite launches a container or VM from a mounted workspace, Supercov uses
88
90
  the isolated workspace as the source presented to that environment. The runtime
89
91
  must be able to cross the launch boundary and return evidence.
90
92
 
93
+ Dependencies stay out of the instrumented copy. The root `node_modules` is
94
+ linked entry by entry to the project's own, so an environment that mounts the
95
+ workspace must bring its own root dependencies (the supported launchers do).
96
+ Nested `node_modules` inside packages and extensions are materialised as real
97
+ directories: cloned copy-on-write where the filesystem allows it (APFS), and
98
+ hard-linked file by file elsewhere on Unix, so they resolve inside the mount
99
+ either way. Only when neither is possible, such as a dependency tree on another
100
+ volume, are they linked entry by entry like the root.
101
+
91
102
  If a remote executor hides the launch or mount boundary, Supercov reports the
92
103
  limitation instead of claiming unseen code was measured. See
93
104
  [Supported suites](supported-suites.md) for the current boundary.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "Zero-edit, runner-aware coverage completeness for JavaScript test suites",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -68,12 +68,12 @@
68
68
  "prepublishOnly": "npm run release:check"
69
69
  },
70
70
  "optionalDependencies": {
71
- "@supercov/cli-darwin-arm64": "0.0.28",
72
- "@supercov/cli-darwin-x64": "0.0.28",
73
- "@supercov/cli-linux-arm64-gnu": "0.0.28",
74
- "@supercov/cli-linux-arm64-musl": "0.0.28",
75
- "@supercov/cli-linux-x64-gnu": "0.0.28",
76
- "@supercov/cli-linux-x64-musl": "0.0.28"
71
+ "@supercov/cli-darwin-arm64": "0.0.30",
72
+ "@supercov/cli-darwin-x64": "0.0.30",
73
+ "@supercov/cli-linux-arm64-gnu": "0.0.30",
74
+ "@supercov/cli-linux-arm64-musl": "0.0.30",
75
+ "@supercov/cli-linux-x64-gnu": "0.0.30",
76
+ "@supercov/cli-linux-x64-musl": "0.0.30"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "@playwright/test": ">=1.55.0",
@@ -1,6 +1,6 @@
1
1
  import childProcess from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { appendFileSync, mkdirSync, statSync } from "node:fs";
4
4
  import Module, { syncBuiltinESMExports } from "node:module";
5
5
  import { dirname, isAbsolute, relative, resolve } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
@@ -30,9 +30,26 @@ function isClassConstructor(value) {
30
30
  return false;
31
31
  }
32
32
  }
33
- function executionLogPath(path) {
33
+ // A pid is not an identity: pool VMs restored from one snapshot run clones
34
+ // of this very process with the same pid, and their appends to one file over
35
+ // a shared mount tear each other's lines. The log name carries a token, and
36
+ // an append that finds the file a different size than this writer left it
37
+ // has met a clone and rotates to a fresh file. The token mixes time in as
38
+ // well as randomness because clones may also share their entropy state.
39
+ function writerToken() {
40
+ let random = "";
41
+ try {
42
+ random = randomBytes(3).toString("hex");
43
+ }
44
+ catch {
45
+ random = Math.floor(Math.random() * 16777215).toString(16);
46
+ }
47
+ return `${random}${process.hrtime.bigint().toString(36).slice(-5)}`;
48
+ }
49
+ let executionLog = { token: writerToken(), path: undefined, size: 0 };
50
+ function executionLogPath(path, token = executionLog.token) {
34
51
  const shard = (process.env["SUPERCOV_EXECUTION_LOG_SHARD"] ?? "host").replace(/[^A-Za-z0-9_.-]/g, "_");
35
- const suffix = `.${shard}.${process.pid}.jsonl`;
52
+ const suffix = `.${shard}.${process.pid}-${token}.jsonl`;
36
53
  return path.endsWith(".jsonl")
37
54
  ? `${path.slice(0, -".jsonl".length)}${suffix}`
38
55
  : `${path}${suffix}`;
@@ -41,15 +58,30 @@ function record(value) {
41
58
  const configuredPath = process.env["SUPERCOV_EXECUTION_LOG"];
42
59
  if (!configuredPath)
43
60
  return;
44
- const path = executionLogPath(configuredPath);
45
61
  try {
46
- mkdirSync(dirname(path), { recursive: true });
47
- appendFileSync(path, `${JSON.stringify({
62
+ let path = executionLogPath(configuredPath);
63
+ if (executionLog.path === path) {
64
+ let currentSize = 0;
65
+ try {
66
+ currentSize = statSync(path).size;
67
+ }
68
+ catch {
69
+ currentSize = 0;
70
+ }
71
+ if (currentSize !== executionLog.size) {
72
+ executionLog = { token: writerToken(), path: undefined, size: 0 };
73
+ path = executionLogPath(configuredPath);
74
+ }
75
+ }
76
+ const line = `${JSON.stringify({
48
77
  at: new Date().toISOString(),
49
78
  pid: process.pid,
50
79
  ppid: process.ppid,
51
80
  ...value,
52
- })}\n`);
81
+ })}\n`;
82
+ mkdirSync(dirname(path), { recursive: true });
83
+ appendFileSync(path, line);
84
+ executionLog = { token: executionLog.token, path, size: executionLog.size + Buffer.byteLength(line) };
53
85
  }
54
86
  catch {
55
87
  // Process tracing is diagnostic and must never change test behavior.
@@ -6,7 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { createHash } from "node:crypto";
9
+ import { createHash, randomBytes } from "node:crypto";
10
10
  import childProcess from "node:child_process";
11
11
  import { mkdirSync, readFileSync, rmSync } from "node:fs";
12
12
  import http from "node:http";
@@ -33,7 +33,22 @@ const adapter = (targetModule === "@playwright/test"
33
33
  const base = (adapter[targetTestExport] ?? adapter.test);
34
34
  const baseExpect = adapter.expect;
35
35
  const GENERATED_EVIDENCE_DIRECTORY = "__SUPERCOV_EVIDENCE_DIRECTORY__";
36
- const evidenceWriterIdentity = () => (process.env.SUPERCOV_EXECUTION_LOG_SHARD ?? `pid-${process.pid}`)
36
+ // A pid is not an identity. Pool runners restore several VMs from one
37
+ // snapshot and re-execute tests in fresh workers that all carry the same
38
+ // pid; without a per-process token their attempts hash to one identity and
39
+ // their journals land in one file. The token mixes time with randomness
40
+ // because restored clones may also share entropy state.
41
+ const processInstanceToken = (() => {
42
+ let random = "";
43
+ try {
44
+ random = randomBytes(3).toString("hex");
45
+ }
46
+ catch {
47
+ random = Math.floor(Math.random() * 16777215).toString(16);
48
+ }
49
+ return `${random}${process.hrtime.bigint().toString(36).slice(-5)}`;
50
+ })();
51
+ const evidenceWriterIdentity = () => (process.env.SUPERCOV_EXECUTION_LOG_SHARD ?? `pid-${process.pid}-${processInstanceToken}`)
37
52
  .replace(/[^A-Za-z0-9_-]/g, "_");
38
53
  const GENERATED_RUN_ID = "__SUPERCOV_RUN_ID__";
39
54
  const PHASE_STORAGE_KEY = "__supercov_phase";
@@ -755,7 +770,7 @@ function currentRunId() {
755
770
  }
756
771
  function executionScope(testInfo) {
757
772
  const runId = currentRunId();
758
- const workerId = `pid-${process.pid}-worker-${testInfo.workerIndex}`;
773
+ const workerId = `pid-${process.pid}-${processInstanceToken}-worker-${testInfo.workerIndex}`;
759
774
  const testKey = createHash("sha256")
760
775
  .update(testInfo.testId)
761
776
  .digest("hex")
@@ -1,10 +1,25 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
2
  import { mkdirSync, readFileSync } from "node:fs";
3
3
  import { relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { atomicWriteFileSync } from "./atomic.js";
6
6
  import { inferTestProvenance } from "./provenance.js";
7
7
  import { serverEvidencePath } from "./transport.js";
8
+
9
+ let cachedProcessInstanceToken;
10
+ function processInstanceToken() {
11
+ if (cachedProcessInstanceToken)
12
+ return cachedProcessInstanceToken;
13
+ let random = "";
14
+ try {
15
+ random = randomBytes(3).toString("hex");
16
+ }
17
+ catch {
18
+ random = Math.floor(Math.random() * 16777215).toString(16);
19
+ }
20
+ cachedProcessInstanceToken = `${random}${process.hrtime.bigint().toString(36).slice(-5)}`;
21
+ return cachedProcessInstanceToken;
22
+ }
8
23
  function localFile(file) {
9
24
  if (!file)
10
25
  return undefined;
@@ -24,7 +39,9 @@ export function runnerTestId(identity) {
24
39
  export function runnerExecutionScope(identity) {
25
40
  const testId = runnerTestId(identity);
26
41
  const retry = identity.retry ?? 0;
27
- const workerId = `${identity.runner}-${process.env["JEST_WORKER_ID"] ?? process.pid}`;
42
+ // Same hazard as the Playwright shim: a pooled runner's fresh workers can
43
+ // share a pid, so the worker identity carries a per-process token.
44
+ const workerId = `${identity.runner}-${process.env["JEST_WORKER_ID"] ?? process.pid}-${processInstanceToken()}`;
28
45
  const testKey = createHash("sha256").update(testId).digest("hex").slice(0, 24);
29
46
  return {
30
47
  version: 1,
@@ -150,6 +150,7 @@ function createState() {
150
150
  persistedServerRecords: /* @__PURE__ */ new Set(),
151
151
  backgroundBuffers: /* @__PURE__ */ new Map(),
152
152
  backgroundWriters: /* @__PURE__ */ new Map(),
153
+ backgroundShardSizes: /* @__PURE__ */ new Map(),
153
154
  backgroundSequence: 0,
154
155
  runtimeSnapshots: false,
155
156
  assertionPhases: /* @__PURE__ */ new Map(),
@@ -427,6 +428,16 @@ function writeExclusiveBackgroundRecord(fs, runId, writer, initialSequence, payl
427
428
  }
428
429
  throw Object.assign(new Error("Could not allocate a collision-free Supercov background evidence record"), { code: "SUPERCOV_BACKGROUND_COLLISION_LIMIT" });
429
430
  }
431
+ function backgroundWriterToken() {
432
+ try {
433
+ const getBuiltinModule = process.getBuiltinModule;
434
+ const crypto = getBuiltinModule == null ? void 0 : getBuiltinModule("node:crypto");
435
+ if (crypto)
436
+ return crypto.randomBytes(4).toString("hex");
437
+ } catch (e) {
438
+ }
439
+ return Math.floor(Math.random() * 4294967295).toString(16);
440
+ }
430
441
  function appendDurableBackgroundRecord(fs, runId, record) {
431
442
  var _a8;
432
443
  const records = state.backgroundBuffers.get(runId) != null ? state.backgroundBuffers.get(runId) : /* @__PURE__ */ new Map();
@@ -437,15 +448,32 @@ function appendDurableBackgroundRecord(fs, runId, record) {
437
448
  const payload = JSON.stringify(record) + "\n";
438
449
  fs.mkdirSync(directory, { recursive: true });
439
450
  let path = state.backgroundWriters.get(runId);
451
+ // A pid is not an identity: pool VMs restored from one snapshot run clones
452
+ // of this very process, same pid and same cached shard path, and their
453
+ // appends over a shared mount tear each other's lines. The shard name gets
454
+ // a fresh random token, and an append that finds the file a different size
455
+ // than this writer left it has met a clone: it moves to a new shard.
456
+ if (path) {
457
+ let currentSize = -1;
458
+ try {
459
+ currentSize = fs.statSync(path).size;
460
+ } catch (e) {
461
+ }
462
+ if (currentSize !== (state.backgroundShardSizes.get(path) ?? -1)) {
463
+ path = void 0;
464
+ }
465
+ }
440
466
  if (!path) {
441
467
  const shard = (_a8 = process.env["SUPERCOV_EXECUTION_LOG_SHARD"]) != null ? _a8 : "process";
442
- const writer = `${shard}-${process.pid}`;
468
+ const writer = `${shard}-${process.pid}-${backgroundWriterToken()}`;
443
469
  const nextSequence = writeExclusiveBackgroundRecord(fs, runId, writer, state.backgroundSequence, payload);
444
470
  state.backgroundSequence = nextSequence;
445
471
  path = backgroundEvidencePath(runId, `${writer}-${nextSequence - 1}`);
446
472
  state.backgroundWriters.set(runId, path);
473
+ state.backgroundShardSizes.set(path, Buffer.byteLength(payload));
447
474
  } else {
448
475
  fs.appendFileSync(path, payload);
476
+ state.backgroundShardSizes.set(path, (state.backgroundShardSizes.get(path) ?? 0) + Buffer.byteLength(payload));
449
477
  }
450
478
  records.set(key, record);
451
479
  state.backgroundBuffers.set(runId, records);