supercov 4.12.2 → 4.14.0

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.
@@ -122,7 +122,7 @@ The file query explained the gap:
122
122
  ```text
123
123
  LINE STATUS SOURCE
124
124
  2 PARTIAL signedIn && !expired
125
- Unobserved: no witness pair shows `!expired` independently changing the decision result
125
+ Unobserved: `!expired` was never false; needs `signedIn` true, `!expired` false, decision false
126
126
  ```
127
127
 
128
128
  Both return paths had run, but neither test checked an expired session. MC/DC
package/docs/cli.md CHANGED
@@ -159,7 +159,7 @@ npx supercov runs <run-id> [query] [options]
159
159
  | `files` | See every included file, including fully covered files; `--group dir` shows coverage by directory, split by test kind |
160
160
  | `file <path>` | Inspect the open obligations in one file |
161
161
  | `decision <id \| path:line>` | Understand missing boolean outcomes and MC/DC witnesses |
162
- | `line <path:line>` | See one line's state, obligations, and covering tests |
162
+ | `line <path:line>...` | See a line's state, obligations, and covering tests; several lines, or `--from-file` with one per line, in one call |
163
163
  | `test <id \| name>` | See the coverage attributed to one test |
164
164
  | `tests without-evidence` | List the tests that made assertions and recorded no coverage, which the summary counts in a warning |
165
165
  | `kinds` | Group coverage by test level, such as unit or E2E |
@@ -204,6 +204,27 @@ In `--json`, every file of `files` and `gaps` carries `totals` and `covered`
204
204
  for each metric beside what is missing, so a percentage can be worked out for
205
205
  any file or set of files.
206
206
 
207
+ `line` reads the run once for every location it is given, so asking for
208
+ twenty lines in one call costs about what one does. `--from-file` takes the
209
+ locations from a file, one per line, skipping blank lines and `#` comments.
210
+ Each line of several lists its state, its gaps and its first three covering
211
+ tests, with `totalTests` and `totalPhases` for the rest; asked alone, a line
212
+ lists every test and phase. In `--json` the reply is `coverage.lines`, one
213
+ entry per location in order; any that do not fit one reply are listed in
214
+ `remaining`, to ask for next.
215
+
216
+ ```sh supercov
217
+ npx supercov runs latest line app/routes/checkout.ts:42 app/routes/checkout.ts:57
218
+ npx supercov runs latest line --from-file lines.txt --json
219
+ ```
220
+
221
+ An MC/DC gap names the value each missing condition never took, in `file`,
222
+ `line` and `decision` (`` `sum < 50` was never true; needs `express` false,
223
+ `sum < 50` true, decision true ``). In `--json` it is `missingOutcomes`:
224
+ `["true"]` or `["false"]`, both when no test evaluated the condition, and
225
+ none when it took both values but never decided the result alone.
226
+ `file --group decision` lists the missing conditions under each decision.
227
+
207
228
  Run any query with `--help` to see only the options valid for that query:
208
229
 
209
230
  ```sh supercov
@@ -249,6 +270,10 @@ with `--min-lines`, `--min-statements`, `--min-functions`, `--min-branches` or
249
270
  eligible obligations, in addition to the whole run. Both report the counts
250
271
  behind the percentage and, for lines, where the gaps are.
251
272
 
273
+ `--kind`, `--runner`, `--tag`, `--title` and `--test-file` hold some of the
274
+ run's tests to a floor of their own, `--kind e2e --min-lines 60` say, from the
275
+ same run.
276
+
252
277
  Floors are compared against the counts, never a rounded percentage: 9,999
253
278
  covered lines out of 10,000 displays as 99.99% and fails a 100% floor, and a
254
279
  run that displayed `100.00%` could never pass one while something is uncovered.
@@ -287,6 +312,9 @@ changed file that looks like product source but is absent from the run is named
287
312
  separately, because treating it as zero uncovered lines would report success for
288
313
  code nothing ran.
289
314
 
315
+ `--kind`, `--runner`, `--tag`, `--title` and `--test-file` ask whether some of
316
+ the tests reach the change: the E2E tests, or the tests of one file.
317
+
290
318
  `--annotate github` prints workflow-command annotations on stdout, combining
291
319
  adjacent misses into one range and capping the total (`--max-annotations`). It
292
320
  needs no token and posts no comment.
@@ -358,9 +386,10 @@ npx supercov runs latest gaps --format jsonl > gaps.jsonl
358
386
 
359
387
  Each line holds the file, the line, the source of that line and what `runs
360
388
  <id> file` says of the obligation: for an MC/DC condition its decision, the
361
- condition and the vectors observed. `--metric`, `--filter`, `--kind` and
362
- `--runner` narrow it as they narrow `gaps`, and nothing is paged, so a tool or
363
- an agent reads a run's every gap in one pass instead of a page at a time.
389
+ condition and the vectors observed. `--metric`, `--filter`, `--kind`,
390
+ `--runner`, `--tag`, `--title` and `--test-file` narrow it as they narrow
391
+ `gaps`, and nothing is paged, so a tool or an agent reads a run's every gap in
392
+ one pass instead of a page at a time.
364
393
 
365
394
  ```sh supercov-example
366
395
  npx supercov runs report --format lcov --output coverage/lcov.info
@@ -369,7 +398,14 @@ npx supercov runs report --format html --output coverage/report
369
398
  ```
370
399
 
371
400
  Both are written from the same view `check` and `patch` read, so a viewer,
372
- hosted service or CI integration sees the totals Supercov enforced. Paths are
401
+ hosted service or CI integration sees the totals Supercov enforced. `--kind`,
402
+ `--runner`, `--tag`, `--title` and `--test-file` write the coverage of only
403
+ those tests, from the same run:
404
+
405
+ ```sh supercov-example
406
+ npx supercov runs report --format lcov --kind e2e --output coverage/e2e.info
407
+ ```
408
+ Paths are
373
409
  repository-relative with forward slashes, ordering is stable, and the file is
374
410
  written atomically; an existing file is kept unless you pass `--force`. Without
375
411
  `--output` the report goes to stdout and diagnostics to stderr, so a redirect
@@ -422,12 +458,39 @@ one as a public artifact.
422
458
  | `--filter all \| passed \| failed` | Recalculate the view from all, successful, or failed attempts |
423
459
  | `--kind <kind>` | Restrict to a test level such as `unit`, `integration`, or `e2e` |
424
460
  | `--runner <runner>` | Restrict to one runner |
461
+ | `--tag <tag>` | Restrict to tests with this tag; repeat it for tests with any of several |
462
+ | `--title <text>` | Restrict to tests whose name contains this text |
463
+ | `--test-file <path>` | Restrict to tests in this file, or in this directory |
425
464
  | `--metric all \| lines \| statements \| functions \| branches \| mcdc` | Choose a metric for `files`, `gaps`, `diff`, or `minimize` |
426
465
  | `--limit N`, `--offset N` | Page through a collection |
427
466
  | `--json` | Return the machine-readable form |
428
467
 
429
468
  Collection output includes a copyable command for the next page.
430
469
 
470
+ ### Read some of a run's tests
471
+
472
+ `--tag`, `--title` and `--test-file` read the coverage of only the tests they
473
+ pick, from a run already measured: the API tests of a Playwright suite, the
474
+ tests of one spec file, everything named `checkout`. Each takes several values
475
+ for tests matching any of them, and when several are given a test must match
476
+ each.
477
+
478
+ ```sh supercov
479
+ npx supercov runs latest --tag @api
480
+ npx supercov runs latest gaps --test-file tests/e2e/checkout.spec.ts
481
+ npx supercov runs latest files --title checkout --tag smoke
482
+ ```
483
+
484
+ A tag is what the runner gives a test: a Playwright tag (`{ tag: '@api' }`, or
485
+ an `@word` in the title), an `@word` in a Jest, Vitest, Mocha, `node:test`,
486
+ AVA or tap title, a pytest marker, an RSpec tag or `type`, a Cucumber tag, a
487
+ JUnit `@Tag` or a TestNG group. Tags are written with or without the `@` and in
488
+ any case. `runs <id> test <name>` lists a test's tags.
489
+
490
+ The first query with a filter works out its coverage from the run's evidence
491
+ and keeps it beside the run (about half a minute for a 1,100-test run); asking
492
+ again reads it back. Nothing is added to the test run itself.
493
+
431
494
  For a large file, group and rank its decisions:
432
495
 
433
496
  ```sh supercov
@@ -514,6 +577,10 @@ npx supercov report --runs 10
514
577
  npx supercov report --output artifacts/supercov-report.html --no-open
515
578
  ```
516
579
 
580
+ `--kind`, `--runner`, `--tag`, `--title` and `--test-file` make a report of
581
+ only those tests' coverage, in every run it holds; a run with none of them is
582
+ left out, and the report names the tests it shows.
583
+
517
584
  A report includes the five newest runs; `--runs N` includes up to 20. A large
518
585
  project's report can pass GitHub's 25 MB attachment limit; `--runs 1` makes
519
586
  the smallest. See [Portable HTML reports](reports.md) for
@@ -62,6 +62,20 @@ that the decision is safe.
62
62
  The same principle applies to Rust, Python, and Ruby boolean expressions and
63
63
  control flow.
64
64
 
65
+ A decision is not only the condition of an `if` or a loop. `&&` and `||` used
66
+ as a value are decisions too:
67
+
68
+ - In Java, Kotlin, Go and Rust every operand is a boolean, so `ok = a && b`
69
+ is a decision over `a` and `b`.
70
+ - In JavaScript, TypeScript, Python and Ruby, `&&` and `||` (`and`/`or`)
71
+ return an operand. The left operand is always tested, and the right one is
72
+ returned, so a compound left operand is the decision: `name = (a && b) ||
73
+ fallback` and `a || b || "default"` each give a decision over `a` and `b`.
74
+ A `!` makes its operand a decision too.
75
+
76
+ Where a Kotlin operand smart-casts the rest of the expression, the
77
+ expression keeps its branch outcomes and is not a decision.
78
+
65
79
  ## What 100% means
66
80
 
67
81
  Supercov derives the coverage denominator from source structure before the test
package/docs/evidence.md CHANGED
@@ -37,7 +37,9 @@ later queries faster; it can rebuild that index from the run.
37
37
 
38
38
  Supercov compares a stored run with the current workspace. If relevant source,
39
39
  tests, dependencies, configuration, or toolchain inputs changed, the run is
40
- marked stale.
40
+ marked stale. A query of a stale run names the files that changed since it,
41
+ tests and source, those added since its commit included, so you can tell which
42
+ tests are newer than the run.
41
43
 
42
44
  A stale run is still valid history. It simply should not be presented as the
43
45
  current state of the repository. Run the complete suite again before choosing
@@ -126,6 +128,11 @@ proof of what the test checks. A name such as `gatewayE2e.test.ts` identifies an
126
128
  E2E test. Ambiguous names keep the runner's default, and the report tells you
127
129
  how many tests use that default.
128
130
 
131
+ A tag naming a level — `@unit`, `@component`, `@integration` or `@e2e` — sets
132
+ a test's kind, ahead of its path and runner: a Playwright tag or an `@word` in
133
+ a JavaScript test's title, a pytest marker, an RSpec tag, a Cucumber tag, a
134
+ JUnit `@Tag` or a TestNG group. Other tags stay tags, which `--tag` selects by.
135
+
129
136
  If your suite has a known kind, set it when running the command:
130
137
 
131
138
  ```sh supercov-example
@@ -81,8 +81,9 @@ npx supercov -- sh -c "npm run build && npm test"
81
81
  ```
82
82
 
83
83
  A `pretest` script, or a Playwright `webServer` that builds, does the same.
84
- When a run fails in a project that has a `build` script the command does not
85
- reach, it says this. `SUPERCOV_KEEP_WORKSPACE=1` leaves the instrumented copy
84
+ When a run fails, it names the tests that failed. When none did and the
85
+ project has a `build` script the command does not reach, it says the build
86
+ output may be what is missing. `SUPERCOV_KEEP_WORKSPACE=1` leaves the instrumented copy
86
87
  in `.supercov/workspaces/` after a run, to inspect.
87
88
 
88
89
  A package inside a larger git repository (`web/` beside `cmd/`, or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "4.12.2",
3
+ "version": "4.14.0",
4
4
  "description": "Coverage, security and code quality for coding agents",
5
5
  "keywords": [
6
6
  "coverage",
@@ -74,7 +74,6 @@
74
74
  "test:fixture": "cargo build -p supercov && node scripts/rust-fixture-matrix.mjs",
75
75
  "test:packed-npx": "cargo build --release -p supercov && node scripts/packed-npx-integration.mjs",
76
76
  "test:isolation": "cargo build -p supercov && node scripts/isolation-integration.mjs",
77
- "test:filesystem": "cargo test --workspace && cargo build -p supercov && node scripts/workspace-crash-integration.mjs",
78
77
  "test:watchdog": "cargo build -p supercov && node scripts/watchdog-integration.mjs",
79
78
  "test:html-report": "cargo build -p supercov && node scripts/html-report-integration.mjs",
80
79
  "test:engine-contract": "cargo build -p supercov && node scripts/engine-contract.mjs",
@@ -83,7 +82,7 @@
83
82
  "eval:skill": "claude plugin eval plugins/supercov --scaffold --trust-plugin --no-publish --model claude-sonnet-5 --judge-model claude-haiku-4-5 --threshold 0.66 --tag behavior trigger --allow-tools Bash Edit Write --max-cost-usd 15",
84
83
  "eval:skill-triggers": "claude plugin eval plugins/supercov --scaffold --trust-plugin --no-publish --model claude-sonnet-5 --judge-model claude-haiku-4-5 --threshold 0.66 --tag trigger --ablation none --max-cost-usd 5",
85
84
  "test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && npm run test:runtime && npm run test:rust-assets && npm run test:engine-integrations",
86
- "test:engine-integrations": "cargo build -p supercov && node scripts/html-report-integration.mjs && 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/mocha-failure-output.mjs && node scripts/failure-output.mjs node ava tap && node scripts/side-by-side.mjs node && node scripts/merge-across.mjs && node scripts/mock-reach.mjs && node scripts/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/cross-origin-requests.mjs && node scripts/leaving-page.mjs && node scripts/shared-page.mjs && node scripts/background-by-time.mjs && node scripts/python-server-playwright.mjs && node scripts/ruby-server-playwright.mjs && node scripts/go-server-playwright.mjs && node scripts/jvm-server-playwright.mjs && node scripts/docker-server-playwright.mjs && node scripts/rust-classic-browser-integration.mjs && node scripts/rust-top-packages-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",
85
+ "test:engine-integrations": "cargo build -p supercov && node scripts/html-report-integration.mjs && 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/mocha-failure-output.mjs && node scripts/failure-output.mjs node ava tap && node scripts/side-by-side.mjs node && node scripts/merge-across.mjs && node scripts/mock-reach.mjs && node scripts/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/cross-origin-requests.mjs && node scripts/leaving-page.mjs && node scripts/shared-page.mjs && node scripts/context-headers.mjs && node scripts/js-syntax-zoo.mjs && node scripts/ruby-syntax-zoo.mjs && node scripts/rust-syntax-zoo.mjs && node scripts/python-syntax-zoo.mjs && node scripts/go-syntax-zoo.mjs && node scripts/java-syntax-zoo.mjs && node scripts/background-by-time.mjs && node scripts/python-server-playwright.mjs && node scripts/ruby-server-playwright.mjs && node scripts/go-server-playwright.mjs && node scripts/jvm-server-playwright.mjs && node scripts/docker-server-playwright.mjs && node scripts/rust-classic-browser-integration.mjs && node scripts/rust-top-packages-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",
87
86
  "test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
88
87
  "test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
89
88
  "test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
@@ -120,14 +119,14 @@
120
119
  "test:windows-tls": "node scripts/windows-tls-test.mjs"
121
120
  },
122
121
  "optionalDependencies": {
123
- "@supercov/cli-darwin-arm64": "4.12.2",
124
- "@supercov/cli-darwin-x64": "4.12.2",
125
- "@supercov/cli-linux-arm64-gnu": "4.12.2",
126
- "@supercov/cli-linux-arm64-musl": "4.12.2",
127
- "@supercov/cli-linux-x64-gnu": "4.12.2",
128
- "@supercov/cli-linux-x64-musl": "4.12.2",
129
- "@supercov/cli-win32-arm64": "4.12.2",
130
- "@supercov/cli-win32-x64": "4.12.2"
122
+ "@supercov/cli-darwin-arm64": "4.14.0",
123
+ "@supercov/cli-darwin-x64": "4.14.0",
124
+ "@supercov/cli-linux-arm64-gnu": "4.14.0",
125
+ "@supercov/cli-linux-arm64-musl": "4.14.0",
126
+ "@supercov/cli-linux-x64-gnu": "4.14.0",
127
+ "@supercov/cli-linux-x64-musl": "4.14.0",
128
+ "@supercov/cli-win32-arm64": "4.14.0",
129
+ "@supercov/cli-win32-x64": "4.14.0"
131
130
  },
132
131
  "peerDependencies": {
133
132
  "@playwright/test": ">=1.55.0",
@@ -67,14 +67,27 @@ const KINDS = [
67
67
  ["integration", /(^|[/_.-])(integration|int)([/_.-]|$)/i],
68
68
  ["e2e", /(^|[/_.-])(e2e|end-to-end|offline|online)([/_.-]|$)/i],
69
69
  ];
70
- function provenance(testFile) {
70
+ // As provenance.mjs reads them: `@word` in a test's name, without the `@`.
71
+ const TITLE_TAG = /(?:^|\s)@([\p{L}\p{N}_][\p{L}\p{N}_:./-]*)/gu;
72
+ const KIND_TAGS = new Set(["unit", "component", "integration", "e2e"]);
73
+ function provenance(testFile, name) {
74
+ const tags = [];
75
+ if (typeof name === "string" && name.includes("@")) {
76
+ for (const match of name.matchAll(TITLE_TAG))
77
+ if (!tags.includes(match[1]))
78
+ tags.push(match[1]);
79
+ }
80
+ const carried = tags.length > 0 ? { tags } : {};
71
81
  const explicit = process.env.SUPERCOV_TEST_KIND?.trim();
72
82
  if (explicit)
73
- return { runner: "jest", kind: explicit.toLowerCase(), source: "explicit" };
83
+ return { runner: "jest", kind: explicit.toLowerCase(), source: "explicit", ...carried };
84
+ const tagged = tags.find((tag) => KIND_TAGS.has(tag.toLowerCase()));
85
+ if (tagged)
86
+ return { runner: "jest", kind: tagged.toLowerCase(), source: "tag", ...carried };
74
87
  const kind = KINDS.find(([, pattern]) => pattern.test(testFile))?.[0];
75
88
  return kind
76
- ? { runner: "jest", kind, source: "path" }
77
- : { runner: "jest", kind: "unit", source: "runner-default" };
89
+ ? { runner: "jest", kind, source: "path", ...carried }
90
+ : { runner: "jest", kind: "unit", source: "runner-default", ...carried };
78
91
  }
79
92
  // One journal per writer, as atomic.mjs's appendEvidenceRecord keeps it: a
80
93
  // file per test cost a directory, a file and a rename each. Jest gives every
@@ -185,7 +198,7 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
185
198
  retry: current.retry,
186
199
  // The reporter's record carries the outcome.
187
200
  status: "unknown",
188
- provenance: provenance(current.testFile),
201
+ provenance: provenance(current.testFile, current.fullName),
189
202
  phases: runtime.takeNodeAssertionPhases(scope),
190
203
  runtime: [runtime.coverageSnapshot()],
191
204
  browser: [],
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { relative, resolve, sep } from "node:path";
3
- import { inferTestProvenance } from "./provenance.mjs";
3
+ import { inferTestProvenance, testTags } from "./provenance.mjs";
4
4
  import { appendEvidenceRecord } from "./atomic.mjs";
5
5
  function digest(value) {
6
6
  return createHash("sha256").update(value).digest("hex").slice(0, 24);
@@ -52,6 +52,7 @@ export default class SupercovJestReporter {
52
52
  runner: "jest",
53
53
  file: testFile,
54
54
  explicitKind: process.env["SUPERCOV_TEST_KIND"],
55
+ tags: testTags(undefined, [result.fullName]),
55
56
  });
56
57
  const record = (attempt, attemptStatus, flaky) => {
57
58
  const payload = {
@@ -11,7 +11,7 @@ import { realpathSync } from "node:fs";
11
11
  import { join, relative, resolve, sep } from "node:path";
12
12
  import { pathToFileURL } from "node:url";
13
13
  import { appendEvidenceRecord } from "./atomic.mjs";
14
- import { inferTestProvenance } from "./provenance.mjs";
14
+ import { inferTestProvenance, testTags } from "./provenance.mjs";
15
15
  import { runnerExecutionScope } from "./runnerEvidence.mjs";
16
16
 
17
17
  const SETUP_HOOK = /^"(?:before|after) all" hook/;
@@ -156,6 +156,7 @@ function observe(runner, runtime, write) {
156
156
  runner: "mocha",
157
157
  file: testFile,
158
158
  explicitKind: process.env.SUPERCOV_TEST_KIND,
159
+ tags: id.role ? [] : testTags(undefined, [id.name]),
159
160
  }),
160
161
  ...(scope ? { phases: runtime.takeNodeAssertionPhases(scope) } : {}),
161
162
  runtime: snapshot ? [snapshot] : [],
@@ -16,7 +16,7 @@ import { relative, resolve, sep } from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
  import * as standardPlaywright from "@playwright/test";
18
18
  import * as coverageRuntime from "./runtime.mjs";
19
- import { inferTestProvenance } from "./provenance.mjs";
19
+ import { inferTestProvenance, testTags } from "./provenance.mjs";
20
20
  import { appendJsonLineDurableSync, appendJsonLineSync } from "./atomic.mjs";
21
21
  import { COVERAGE_PHASE_HEADER, COVERAGE_PHASE_COOKIE, COVERAGE_PHASE_SITE_COOKIE, COVERAGE_SCOPE_COOKIE, COVERAGE_SCOPE_HEADER, COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_CARRIER_ENV, encodeCoverageCarrier, encodeCoverageScope, serverEvidenceDirectory, serverEvidencePath, } from "./transport.mjs";
22
22
  export * from "@playwright/test";
@@ -194,7 +194,6 @@ class CoveragePhaseController {
194
194
  pages = new Set();
195
195
  workers = new Set();
196
196
  contexts = new Set();
197
- contextConfiguredHeaders = new Map();
198
197
  cdpSessions = new Map();
199
198
  newDocumentScriptIds = new Map();
200
199
  pendingRegistrations = new Set();
@@ -387,7 +386,7 @@ class CoveragePhaseController {
387
386
  for (const context of liveTrackedContexts()) {
388
387
  if (!this.contexts.has(context)) {
389
388
  const headersKnown = trackedContextConfiguredHeaders.has(context);
390
- await this.registerContext(context, trackedContextConfiguredHeaders.get(context) ?? this.configuredHeaders, { adopted: !headersKnown }).catch(() => undefined);
389
+ await this.registerContext(context, { adopted: !headersKnown }).catch(() => undefined);
391
390
  }
392
391
  }
393
392
  }
@@ -454,7 +453,7 @@ class CoveragePhaseController {
454
453
  const context = page.context();
455
454
  const tracked = liveTrackedContexts().has(context);
456
455
  const headersKnown = trackedContextConfiguredHeaders.has(context);
457
- await this.registerContext(context, trackedContextConfiguredHeaders.get(context) ?? this.configuredHeaders, {
456
+ await this.registerContext(context, {
458
457
  adopted: !this.contexts.has(context) && tracked && !headersKnown,
459
458
  });
460
459
  const cdp = await page.context().newCDPSession(page).catch(() => undefined);
@@ -490,11 +489,10 @@ class CoveragePhaseController {
490
489
  for (const worker of page.workers())
491
490
  void this.registerWorker(worker);
492
491
  }
493
- async registerContext(context, configuredHeaders = this.configuredHeaders, { adopted = false } = {}) {
492
+ async registerContext(context, { adopted = false } = {}) {
494
493
  if (this.contexts.has(context) || this.disposed)
495
494
  return;
496
495
  this.contexts.add(context);
497
- this.contextConfiguredHeaders.set(context, configuredHeaders);
498
496
  if (adopted)
499
497
  this.adoptedContexts.add(context);
500
498
  await context.addInitScript({ content: browserRuntimePrelude }).catch(() => undefined);
@@ -646,14 +644,6 @@ class CoveragePhaseController {
646
644
  this.disposed = true;
647
645
  await Promise.all([...this.pendingRegistrations]);
648
646
  await this.scriptUpdate;
649
- // A worker-scoped context survives this test. Remove its test scope
650
- // while preserving the exact headers supplied by the suite so work
651
- // between tests cannot be charged to the attempt that just ended.
652
- await Promise.all([...this.contexts]
653
- .filter((context) => !this.adoptedContexts.has(context))
654
- .map((context) => context
655
- .setExtraHTTPHeaders(this.contextConfiguredHeaders.get(context) ?? {})
656
- .catch(() => undefined)));
657
647
  for (const [context, listeners] of this.contextListeners) {
658
648
  for (const [event, listener] of listeners)
659
649
  context.off?.(event, listener);
@@ -773,7 +763,7 @@ class CoveragePhaseController {
773
763
  const configuredHeaders = sourceArgs?.[0]
774
764
  ?.extraHTTPHeaders ?? this.configuredHeaders;
775
765
  trackContext(result, configuredHeaders);
776
- await this.registerContext(result, configuredHeaders);
766
+ await this.registerContext(result);
777
767
  }
778
768
  if (typeof candidate["frames"] === "function" &&
779
769
  typeof candidate["context"] === "function")
@@ -896,11 +886,10 @@ class CoveragePhaseController {
896
886
  [scopeName, scope],
897
887
  ...(phase ? [[phaseName, phase]] : []),
898
888
  ];
889
+ // The context's own headers are left alone: they are the suite's, and
890
+ // a test that sets more with `setExtraHTTPHeaders` keeps them. Setting
891
+ // them back at every phase dropped a test's `X-Forwarded-For`.
899
892
  await Promise.all([
900
- // The context's own headers are the owner's, unchanged.
901
- context
902
- .setExtraHTTPHeaders(this.contextConfiguredHeaders.get(context) ?? this.configuredHeaders)
903
- .catch(() => undefined),
904
893
  ...this.appHosts.map((url) => {
905
894
  const host = new URL(url);
906
895
  // For the app's own site: requests, navigations and sockets.
@@ -1078,7 +1067,7 @@ function patchBrowserPrototype(browser) {
1078
1067
  trackContext(context, configuredHeaders);
1079
1068
  const controller = activeController;
1080
1069
  if (controller && !controller.contexts.has(context))
1081
- await controller.registerContext(context, configuredHeaders).catch(() => undefined);
1070
+ await controller.registerContext(context).catch(() => undefined);
1082
1071
  return context;
1083
1072
  });
1084
1073
  }
@@ -1128,7 +1117,7 @@ function installBrowserLaunchTracking() {
1128
1117
  trackContext(context, configuredHeaders);
1129
1118
  const controller = activeController;
1130
1119
  if (controller)
1131
- await controller.registerContext(context, configuredHeaders).catch(() => undefined);
1120
+ await controller.registerContext(context).catch(() => undefined);
1132
1121
  return context;
1133
1122
  });
1134
1123
  }
@@ -1603,6 +1592,7 @@ const instrumentedFixtures = {
1603
1592
  file: testFile,
1604
1593
  project: testInfo.project.name,
1605
1594
  explicitKind: process.env["SUPERCOV_TEST_KIND"],
1595
+ tags: testTags(testInfo.tags, testInfo.titlePath),
1606
1596
  }),
1607
1597
  phases: mergeCollectedPhases(controller.phases, directRuntime()?.takeNodeAssertionPhases(scope) ?? []),
1608
1598
  ...(controller.workerEvidenceMissing ? { attribution: "partial" } : {}),
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
- import { inferTestProvenance } from "./provenance.mjs";
4
+ import { inferTestProvenance, testTags } from "./provenance.mjs";
5
5
  import { appendJsonLineDurableSync, appendJsonLineSync } from "./atomic.mjs";
6
6
  const GENERATED_EVIDENCE_DIRECTORY = "__SUPERCOV_EVIDENCE_DIRECTORY__";
7
7
  const evidenceWriterIdentity = () => (process.env.SUPERCOV_EXECUTION_LOG_SHARD ?? `pid-${process.pid}`)
@@ -94,6 +94,7 @@ export default class SupercovPlaywrightReporter {
94
94
  file: testFile,
95
95
  project: test.parent.project()?.name,
96
96
  explicitKind: process.env["SUPERCOV_TEST_KIND"],
97
+ tags: testTags(test.tags, test.titlePath()),
97
98
  }),
98
99
  browser: [],
99
100
  server: [],
@@ -49,7 +49,46 @@ function basenameKind(file) {
49
49
  return undefined;
50
50
  return segmentKind(file.split(/[/\\]/).pop());
51
51
  }
52
- export function inferTestProvenance({ runner, file, project, explicitKind, }) {
52
+ // A tag naming a test level says what the test is, as an explicit kind does.
53
+ const KIND_TAGS = new Set(["unit", "component", "integration", "e2e"]);
54
+ // `@word` in a title is a tag, as Playwright reads one: `checkout @e2e`.
55
+ const TITLE_TAG = /(?:^|\s)@([\p{L}\p{N}_][\p{L}\p{N}_:./-]*)/gu;
56
+ // The tags a test carries: those its runner names, then any `@word` in its
57
+ // titles, without the `@`, each once. Only text the runner already handed
58
+ // over is read, so a run costs what it did.
59
+ export function testTags(named, titles) {
60
+ const tags = [];
61
+ const add = (value) => {
62
+ const tag = String(value).trim().replace(/^@/, "");
63
+ if (tag && !tags.includes(tag))
64
+ tags.push(tag);
65
+ };
66
+ for (const tag of Array.isArray(named) ? named : [])
67
+ add(tag);
68
+ for (const title of Array.isArray(titles) ? titles : []) {
69
+ if (typeof title !== "string" || !title.includes("@"))
70
+ continue;
71
+ for (const match of title.matchAll(TITLE_TAG))
72
+ add(match[1]);
73
+ }
74
+ return tags;
75
+ }
76
+ export function inferTestProvenance({ runner, file, project, explicitKind, tags, }) {
77
+ const carried = tags ?? [];
78
+ const kindTag = explicitKind?.trim()
79
+ ? undefined
80
+ : carried.find((tag) => KIND_TAGS.has(tag.toLowerCase()));
81
+ const provenance = kindTag
82
+ ? {
83
+ runner,
84
+ kind: kindTag.toLowerCase(),
85
+ ...(project ? { project } : {}),
86
+ source: "tag",
87
+ }
88
+ : inferredProvenance({ runner, file, project, explicitKind });
89
+ return carried.length > 0 ? { ...provenance, tags: carried } : provenance;
90
+ }
91
+ function inferredProvenance({ runner, file, project, explicitKind, }) {
53
92
  if (explicitKind?.trim()) {
54
93
  return {
55
94
  runner,
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { appendEvidenceRecord } from "./atomic.mjs";
6
- import { inferTestProvenance } from "./provenance.mjs";
6
+ import { inferTestProvenance, testTags } from "./provenance.mjs";
7
7
  import { serverEvidencePath } from "./transport.mjs";
8
8
 
9
9
  let cachedProcessInstanceToken;
@@ -123,6 +123,7 @@ export function writeRunnerEvidence(identity, status, scope, evidenceDirectoryOv
123
123
  runner: identity.runner,
124
124
  file: testFile,
125
125
  explicitKind: process.env["SUPERCOV_TEST_KIND"],
126
+ tags: identity.role ? [] : testTags(identity.tags, [identity.name]),
126
127
  }),
127
128
  ...(phases.length > 0 ? { phases } : {}),
128
129
  runtime: [],
@@ -1697,6 +1697,10 @@ function decodeProbeV2Vector(conditionCount, encoded, outcome) {
1697
1697
  return remaining === 0 ? { values, outcome } : void 0;
1698
1698
  }
1699
1699
  function mcdcEndV2(file, decisionIndex, encoded, value) {
1700
+ // Nothing evaluated: a decision used as a value in a tree whose evaluation
1701
+ // never reached it. Every evaluation evaluates its first condition.
1702
+ if (encoded === 0)
1703
+ return value;
1700
1704
  const clock = file.clock;
1701
1705
  if (clock.fast) {
1702
1706
  const seen = file.decisionEpochs[decisionIndex];
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { relative, resolve, sep } from "node:path";
3
3
  import { afterEach, beforeEach } from "vitest";
4
4
  import { coverageSnapshot as localCoverageSnapshot, activateCoverageScope as localActivateCoverageScope, enableRuntimeSnapshotEvidence as localEnableRuntimeSnapshotEvidence, resetCoverage as localResetCoverage, takeNodeAssertionPhases as localTakeNodeAssertionPhases, } from "./runtime.mjs";
5
- import { inferTestProvenance } from "./provenance.mjs";
5
+ import { inferTestProvenance, testTags } from "./provenance.mjs";
6
6
  import { appendEvidenceRecord } from "./atomic.mjs";
7
7
  const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
8
8
  const emittedSetupFiles = new Set();
@@ -109,6 +109,7 @@ afterEach((context) => {
109
109
  file: testFile,
110
110
  project: task.file.projectName,
111
111
  explicitKind: process.env["SUPERCOV_TEST_KIND"],
112
+ tags: testTags(task.tags, [...titlePath(task)]),
112
113
  }),
113
114
  ...(scope ? { phases: runtime.takeNodeAssertionPhases(scope) } : {}),
114
115
  runtime: [runtime.coverageSnapshot()],
@@ -1,6 +1,6 @@
1
1
  import { relative, resolve, sep } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { inferTestProvenance } from "./provenance.mjs";
3
+ import { inferTestProvenance, testTags } from "./provenance.mjs";
4
4
  import { appendEvidenceRecord } from "./atomic.mjs";
5
5
  function sourcePath(moduleId) {
6
6
  const absolute = moduleId.startsWith("file:")
@@ -59,6 +59,7 @@ export default class SupercovVitestReporter {
59
59
  file: testFile,
60
60
  project: testCase.project.name,
61
61
  explicitKind: process.env["SUPERCOV_TEST_KIND"],
62
+ tags: testTags(testCase.tags ?? testCase.options?.tags, [testCase.fullName]),
62
63
  }),
63
64
  runtime: [],
64
65
  browser: [],
@@ -104,6 +105,7 @@ export default class SupercovVitestReporter {
104
105
  file: testFile,
105
106
  project: file.projectName,
106
107
  explicitKind: process.env["SUPERCOV_TEST_KIND"],
108
+ tags: testTags(task.tags, names),
107
109
  }),
108
110
  runtime: [],
109
111
  browser: [],