@piwitests/reporter 0.36.0 → 0.38.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/dist/cli/index.js CHANGED
@@ -422,6 +422,7 @@ function formatGateResult(result) {
422
422
  );
423
423
  }
424
424
  for (const violation of result.violations) lines.push(` \u2716 ${violation.message}`);
425
+ for (const warning of result.warnings ?? []) lines.push(` \u26A0 ${warning}`);
425
426
  lines.push(` ${facts.runUrl}`);
426
427
  return lines.join("\n");
427
428
  }
@@ -456,6 +457,9 @@ Policy (at least one is required):
456
457
  --fail-on-flaky Fail when this run contains any flaky test
457
458
  --require-selection <key> Fail when a test the named selection matches did not run or failed
458
459
 
460
+ Warn-only (reported, never fails the build):
461
+ --max-uncovered-changes <n> Warn when more than n changed files have no observed test reach
462
+
459
463
  Other:
460
464
  --json Print the raw result as JSON instead of a summary
461
465
  -h, --help Show this help
@@ -477,9 +481,9 @@ function readCount(argv, name) {
477
481
  if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
478
482
  return Math.floor(n);
479
483
  }
480
- function readRunIdFromFile(path11) {
484
+ function readRunIdFromFile(path13) {
481
485
  try {
482
- const parsed = JSON.parse(fs3.readFileSync(path11, "utf-8"));
486
+ const parsed = JSON.parse(fs3.readFileSync(path13, "utf-8"));
483
487
  const runId = Number(parsed.runId);
484
488
  return Number.isFinite(runId) && runId > 0 ? runId : null;
485
489
  } catch {
@@ -510,7 +514,13 @@ function parseGateArgs(argv, env) {
510
514
  failOnFlaky: argv.includes("--fail-on-flaky"),
511
515
  requireSelection: readOption2(argv, "--require-selection")
512
516
  };
513
- return { serverUrl, apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null, runId, policy };
517
+ return {
518
+ serverUrl,
519
+ apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
520
+ runId,
521
+ policy,
522
+ maxUncoveredChanges: readCount(argv, "--max-uncovered-changes")
523
+ };
514
524
  }
515
525
  async function requestGate(args) {
516
526
  const headers = { "Content-Type": "application/json" };
@@ -518,7 +528,7 @@ async function requestGate(args) {
518
528
  const res = await fetch(`${args.serverUrl}/api/test-runs/${args.runId}/gate`, {
519
529
  method: "POST",
520
530
  headers,
521
- body: JSON.stringify(args.policy)
531
+ body: JSON.stringify({ ...args.policy, maxUncoveredChanges: args.maxUncoveredChanges })
522
532
  });
523
533
  if (!res.ok) {
524
534
  const body = await res.json().catch(() => ({}));
@@ -778,7 +788,8 @@ var WORKFLOW_SKILLS = [
778
788
  "investigate-failure",
779
789
  "apply-locator-healing",
780
790
  "stabilize-flaky-tests",
781
- "run-the-right-tests"
791
+ "run-the-right-tests",
792
+ "write-the-missing-test"
782
793
  ];
783
794
  var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
784
795
  var DEFAULT_SKILLS_DIR = path7.join(".claude", "skills");
@@ -1675,8 +1686,267 @@ async function runRun(argv, env = process.env) {
1675
1686
  return spawnPlaywrightForRun(args.pkgRunner, [...resolution.materialization.args, ...args.extra], runEnv);
1676
1687
  }
1677
1688
 
1678
- // src/cli/index.ts
1689
+ // src/cli/probe.ts
1690
+ var import_node_child_process4 = require("child_process");
1691
+ var import_node_module3 = require("module");
1692
+ var fs10 = __toESM(require("fs"));
1693
+ var os2 = __toESM(require("os"));
1694
+ var path12 = __toESM(require("path"));
1695
+
1696
+ // src/internal/config/desktop.ts
1697
+ var fs9 = __toESM(require("fs"));
1698
+ var os = __toESM(require("os"));
1699
+ var path11 = __toESM(require("path"));
1700
+
1701
+ // src/internal/config/env.ts
1702
+ var DEFAULTS = {
1703
+ projectName: "default-project",
1704
+ uploadTraces: true,
1705
+ uploadReport: true,
1706
+ liveFileUploads: true,
1707
+ collectScmInfo: true,
1708
+ collectCiInfo: true,
1709
+ collectPerformanceMetrics: true,
1710
+ captureLocators: true,
1711
+ capturePageState: true,
1712
+ // Off by default: the page inventory reads control and link names from every
1713
+ // visited page, so it stays opt-in until a project turns it on.
1714
+ capturePageInventory: false,
1715
+ captureServerTraces: true,
1716
+ sampleAriaOnPass: true,
1717
+ uploadManifest: true,
1718
+ defaultCapture: true,
1719
+ streaming: true,
1720
+ streamingBatchSize: 5,
1721
+ streamingBatchDelay: 2e3,
1722
+ maxStreamBufferBytes: 100 * 1024 * 1024,
1723
+ failOnFlakyTests: false,
1724
+ username: null,
1725
+ password: null,
1726
+ apiKey: null,
1727
+ verbose: false
1728
+ };
1729
+ var PIWI_ENV_KEYS = {
1730
+ serverUrl: "PIWI_DASHBOARD_URL",
1731
+ projectName: "PIWI_PROJECT_NAME",
1732
+ verbose: "PIWI_VERBOSE",
1733
+ apiKey: "PIWI_API_KEY",
1734
+ username: "PIWI_USERNAME",
1735
+ password: "PIWI_PASSWORD",
1736
+ environment: "PIWI_ENVIRONMENT",
1737
+ label: "PIWI_LABEL",
1738
+ runLabel: "PIWI_RUN_LABEL",
1739
+ keep: "PIWI_KEEP",
1740
+ streaming: "PIWI_STREAMING",
1741
+ streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
1742
+ streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
1743
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
1744
+ liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
1745
+ failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
1746
+ uploadTraces: "PIWI_UPLOAD_TRACES",
1747
+ uploadReport: "PIWI_UPLOAD_REPORT",
1748
+ captureLocators: "PIWI_CAPTURE_LOCATORS",
1749
+ capturePageState: "PIWI_CAPTURE_PAGE_STATE",
1750
+ capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
1751
+ captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
1752
+ sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
1753
+ uploadManifest: "PIWI_UPLOAD_MANIFEST",
1754
+ defaultCapture: "PIWI_DEFAULT_CAPTURE",
1755
+ inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
1756
+ pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
1757
+ outputFile: "PIWI_OUTPUT_FILE",
1758
+ aiMode: "PIWI_AI",
1759
+ aiDir: "PIWI_AI_DIR",
1760
+ aiOnMiss: "PIWI_AI_ON_MISS",
1761
+ aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
1762
+ aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
1763
+ aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
1764
+ aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
1765
+ aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
1766
+ };
1767
+ var PIWI_PROBE_ENV = {
1768
+ flag: "PIWI_PROBE",
1769
+ plan: "PIWI_PROBE_PLAN",
1770
+ results: "PIWI_PROBE_RESULTS",
1771
+ secret: "PIWI_PROBE_SECRET"
1772
+ };
1773
+ var ENV_FALLBACK_SPECS = [
1774
+ { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
1775
+ { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
1776
+ { option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
1777
+ { option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
1778
+ { option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
1779
+ { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
1780
+ { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
1781
+ { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
1782
+ { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
1783
+ { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
1784
+ { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
1785
+ { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
1786
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
1787
+ { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
1788
+ { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
1789
+ { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
1790
+ { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
1791
+ { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
1792
+ { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
1793
+ { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
1794
+ { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
1795
+ { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
1796
+ { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
1797
+ { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
1798
+ { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
1799
+ { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
1800
+ { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
1801
+ ];
1802
+
1803
+ // src/cli/probe.ts
1804
+ var EXIT_OK4 = 0;
1805
+ var EXIT_ERROR4 = 2;
1679
1806
  var USAGE6 = `
1807
+ piwi probe \u2014 run the dashboard's probe plan and record what the suite noticed
1808
+
1809
+ Usage:
1810
+ npx @piwitests/reporter probe [options] [-- <playwright args>]
1811
+
1812
+ Options:
1813
+ --server-url <url> Dashboard URL (or PIWI_DASHBOARD_URL)
1814
+ --api-key <key> Reporter API key (or PIWI_API_KEY)
1815
+ --project <name|id> Project name or id (or PIWI_PROJECT_NAME)
1816
+ --budget <n> Max pairs to probe this run (default the server's)
1817
+
1818
+ Everything after \`--\` is passed to \`playwright test\`.
1819
+ `.trim();
1820
+ function readOption6(argv, name) {
1821
+ const i = argv.indexOf(name);
1822
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : void 0;
1823
+ }
1824
+ function escapeRegex(s) {
1825
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1826
+ }
1827
+ function resolvePlaywrightCli2() {
1828
+ const require2 = (0, import_node_module3.createRequire)(path12.join(process.cwd(), "noop.js"));
1829
+ for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
1830
+ try {
1831
+ return require2.resolve(id);
1832
+ } catch {
1833
+ }
1834
+ }
1835
+ return null;
1836
+ }
1837
+ function spawnPlaywright2(playwrightArgs, env) {
1838
+ const cli = resolvePlaywrightCli2();
1839
+ const child = cli ? (0, import_node_child_process4.spawn)(process.execPath, [cli, "test", ...playwrightArgs], { stdio: "inherit", env }) : (0, import_node_child_process4.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs], {
1840
+ stdio: "inherit",
1841
+ env
1842
+ });
1843
+ return new Promise((resolve6) => {
1844
+ child.on("error", (err) => {
1845
+ console.error(`piwi probe: could not start Playwright \u2014 ${err.message}`);
1846
+ resolve6(EXIT_ERROR4);
1847
+ });
1848
+ child.on("exit", (code) => resolve6(code ?? EXIT_ERROR4));
1849
+ });
1850
+ }
1851
+ async function runProbe(argv) {
1852
+ const sep3 = argv.indexOf("--");
1853
+ const own = sep3 < 0 ? argv : argv.slice(0, sep3);
1854
+ const playwrightArgs = sep3 < 0 ? [] : argv.slice(sep3 + 1);
1855
+ if (own.includes("-h") || own.includes("--help")) {
1856
+ console.log(USAGE6);
1857
+ return EXIT_OK4;
1858
+ }
1859
+ const env = process.env;
1860
+ const serverUrl = (readOption6(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
1861
+ if (!serverUrl) {
1862
+ console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
1863
+ return EXIT_ERROR4;
1864
+ }
1865
+ const apiKey = readOption6(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
1866
+ const project = readOption6(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
1867
+ const budget = readOption6(own, "--budget");
1868
+ let projectId;
1869
+ try {
1870
+ projectId = await resolveProjectId({ serverUrl, apiKey, project, key: "" });
1871
+ } catch (e) {
1872
+ console.error(`piwi probe: ${e.message}`);
1873
+ return EXIT_ERROR4;
1874
+ }
1875
+ const authHeaders2 = apiKey ? { "X-API-Key": apiKey } : {};
1876
+ const planUrl = `${serverUrl}/api/projects/${projectId}/probes/plan${budget ? `?budget=${encodeURIComponent(budget)}` : ""}`;
1877
+ let plan;
1878
+ try {
1879
+ const res = await fetch(planUrl, { headers: authHeaders2 });
1880
+ if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
1881
+ plan = await res.json();
1882
+ } catch (e) {
1883
+ console.error(`piwi probe: could not fetch the probe plan \u2014 ${e.message}`);
1884
+ return EXIT_ERROR4;
1885
+ }
1886
+ if (!plan.items || plan.items.length === 0) {
1887
+ console.error("piwi probe: nothing to probe \u2014 the plan is empty.");
1888
+ return EXIT_OK4;
1889
+ }
1890
+ console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
1891
+ const dir = fs10.mkdtempSync(path12.join(os2.tmpdir(), "piwi-probe-"));
1892
+ const planFile = path12.join(dir, "plan.json");
1893
+ const resultsFile = path12.join(dir, "results.jsonl");
1894
+ const runOutputFile = path12.join(dir, "run.json");
1895
+ fs10.writeFileSync(planFile, JSON.stringify(plan));
1896
+ fs10.writeFileSync(resultsFile, "");
1897
+ const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex).join("|");
1898
+ const pwArgs = [...playwrightArgs];
1899
+ if (grep && !playwrightArgs.includes("--grep") && !playwrightArgs.includes("-g")) pwArgs.push("--grep", grep);
1900
+ const childEnv = {
1901
+ ...env,
1902
+ [PIWI_PROBE_ENV.flag]: "1",
1903
+ [PIWI_PROBE_ENV.plan]: planFile,
1904
+ [PIWI_PROBE_ENV.results]: resultsFile,
1905
+ // The reporter creates the probe run and knows its server-assigned id; have
1906
+ // it write the run output here so the results POST can name the run.
1907
+ [PIWI_ENV_KEYS.outputFile]: runOutputFile
1908
+ };
1909
+ await spawnPlaywright2(pwArgs, childEnv);
1910
+ const lines = fs10.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
1911
+ const results = [];
1912
+ for (const line of lines) {
1913
+ try {
1914
+ results.push(JSON.parse(line));
1915
+ } catch {
1916
+ }
1917
+ }
1918
+ if (results.length === 0) {
1919
+ console.error("piwi probe: no probe outcomes were recorded (are the Piwi capture fixtures active?).");
1920
+ return EXIT_OK4;
1921
+ }
1922
+ let runId = null;
1923
+ try {
1924
+ const out = JSON.parse(fs10.readFileSync(runOutputFile, "utf8"));
1925
+ if (typeof out.runId === "number") runId = out.runId;
1926
+ } catch {
1927
+ }
1928
+ try {
1929
+ const res = await fetch(`${serverUrl}/api/projects/${projectId}/probes/results`, {
1930
+ method: "POST",
1931
+ headers: { "Content-Type": "application/json", ...authHeaders2 },
1932
+ body: JSON.stringify({ runId, results })
1933
+ });
1934
+ if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
1935
+ const body = await res.json();
1936
+ const noticed = results.filter((r) => r.outcome === "noticed").length;
1937
+ const notNoticed = results.filter((r) => r.outcome === "not-noticed").length;
1938
+ console.error(
1939
+ `piwi probe: recorded ${body.recorded ?? results.length} outcome(s) \u2014 ${noticed} noticed, ${notNoticed} not noticed.`
1940
+ );
1941
+ } catch (e) {
1942
+ console.error(`piwi probe: could not post probe results \u2014 ${e.message}`);
1943
+ return EXIT_ERROR4;
1944
+ }
1945
+ return EXIT_OK4;
1946
+ }
1947
+
1948
+ // src/cli/index.ts
1949
+ var USAGE7 = `
1680
1950
  piwi \u2014 companion commands for the Piwi Dashboard reporter
1681
1951
 
1682
1952
  Usage:
@@ -1688,6 +1958,7 @@ Commands:
1688
1958
  gate Fail a CI job on the dashboard's analysis of a run
1689
1959
  select Print the Playwright args for a saved test selection
1690
1960
  run Run a saved test selection with playwright test
1961
+ probe Run the dashboard's probe plan and record what the suite noticed
1691
1962
  ai Manage committed natural-language AI-step artifacts
1692
1963
 
1693
1964
  Run \`npx @piwitests/reporter <command> --help\` for a command's options.
@@ -1707,17 +1978,19 @@ async function main() {
1707
1978
  return runSelect(rest);
1708
1979
  case "run":
1709
1980
  return runRun(rest);
1981
+ case "probe":
1982
+ return runProbe(rest);
1710
1983
  case "ai":
1711
1984
  return runAi(rest);
1712
1985
  case void 0:
1713
1986
  case "-h":
1714
1987
  case "--help":
1715
- console.log(USAGE6);
1988
+ console.log(USAGE7);
1716
1989
  return 0;
1717
1990
  default:
1718
1991
  console.error(`piwi: unknown command "${command}"
1719
1992
  `);
1720
- console.error(USAGE6);
1993
+ console.error(USAGE7);
1721
1994
  return 2;
1722
1995
  }
1723
1996
  }
@@ -35,7 +35,7 @@ __export(global_setup_module_exports, {
35
35
  module.exports = __toCommonJS(global_setup_module_exports);
36
36
 
37
37
  // src/public/global-setup.ts
38
- var path4 = __toESM(require("path"));
38
+ var path5 = __toESM(require("path"));
39
39
 
40
40
  // src/internal/support/errors.ts
41
41
  function errorMessage(error) {
@@ -44,7 +44,7 @@ function errorMessage(error) {
44
44
  }
45
45
 
46
46
  // src/public/global-setup.ts
47
- var fs4 = __toESM(require("fs"));
47
+ var fs5 = __toESM(require("fs"));
48
48
 
49
49
  // src/internal/config/desktop.ts
50
50
  var fs = __toESM(require("fs"));
@@ -77,8 +77,12 @@ var DEFAULTS = {
77
77
  collectPerformanceMetrics: true,
78
78
  captureLocators: true,
79
79
  capturePageState: true,
80
+ // Off by default: the page inventory reads control and link names from every
81
+ // visited page, so it stays opt-in until a project turns it on.
82
+ capturePageInventory: false,
80
83
  captureServerTraces: true,
81
84
  sampleAriaOnPass: true,
85
+ uploadManifest: true,
82
86
  defaultCapture: true,
83
87
  streaming: true,
84
88
  streamingBatchSize: 5,
@@ -100,6 +104,7 @@ var PIWI_ENV_KEYS = {
100
104
  environment: "PIWI_ENVIRONMENT",
101
105
  label: "PIWI_LABEL",
102
106
  runLabel: "PIWI_RUN_LABEL",
107
+ keep: "PIWI_KEEP",
103
108
  streaming: "PIWI_STREAMING",
104
109
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
105
110
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
@@ -110,8 +115,10 @@ var PIWI_ENV_KEYS = {
110
115
  uploadReport: "PIWI_UPLOAD_REPORT",
111
116
  captureLocators: "PIWI_CAPTURE_LOCATORS",
112
117
  capturePageState: "PIWI_CAPTURE_PAGE_STATE",
118
+ capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
113
119
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
114
120
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
121
+ uploadManifest: "PIWI_UPLOAD_MANIFEST",
115
122
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
116
123
  inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
117
124
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
@@ -139,6 +146,7 @@ var ENV_FALLBACK_SPECS = [
139
146
  { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
140
147
  { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
141
148
  { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
149
+ { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
142
150
  { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
143
151
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
144
152
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
@@ -149,8 +157,10 @@ var ENV_FALLBACK_SPECS = [
149
157
  { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
150
158
  { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
151
159
  { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
160
+ { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
152
161
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
153
162
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
163
+ { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
154
164
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
155
165
  { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
156
166
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
@@ -351,6 +361,27 @@ var HttpClient = class {
351
361
  return {};
352
362
  }
353
363
  }
364
+ /** Send a JSON PUT request. Used for the declared-surface manifest upload. */
365
+ async putJSON(pathname, payload, auth) {
366
+ const body = JSON.stringify(payload);
367
+ const res = await this.request("PUT", pathname, {
368
+ headers: {
369
+ "Content-Type": "application/json",
370
+ "Content-Length": Buffer.byteLength(body)
371
+ },
372
+ body,
373
+ auth
374
+ });
375
+ if (res.status < 200 || res.status >= 300) {
376
+ this.logger.debugError(`Response: ${res.text}`);
377
+ throw new HttpError(res.status);
378
+ }
379
+ try {
380
+ return JSON.parse(res.text);
381
+ } catch {
382
+ return {};
383
+ }
384
+ }
354
385
  /** Send a multipart form-data POST request. Used for report and trace uploads. */
355
386
  async postFormData(pathname, form, auth) {
356
387
  const headers = form.getHeaders();
@@ -453,6 +484,36 @@ function detectCiRunLabel() {
453
484
  return null;
454
485
  }
455
486
 
487
+ // src/internal/collect/metadata-collector.ts
488
+ var import_node_child_process = require("child_process");
489
+ var DETACHED_HEAD = "HEAD";
490
+ function resolveScmBranch(env, gitBranch) {
491
+ const pick = (...names) => {
492
+ for (const name of names) {
493
+ const value = env[name]?.trim();
494
+ if (value) return value;
495
+ }
496
+ return void 0;
497
+ };
498
+ const override = env.PIWI_BRANCH?.trim();
499
+ if (override) return override;
500
+ let provider;
501
+ if (env.JENKINS_URL) provider = pick("CHANGE_BRANCH", "BRANCH_NAME");
502
+ else if (env.GITHUB_ACTIONS) provider = pick("GITHUB_HEAD_REF", "GITHUB_REF_NAME");
503
+ else if (env.GITLAB_CI) provider = pick("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME", "CI_COMMIT_REF_NAME");
504
+ else if (env.CIRCLECI) provider = pick("CIRCLE_BRANCH");
505
+ else if (env.TRAVIS) provider = pick("TRAVIS_PULL_REQUEST_BRANCH", "TRAVIS_BRANCH");
506
+ else if (env.TF_BUILD) provider = pick("SYSTEM_PULLREQUEST_SOURCEBRANCH", "BUILD_SOURCEBRANCHNAME");
507
+ else if (env.BITBUCKET_BUILD_NUMBER) provider = pick("BITBUCKET_BRANCH");
508
+ if (provider) return normalizeRef(provider);
509
+ const trimmed = gitBranch?.trim();
510
+ if (trimmed && trimmed !== DETACHED_HEAD) return trimmed;
511
+ return void 0;
512
+ }
513
+ function normalizeRef(ref) {
514
+ return ref.replace(/^refs\/heads\//, "");
515
+ }
516
+
456
517
  // src/internal/support/setup-file.ts
457
518
  var path2 = __toESM(require("path"));
458
519
  var os3 = __toESM(require("os"));
@@ -500,17 +561,83 @@ function isListMode(argv = process.argv) {
500
561
  return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
501
562
  }
502
563
 
564
+ // src/internal/manifest/collect.ts
565
+ var fs4 = __toESM(require("fs"));
566
+ var path4 = __toESM(require("path"));
567
+ var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
568
+ var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
569
+ var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
570
+ function parseManifestJson(text) {
571
+ let parsed;
572
+ try {
573
+ parsed = JSON.parse(text);
574
+ } catch {
575
+ return null;
576
+ }
577
+ if (!parsed || typeof parsed !== "object") return null;
578
+ const obj = parsed;
579
+ const routes = Array.isArray(obj.routes) ? obj.routes.filter((r) => !!r && typeof r === "object").filter((r) => typeof r.method === "string" && typeof r.pattern === "string").map((r) => ({
580
+ method: r.method,
581
+ pattern: r.pattern,
582
+ handler: typeof r.handler === "string" ? r.handler : void 0,
583
+ responses: Array.isArray(r.responses) ? r.responses.filter((c) => Number.isInteger(c)) : void 0
584
+ })) : void 0;
585
+ const pages = Array.isArray(obj.pages) ? obj.pages.filter((p) => !!p && typeof p === "object").filter((p) => typeof p.pattern === "string").map((p) => ({ pattern: p.pattern, name: typeof p.name === "string" ? p.name : void 0 })) : void 0;
586
+ if (!routes && !pages) return null;
587
+ return { routes, pages };
588
+ }
589
+ function hasInstrumentationHeader(headers) {
590
+ const get = (name) => typeof headers.get === "function" ? headers.get(name) : headers[name];
591
+ return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
592
+ }
593
+ function readCommittedManifest(configDir) {
594
+ const file = path4.join(configDir, COMMITTED_MANIFEST_FILE);
595
+ try {
596
+ if (!fs4.existsSync(file)) return null;
597
+ return parseManifestJson(fs4.readFileSync(file, "utf8"));
598
+ } catch {
599
+ return null;
600
+ }
601
+ }
602
+ function configDirFromConfig(config) {
603
+ if (config?.configFile) return path4.dirname(config.configFile);
604
+ if (config?.rootDir) return config.rootDir;
605
+ return process.cwd();
606
+ }
607
+ function baseUrlFromConfig(config) {
608
+ const top = config?.use?.baseURL;
609
+ if (typeof top === "string" && top) return top;
610
+ for (const project of config?.projects ?? []) {
611
+ const url = project?.use?.baseURL;
612
+ if (typeof url === "string" && url) return url;
613
+ }
614
+ return null;
615
+ }
616
+ var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
617
+ async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
618
+ try {
619
+ const probe = await fetchImpl(baseUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
620
+ if (!hasInstrumentationHeader(probe.headers)) return null;
621
+ const manifestUrl = new URL(INSTRUMENTATION_MANIFEST_PATH, baseUrl).toString();
622
+ const res = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
623
+ if (!res.ok) return null;
624
+ return parseManifestJson(await res.text());
625
+ } catch {
626
+ return null;
627
+ }
628
+ }
629
+
503
630
  // src/public/global-setup.ts
504
631
  function createGlobalSetup(options, userSetup) {
505
632
  return async function globalSetupFn(config) {
506
- const piwiReporterPath = path4.resolve(__dirname, "./index.js");
633
+ const piwiReporterPath = path5.resolve(__dirname, "./index.js");
507
634
  let inlineReporterOptions = {};
508
635
  if (Array.isArray(config?.reporter)) {
509
636
  for (const r of config.reporter) {
510
637
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
511
638
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
512
639
  try {
513
- return path4.resolve(require.resolve(r[0])) === piwiReporterPath;
640
+ return path5.resolve(require.resolve(r[0])) === piwiReporterPath;
514
641
  } catch {
515
642
  return false;
516
643
  }
@@ -542,7 +669,7 @@ function createGlobalSetup(options, userSetup) {
542
669
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
543
670
  if (r[0].toLowerCase().includes("piwi")) return true;
544
671
  try {
545
- return path4.resolve(require.resolve(r[0])) === piwiReporterPath;
672
+ return path5.resolve(require.resolve(r[0])) === piwiReporterPath;
546
673
  } catch {
547
674
  return false;
548
675
  }
@@ -566,6 +693,7 @@ function createGlobalSetup(options, userSetup) {
566
693
  projectDescription: opts.projectDescription,
567
694
  environment: opts.environment || null,
568
695
  label: opts.label || null,
696
+ keep: opts.keep === true,
569
697
  startTime: (/* @__PURE__ */ new Date()).toISOString(),
570
698
  instanceId: computeInstanceId(opts.projectName, runLabel),
571
699
  shardIndex,
@@ -574,7 +702,7 @@ function createGlobalSetup(options, userSetup) {
574
702
  auth
575
703
  );
576
704
  if (response?.runId && response?.setupToken) {
577
- fs4.writeFileSync(
705
+ fs5.writeFileSync(
578
706
  getSetupFilePath(opts.projectName),
579
707
  JSON.stringify({
580
708
  runId: response.runId,
@@ -585,18 +713,43 @@ function createGlobalSetup(options, userSetup) {
585
713
  logger.debug(`Global setup: initializing run #${response.runId}`);
586
714
  }
587
715
  if (opts.projectName) clearAriaSampleFile(opts.projectName);
588
- if (opts.sampleAriaOnPass !== false && opts.projectName) {
716
+ const wantsAria = opts.sampleAriaOnPass !== false && !!opts.projectName;
717
+ const committedManifest = opts.uploadManifest !== false ? readCommittedManifest(configDirFromConfig(config)) : null;
718
+ const manifestBaseUrl = opts.uploadManifest !== false ? baseUrlFromConfig(config) : null;
719
+ const wantsManifest = opts.uploadManifest !== false && !!opts.projectName && (!!committedManifest || !!manifestBaseUrl);
720
+ let projectId;
721
+ if ((wantsAria || wantsManifest) && opts.projectName) {
589
722
  const menu = await httpClient.getJSON("/api/projects/menu", auth);
590
- const projectId = menu?.items?.find(
723
+ projectId = menu?.items?.find(
591
724
  (p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
592
725
  )?.id;
593
- if (projectId != null) {
594
- const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
595
- const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
596
- if (tests) {
597
- const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
598
- writeAriaSampleFile(opts.projectName, identities);
599
- logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
726
+ }
727
+ if (wantsAria && projectId != null) {
728
+ const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
729
+ const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
730
+ if (tests) {
731
+ const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
732
+ writeAriaSampleFile(opts.projectName, identities);
733
+ logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
734
+ }
735
+ }
736
+ if (wantsManifest && projectId != null) {
737
+ const branch = resolveScmBranch(process.env) ?? null;
738
+ if (committedManifest) {
739
+ await httpClient.putJSON(
740
+ `/api/projects/${projectId}/surface/manifest`,
741
+ { source: "committed", branch, manifest: committedManifest },
742
+ auth
743
+ ).catch((e) => logger.debug(`Manifest upload (committed) skipped: ${errorMessage(e)}`));
744
+ }
745
+ if (manifestBaseUrl) {
746
+ const declared = await fetchInstrumentationManifest(manifestBaseUrl);
747
+ if (declared) {
748
+ await httpClient.putJSON(
749
+ `/api/projects/${projectId}/surface/manifest`,
750
+ { source: "instrumentation", branch, manifest: declared },
751
+ auth
752
+ ).catch((e) => logger.debug(`Manifest upload (instrumentation) skipped: ${errorMessage(e)}`));
600
753
  }
601
754
  }
602
755
  }