supercov 4.5.0 → 4.7.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.
package/docs/cli.md CHANGED
@@ -121,6 +121,18 @@ npx supercov -- cargo nextest run
121
121
  Use the complete command you rely on before merging or deploying. A coverage run
122
122
  preserves the wrapped command's exit status, so it can remain a CI gate.
123
123
 
124
+ A script that queries the run it just recorded can have Supercov write the
125
+ run's id to a file. `latest` is the newest run, which is another one when two
126
+ commands record runs side by side:
127
+
128
+ ```sh supercov-example
129
+ SUPERCOV_RUN_ID_FILE=unit.id npx supercov -- <test command>
130
+ npx supercov runs <run-id> check --min-lines 80 # the id in unit.id
131
+ ```
132
+
133
+ The file is removed when the run starts and written when it is recorded, so a
134
+ run that is not recorded leaves no id behind.
135
+
124
136
  ## List and select runs
125
137
 
126
138
  ```sh supercov
@@ -511,8 +523,13 @@ pass in the run is listed regardless.
511
523
  per line, for a runner's filter. A Vitest, Playwright or node:test test inside
512
524
  `describe` blocks is printed as the blocks and the test joined by spaces
513
525
  (`small charges`), which is what `vitest -t`, `--grep` and
514
- `--test-name-pattern` match. A dependency, lockfile, configuration or
515
- toolchain change affects every test and is reported as such. A source file
526
+ `--test-name-pattern` match. A helper beside the tests, or source outside what is
527
+ measured, that changed affects the tests whose files import it (a JavaScript or
528
+ TypeScript helper that only gained functions affects none), and every test
529
+ where Supercov cannot read the imports. A dependency,
530
+ lockfile, configuration or toolchain change affects every test and is reported
531
+ as such; a test runner's settings affect only the runs that used that runner.
532
+ A source file
516
533
  added since the run is outside every test's record; the working-tree check
517
534
  says so, and the suite should run in full.
518
535
 
@@ -595,24 +612,61 @@ A test that is in two of the runs is one test in the merge. If it failed in
595
612
  either, it is failed, which is why a command run again replaces its earlier
596
613
  run and is not merged with it.
597
614
 
598
- The runs may differ in their tests when the later ones have only gained test
599
- files. Measuring a new test alone and adding it to the runs before it is
600
- then the whole suite's figure, without the whole suite's time:
615
+ The runs may differ in their tests, the helpers beside them, and the test
616
+ runners' settings, as long as nothing that changed reaches what an earlier
617
+ run's tests did. Measuring a new test alone and adding it to the runs before it
618
+ is then the whole suite's figure, without the whole suite's time:
601
619
 
602
620
  ```sh supercov-example
603
- npx supercov -- npm test # the suite, once
604
- npx supercov -- npx vitest run test/new.test.ts # a test added since, alone
621
+ npx supercov -- npm test # the suite, once
622
+ npx supercov -- <the new test alone> # a test added since
605
623
  npx supercov merge
606
624
  ```
607
625
 
608
- A run is left out, and named, when a file of its own tests has changed since
609
- it ran, or a file tests load that is not itself a test's: what it recorded is
610
- then of other tests. Measure those again to count them.
626
+ Each file that differs from what an earlier run was recorded with is judged by
627
+ what that run's tests did with it:
628
+
629
+ - Another run's test file: it was not in this run. A file of the run's own
630
+ tests: what it recorded is of other tests, and the run is left out.
631
+ - A file the run measured: left out, always.
632
+ - A helper, fixture or support file, or source outside what is measured (a
633
+ simulator kept out of `SUPERCOV_SOURCE_ROOTS`): left out of a run only when
634
+ that run's test files import it, directly or through other files. Supercov
635
+ reads the imports of JavaScript, TypeScript and Python tests, with pytest's
636
+ `conftest.py` files and `pytest_plugins`; a computed import, a change to
637
+ `sys.path`, or tests in another language count every helper as loaded. A
638
+ JavaScript or TypeScript helper that only gained functions, which code
639
+ written before cannot have called, leaves every run in.
640
+ - A test runner's settings: `playwright.config.*`, `vitest.config.*`,
641
+ `jest.config.*` and `ava.config.*` belong to their runner, and a run that
642
+ did not use that runner keeps. Of Playwright's settings, only those that
643
+ decide what a test does count: `use`, `testDir`, `webServer`,
644
+ `globalSetup`, `globalTeardown`, and a project's `dependencies` and
645
+ `teardown`. Timeouts, retries, workers, reporters, which files and names to
646
+ run, and projects added since schedule tests and change nothing an earlier
647
+ test reached. This needs the newer run to have measured Playwright.
648
+ - Any other configuration, such as `tsconfig.json`, `vite.config.*` or a
649
+ Babel file, may set how any test runs, and leaves the run out.
650
+
651
+ A run left out is named with the files and the functions that changed. The
652
+ merged run lists what differed and why it does not reach the earlier runs:
653
+ `merged across:` in `runs <id>`, and `mergedAcross` in JSON.
654
+
655
+ When you know a change Supercov counts does not alter what the earlier tests
656
+ ran, say so:
657
+
658
+ ```sh supercov-example
659
+ npx supercov merge --allow-changed
660
+ ```
661
+
662
+ The product code, the dependencies, and what is measured must still be the
663
+ same, and a run whose own test files changed is still left out. What was
664
+ accepted is listed in `merged across:` as accepted.
611
665
 
612
666
  `assertions assess` assesses a merged run across every suite merged into it,
613
- reusing the answers saved when its parts were assessed. `tests affected` is
614
- asked of the runs that were merged, which hold the record of what each test
615
- ran.
667
+ reusing the answers saved when its parts were assessed. `tests affected` of a
668
+ merged run is answered from the runs it was merged from, which hold the record
669
+ of what each test ran; a test in several of them is affected where any says so.
616
670
 
617
671
  ## Clean local data
618
672
 
@@ -660,6 +714,7 @@ terminal or offline environment after the package has been downloaded.
660
714
  | `SUPERCOV_SOURCE_ROOTS` | Comma-separated directories or files that hold your own code, in any language; everything else is left out |
661
715
  | `SUPERCOV_TEST_KIND` | Label the wrapped command as a test level such as `unit` or `e2e` |
662
716
  | `SUPERCOV_SOURCE_TOOLS` | Comma-separated names of tools the test command runs that read source as text, such as a linter or a spell checker, besides Biome, oxlint, dprint, cspell and knip. Each is run on your source as you wrote it instead of on the instrumented copy |
717
+ | `SUPERCOV_RUN_ID_FILE` | A file to write the recorded run's id to, for a script to query that run rather than `latest` |
663
718
  | `SUPERCOV_KEEP_WORKSPACE` | Set to `1` to leave the instrumented copy of the project in `.supercov/workspaces/` after the run, to inspect what the command ran |
664
719
 
665
720
  Examples:
@@ -120,6 +120,20 @@ Each completed run stores compressed evidence and metadata under
120
120
  `.supercov/runs/<run-id>/`. Query views are derived from that evidence rather
121
121
  than stored as a full report for every filter.
122
122
 
123
+ Most of a large run is its query index, `query-index.v1.bin`: on a suite of
124
+ about 400 tests with per-test evidence, 160 MB of a 216 MB run, beside 58 MB of
125
+ evidence. The index is a cache. Supercov builds it again from the evidence the
126
+ first time the run is queried without it, so a CI job that keeps runs as an
127
+ artifact can leave it out:
128
+
129
+ ```sh supercov-example
130
+ tar czf supercov-runs.tgz --exclude 'query-index.v1.bin' .supercov/runs
131
+ ```
132
+
133
+ A merged run holds a copy of each run's evidence and an index of its own, so
134
+ merging adds about as much again. To hand a run to a reviewer, the HTML report
135
+ (`npx supercov report`) is usually the smaller artifact.
136
+
123
137
  The isolated workspace may be larger because it can contain an instrumented
124
138
  build cache. Supercov does not delete history in the background.
125
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "Coverage, security and code quality for coding agents",
5
5
  "keywords": [
6
6
  "coverage",
@@ -83,7 +83,7 @@
83
83
  "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
84
  "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
85
  "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/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.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",
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/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/cross-origin-requests.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
87
  "test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
88
88
  "test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
89
89
  "test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
@@ -120,14 +120,14 @@
120
120
  "test:windows-tls": "node scripts/windows-tls-test.mjs"
121
121
  },
122
122
  "optionalDependencies": {
123
- "@supercov/cli-darwin-arm64": "4.5.0",
124
- "@supercov/cli-darwin-x64": "4.5.0",
125
- "@supercov/cli-linux-arm64-gnu": "4.5.0",
126
- "@supercov/cli-linux-arm64-musl": "4.5.0",
127
- "@supercov/cli-linux-x64-gnu": "4.5.0",
128
- "@supercov/cli-linux-x64-musl": "4.5.0",
129
- "@supercov/cli-win32-arm64": "4.5.0",
130
- "@supercov/cli-win32-x64": "4.5.0"
123
+ "@supercov/cli-darwin-arm64": "4.7.0",
124
+ "@supercov/cli-darwin-x64": "4.7.0",
125
+ "@supercov/cli-linux-arm64-gnu": "4.7.0",
126
+ "@supercov/cli-linux-arm64-musl": "4.7.0",
127
+ "@supercov/cli-linux-x64-gnu": "4.7.0",
128
+ "@supercov/cli-linux-x64-musl": "4.7.0",
129
+ "@supercov/cli-win32-arm64": "4.7.0",
130
+ "@supercov/cli-win32-x64": "4.7.0"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "@playwright/test": ">=1.55.0",
@@ -18,7 +18,7 @@ import * as standardPlaywright from "@playwright/test";
18
18
  import * as coverageRuntime from "./runtime.mjs";
19
19
  import { inferTestProvenance } from "./provenance.mjs";
20
20
  import { appendJsonLineDurableSync, appendJsonLineSync } from "./atomic.mjs";
21
- import { COVERAGE_PHASE_HEADER, COVERAGE_PHASE_COOKIE, COVERAGE_SCOPE_COOKIE, COVERAGE_SCOPE_HEADER, COVERAGE_CARRIER_ENV, encodeCoverageCarrier, encodeCoverageScope, serverEvidenceDirectory, serverEvidencePath, } from "./transport.mjs";
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";
23
23
  const generatedTargetModule = "__SUPERCOV_PLAYWRIGHT_MODULE__";
24
24
  const targetModule = process.env["SUPERCOV_PLAYWRIGHT_MODULE"] ??
@@ -236,9 +236,21 @@ class CoveragePhaseController {
236
236
  // Parameter properties are stateful despite the base ESLint rule treating
237
237
  // this as an empty constructor.
238
238
  // eslint-disable-next-line no-useless-constructor
239
- constructor(scope, configuredHeaders = {}) {
239
+ constructor(scope, configuredHeaders = {}, baseURL) {
240
240
  this.scope = scope;
241
241
  this.configuredHeaders = configuredHeaders;
242
+ // Where the app under test is served: the project's base URL, and the
243
+ // local hosts a web server listens on. A cookie is kept per host, for
244
+ // every port of it.
245
+ const hosts = ["http://localhost/", "http://127.0.0.1/"];
246
+ try {
247
+ if (baseURL)
248
+ hosts.unshift(new URL("/", baseURL).href);
249
+ }
250
+ catch {
251
+ // A base URL Playwright itself would refuse.
252
+ }
253
+ this.appHosts = [...new Set(hosts)];
242
254
  }
243
255
  allPages() {
244
256
  return [...this.pages];
@@ -446,17 +458,34 @@ class CoveragePhaseController {
446
458
  // own attempt, so the newest wins; the fetch patch is installed once
447
459
  // and reads the attempt at call time, so a stale copy can never pin an
448
460
  // old scope onto a request the way nested wrappers would.
449
- await context.addInitScript(({ attemptId, scopeHeader, scopeValue, scopeCookie }) => {
461
+ await context.addInitScript(({ attemptId, scopeHeader, scopeValue, scopeCookie, scopeSiteCookie }) => {
450
462
  const coverageGlobal = globalThis;
451
463
  coverageGlobal.__SUPERCOV_ATTEMPT__ = { attemptId, scopeValue };
452
464
  coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ = attemptId;
453
465
  try {
454
466
  document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
467
+ // And for this document's frames on another of the app's
468
+ // hosts, where crossing sites is allowed.
469
+ if (globalThis.isSecureContext)
470
+ document.cookie = `${scopeSiteCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=None; Secure`;
455
471
  if (!coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__) {
456
472
  const originalFetch = globalThis.fetch?.bind(globalThis);
457
473
  if (originalFetch) {
458
474
  coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__ = true;
459
475
  globalThis.fetch = ((input, init) => {
476
+ // Only the page's own origin hears the scope. A
477
+ // header on a request to another origin makes the
478
+ // browser ask that origin first, and one that
479
+ // allows only the headers it uses refuses: the
480
+ // page's fetch failed under measurement alone.
481
+ try {
482
+ const target = new URL(input instanceof Request ? input.url : String(input), location.href);
483
+ if (target.origin !== location.origin)
484
+ return originalFetch(input, init);
485
+ }
486
+ catch {
487
+ return originalFetch(input, init);
488
+ }
460
489
  const headers = new Headers(init?.headers ??
461
490
  (input instanceof Request ? input.headers : undefined));
462
491
  headers.set(scopeHeader, coverageGlobal.__SUPERCOV_ATTEMPT__?.scopeValue ?? scopeValue);
@@ -476,6 +505,7 @@ class CoveragePhaseController {
476
505
  scopeHeader: COVERAGE_SCOPE_HEADER,
477
506
  scopeValue: encodeCoverageScope(this.scope),
478
507
  scopeCookie: COVERAGE_SCOPE_COOKIE,
508
+ scopeSiteCookie: COVERAGE_SCOPE_SITE_COOKIE,
479
509
  }).catch(() => undefined);
480
510
  const register = (page) => {
481
511
  const pending = this.registerPage(page).finally(() => this.pendingRegistrations.delete(pending));
@@ -513,6 +543,16 @@ class CoveragePhaseController {
513
543
  if (!originalFetch)
514
544
  return answer;
515
545
  globalThis.fetch = ((input, init) => {
546
+ // As in a page: another origin would be asked first, and may
547
+ // refuse a header it does not expect.
548
+ try {
549
+ const target = new URL(input instanceof Request ? input.url : String(input), globalThis.location.href);
550
+ if (target.origin !== globalThis.location.origin)
551
+ return originalFetch(input, init);
552
+ }
553
+ catch {
554
+ return originalFetch(input, init);
555
+ }
516
556
  const headers = new Headers(init?.headers ??
517
557
  (input instanceof Request ? input.headers : undefined));
518
558
  headers.set(scopeHeader, scopeValue);
@@ -705,13 +745,10 @@ class CoveragePhaseController {
705
745
  const options = scoped[0] && typeof scoped[0] === "object"
706
746
  ? scoped[0]
707
747
  : {};
708
- scoped[0] = {
709
- ...options,
710
- extraHTTPHeaders: {
711
- ...(options.extraHTTPHeaders ?? {}),
712
- ...(activeCoverageHeaders() ?? {}),
713
- },
714
- };
748
+ // The scope reaches the app in cookies once the context is
749
+ // registered (`updateContextHeaders`); headers on the context would
750
+ // go to every origin the page fetches from.
751
+ scoped[0] = { ...options };
715
752
  return scoped;
716
753
  }
717
754
  /**
@@ -734,7 +771,7 @@ class CoveragePhaseController {
734
771
  }
735
772
  async activateFrames(phaseId) {
736
773
  await timed("activate.frameEvaluate", () => Promise.all([...this.pages].flatMap((page) => page.frames()).map((frame) => frame
737
- .evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie }) => {
774
+ .evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie, scopeSiteCookie, phaseSiteCookie }) => {
738
775
  globalThis.__SUPERCOV_PHASE_ID__ = id;
739
776
  const coverageGlobal = globalThis;
740
777
  // A document that loaded under an earlier test's attempt (a page
@@ -746,6 +783,10 @@ class CoveragePhaseController {
746
783
  localStorage.setItem(storageKey, id);
747
784
  document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
748
785
  document.cookie = `${phaseCookie}=${encodeURIComponent(id)}; Path=/; SameSite=Lax`;
786
+ if (globalThis.isSecureContext) {
787
+ document.cookie = `${scopeSiteCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=None; Secure`;
788
+ document.cookie = `${phaseSiteCookie}=${encodeURIComponent(id)}; Path=/; SameSite=None; Secure`;
789
+ }
749
790
  }
750
791
  catch {
751
792
  // Sandboxed/cross-origin frames may not expose localStorage.
@@ -757,6 +798,8 @@ class CoveragePhaseController {
757
798
  scopeCookie: COVERAGE_SCOPE_COOKIE,
758
799
  scopeValue: encodeCoverageScope(this.scope),
759
800
  phaseCookie: COVERAGE_PHASE_COOKIE,
801
+ scopeSiteCookie: COVERAGE_SCOPE_SITE_COOKIE,
802
+ phaseSiteCookie: COVERAGE_PHASE_SITE_COOKIE,
760
803
  })
761
804
  .catch(() => undefined))));
762
805
  }
@@ -779,14 +822,43 @@ class CoveragePhaseController {
779
822
  this.unresponsiveWorkers.add(worker);
780
823
  })));
781
824
  }
825
+ /**
826
+ * Tell the app which test and phase its requests belong to, in cookies on
827
+ * the app's hosts. Headers set on the context went with every request the
828
+ * browser made, to any origin, and a header on a request to another
829
+ * origin makes the browser ask that origin first: one that allows only the
830
+ * headers it uses refused, and the page's request failed under
831
+ * measurement alone. A cookie never does. The page sends it to its own
832
+ * site from the first navigation on, and, on a secure origin, to frames
833
+ * of the app's other hosts as well.
834
+ */
782
835
  async updateContextHeaders(context, phaseId) {
783
- await context
784
- .setExtraHTTPHeaders({
785
- ...(this.contextConfiguredHeaders.get(context) ?? this.configuredHeaders),
786
- [COVERAGE_SCOPE_HEADER]: encodeCoverageScope(this.scope),
787
- ...(phaseId ? { [COVERAGE_PHASE_HEADER]: phaseId } : {}),
788
- })
789
- .catch(() => undefined);
836
+ const scope = encodeURIComponent(encodeCoverageScope(this.scope));
837
+ const phase = phaseId ? encodeURIComponent(phaseId) : undefined;
838
+ const pairs = (scopeName, phaseName) => [
839
+ [scopeName, scope],
840
+ ...(phase ? [[phaseName, phase]] : []),
841
+ ];
842
+ await Promise.all([
843
+ // The context's own headers are the owner's, unchanged.
844
+ context
845
+ .setExtraHTTPHeaders(this.contextConfiguredHeaders.get(context) ?? this.configuredHeaders)
846
+ .catch(() => undefined),
847
+ ...this.appHosts.map((url) => {
848
+ const host = new URL(url);
849
+ // For the app's own site: requests, navigations and sockets.
850
+ const cookies = pairs(COVERAGE_SCOPE_COOKIE, COVERAGE_PHASE_COOKIE).map(([name, value]) => ({ name, value, url, sameSite: "Lax" }));
851
+ // For its frames on another of its hosts: a cookie that
852
+ // crosses sites must be Secure, which needs https or a local
853
+ // host. Chromium keeps such a cookie only when it is given by
854
+ // domain; given by an http URL it is dropped without a word.
855
+ if (host.protocol === "https:" || ["localhost", "127.0.0.1"].includes(host.hostname)) {
856
+ cookies.push(...pairs(COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_PHASE_SITE_COOKIE).map(([name, value]) => ({ name, value, domain: host.hostname, path: "/", sameSite: "None", secure: true })));
857
+ }
858
+ // One host refused leaves the others theirs.
859
+ return context.addCookies(cookies).catch(() => undefined);
860
+ }),
861
+ ]);
790
862
  }
791
863
  async activatePage(page, phaseId) {
792
864
  const cdp = this.cdpSessions.get(page);
@@ -1352,7 +1424,7 @@ const instrumentedFixtures = {
1352
1424
  async ({}, use, testInfo) => {
1353
1425
  const scope = executionScope(testInfo);
1354
1426
  const configuredHeaders = Object.fromEntries(Object.entries(testInfo.project.use.extraHTTPHeaders ?? {}).map(([name, value]) => [name, String(value)]));
1355
- const controller = new CoveragePhaseController(scope, configuredHeaders);
1427
+ const controller = new CoveragePhaseController(scope, configuredHeaders, testInfo.project.use.baseURL);
1356
1428
  controllers.set(scope.attemptId, controller);
1357
1429
  activeController = controller;
1358
1430
  directRuntime()?.activateCoverageScope(scope);
@@ -34,6 +34,8 @@ var COVERAGE_SCOPE_HEADER = "x-supercov-scope";
34
34
  var COVERAGE_PHASE_HEADER = "x-supercov-phase";
35
35
  var COVERAGE_SCOPE_COOKIE = "__supercov_scope";
36
36
  var COVERAGE_PHASE_COOKIE = "__supercov_phase";
37
+ var COVERAGE_SCOPE_SITE_COOKIE = "__supercov_scope_site";
38
+ var COVERAGE_PHASE_SITE_COOKIE = "__supercov_phase_site";
37
39
  var COVERAGE_CARRIER_ENV = "SUPERCOV_CONTEXT";
38
40
  var DEFAULT_SERVER_EVIDENCE_ROOT = "/tmp/supercov-server-evidence";
39
41
  function configuredServerEvidenceRoot() {
@@ -1345,8 +1347,10 @@ function requestCoverageContext(value) {
1345
1347
  }
1346
1348
  }
1347
1349
  }
1348
- const encodedScope = (_a8 = headers.get(COVERAGE_SCOPE_HEADER)) != null ? _a8 : cookies.get(COVERAGE_SCOPE_COOKIE);
1349
- const rawPhaseId = (_b = headers.get(COVERAGE_PHASE_HEADER)) != null ? _b : cookies.get(COVERAGE_PHASE_COOKIE);
1350
+ // A frame of the app on another of its hosts carries only the cookies
1351
+ // made to cross sites.
1352
+ const encodedScope = (_a8 = headers.get(COVERAGE_SCOPE_HEADER)) != null ? _a8 : cookies.has(COVERAGE_SCOPE_COOKIE) ? cookies.get(COVERAGE_SCOPE_COOKIE) : cookies.get(COVERAGE_SCOPE_SITE_COOKIE);
1353
+ const rawPhaseId = (_b = headers.get(COVERAGE_PHASE_HEADER)) != null ? _b : cookies.has(COVERAGE_PHASE_COOKIE) ? cookies.get(COVERAGE_PHASE_COOKIE) : cookies.get(COVERAGE_PHASE_SITE_COOKIE);
1350
1354
  const scope = decodeCoverageScope(typeof encodedScope === "string" ? encodedScope : void 0);
1351
1355
  // The phase cookie outlives a test in a shared browser context just like
1352
1356
  // the stored phase does (see currentPhaseId); a request carrying a scope
@@ -2,6 +2,11 @@ export const COVERAGE_SCOPE_HEADER = "x-supercov-scope";
2
2
  export const COVERAGE_PHASE_HEADER = "x-supercov-phase";
3
3
  export const COVERAGE_SCOPE_COOKIE = "__supercov_scope";
4
4
  export const COVERAGE_PHASE_COOKIE = "__supercov_phase";
5
+ // The same, for frames of the app on another of its hosts: a cookie that
6
+ // crosses sites must be Secure, and a Secure cookie is not sent over ws://,
7
+ // so the two cannot be one.
8
+ export const COVERAGE_SCOPE_SITE_COOKIE = "__supercov_scope_site";
9
+ export const COVERAGE_PHASE_SITE_COOKIE = "__supercov_phase_site";
5
10
  export const COVERAGE_CARRIER_ENV = "SUPERCOV_CONTEXT";
6
11
  export const DEFAULT_SERVER_EVIDENCE_ROOT = "/tmp/supercov-server-evidence";
7
12
  // Buffer as it was when Supercov loaded. Tests replace globals -- axios's set