@validate.qa/runner 1.0.101 → 1.0.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +3 -0
  2. package/dist/{chunk-IGXGSDE3.mjs → chunk-3JM6XTSZ.mjs} +1298 -384
  3. package/dist/{chunk-BBTK6OX6.mjs → chunk-4WLP3RWN.mjs} +383 -11
  4. package/dist/{chunk-XZUUBMP3.mjs → chunk-5DFBEL72.mjs} +1 -1
  5. package/dist/{chunk-M3VZGHZQ.mjs → chunk-BXLEPT4M.mjs} +20 -10
  6. package/dist/{chunk-FQUH2PU2.mjs → chunk-CBVHPYVJ.mjs} +62 -2
  7. package/dist/{chunk-4SZC5ZX3.mjs → chunk-DN3O6EYT.mjs} +19461 -4077
  8. package/dist/{chunk-SN3PN4G4.mjs → chunk-E2RRAE27.mjs} +6 -2
  9. package/dist/{chunk-2OKZY3WN.mjs → chunk-E2VEMBQQ.mjs} +2 -2
  10. package/dist/{chunk-CYWHPXNZ.mjs → chunk-HJ6FKHMM.mjs} +1 -1
  11. package/dist/{chunk-CO5KHIVX.mjs → chunk-PWFWMVZU.mjs} +1 -1
  12. package/dist/{chunk-TPU42VH2.mjs → chunk-ZZYBRIBT.mjs} +10 -5
  13. package/dist/cli.js +135916 -118926
  14. package/dist/cli.mjs +349 -101
  15. package/dist/credential-mcp-server.js +1327 -348
  16. package/dist/credential-mcp-server.mjs +3 -3
  17. package/dist/discovery-mcp-server.js +1328 -400
  18. package/dist/discovery-mcp-server.mjs +4 -4
  19. package/dist/dist-52M5RDZ6.mjs +17 -0
  20. package/dist/inbox-mcp-server.js +1579 -605
  21. package/dist/inbox-mcp-server.mjs +3 -3
  22. package/dist/route-seed-worker.js +1244 -330
  23. package/dist/route-seed-worker.mjs +4 -4
  24. package/dist/run-write-guard-preload.js +1622 -332
  25. package/dist/run-write-guard-preload.mjs +4 -4
  26. package/dist/run-write-guard.js +1622 -332
  27. package/dist/run-write-guard.mjs +3 -3
  28. package/dist/spec-driver.js +1 -1
  29. package/dist/spec-driver.mjs +1 -1
  30. package/package.json +3 -2
  31. package/dist/dist-VTPBY2C4.mjs +0 -17
package/dist/cli.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ init_public_address,
4
+ public_address_exports,
3
5
  require_ai_queue,
4
6
  require_api_auth_headers,
5
7
  require_api_call_redaction,
@@ -31,22 +33,23 @@ import {
31
33
  require_resource_governor,
32
34
  require_result_api_call_bodies,
33
35
  require_result_shed
34
- } from "./chunk-4SZC5ZX3.mjs";
36
+ } from "./chunk-DN3O6EYT.mjs";
35
37
  import {
36
38
  require_tar_isolated
37
39
  } from "./chunk-AAUHXGDS.mjs";
38
40
  import "./chunk-EXRFHATN.mjs";
39
41
  import {
40
- require_browser_config
41
- } from "./chunk-TPU42VH2.mjs";
42
- import "./chunk-M3VZGHZQ.mjs";
43
- import "./chunk-XZUUBMP3.mjs";
44
- import "./chunk-FQUH2PU2.mjs";
45
- import "./chunk-CYWHPXNZ.mjs";
46
- import "./chunk-CO5KHIVX.mjs";
47
- import "./chunk-2OKZY3WN.mjs";
48
- import "./chunk-BBTK6OX6.mjs";
49
- import "./chunk-SN3PN4G4.mjs";
42
+ require_browser_config,
43
+ require_cloud_runner
44
+ } from "./chunk-ZZYBRIBT.mjs";
45
+ import "./chunk-BXLEPT4M.mjs";
46
+ import "./chunk-5DFBEL72.mjs";
47
+ import "./chunk-CBVHPYVJ.mjs";
48
+ import "./chunk-HJ6FKHMM.mjs";
49
+ import "./chunk-PWFWMVZU.mjs";
50
+ import "./chunk-E2VEMBQQ.mjs";
51
+ import "./chunk-4WLP3RWN.mjs";
52
+ import "./chunk-E2RRAE27.mjs";
50
53
  import {
51
54
  LIVE_FRAME_IDLE_PUSH_INTERVAL_MS,
52
55
  MOBILE_COLD_BOOT_SETTLE_MS,
@@ -81,18 +84,175 @@ import {
81
84
  stepAppliesToPlatform,
82
85
  stepsMissingPlatformLocators,
83
86
  stepsNeedingReviewForPlatform
84
- } from "./chunk-IGXGSDE3.mjs";
87
+ } from "./chunk-3JM6XTSZ.mjs";
85
88
  import {
86
89
  __commonJS,
90
+ __require,
91
+ __toCommonJS,
87
92
  __toESM
88
93
  } from "./chunk-WQXBGQU4.mjs";
89
94
 
95
+ // ../runner-core/dist/utils/playwright-signal-handlers.js
96
+ var require_playwright_signal_handlers = __commonJS({
97
+ "../runner-core/dist/utils/playwright-signal-handlers.js"(exports) {
98
+ "use strict";
99
+ Object.defineProperty(exports, "__esModule", { value: true });
100
+ exports.patchBrowserTypeSignalDefaults = patchBrowserTypeSignalDefaults;
101
+ exports.disablePlaywrightSignalHandlers = disablePlaywrightSignalHandlers2;
102
+ var playwright_1 = __require("playwright");
103
+ var PATCHED = /* @__PURE__ */ Symbol.for("validateqa.playwrightSignalDefaults");
104
+ var LAUNCH_METHODS = { launch: 0, launchPersistentContext: 1 };
105
+ function patchBrowserTypeSignalDefaults(browserType) {
106
+ const target = browserType;
107
+ if (target[PATCHED])
108
+ return;
109
+ for (const [method, optionsIndex] of Object.entries(LAUNCH_METHODS)) {
110
+ const original = target[method];
111
+ if (typeof original !== "function")
112
+ continue;
113
+ target[method] = function(...args) {
114
+ const given = args[optionsIndex];
115
+ args[optionsIndex] = { handleSIGINT: false, handleSIGTERM: false, ...given && typeof given === "object" ? given : {} };
116
+ return original.apply(this, args);
117
+ };
118
+ }
119
+ target[PATCHED] = true;
120
+ }
121
+ function disablePlaywrightSignalHandlers2() {
122
+ patchBrowserTypeSignalDefaults(Object.getPrototypeOf(playwright_1.chromium));
123
+ }
124
+ }
125
+ });
126
+
127
+ // ../runner-core/dist/utils/tls-cert-probe.js
128
+ var require_tls_cert_probe = __commonJS({
129
+ "../runner-core/dist/utils/tls-cert-probe.js"(exports) {
130
+ "use strict";
131
+ var __importDefault = exports && exports.__importDefault || function(mod) {
132
+ return mod && mod.__esModule ? mod : { "default": mod };
133
+ };
134
+ Object.defineProperty(exports, "__esModule", { value: true });
135
+ exports.RUN_NOTE_WAIT_MS = void 0;
136
+ exports.probeTargetCertificate = probeTargetCertificate;
137
+ exports.tlsCertFindingLine = tlsCertFindingLine;
138
+ exports.targetCertificateNote = targetCertificateNote2;
139
+ exports.noteWithinCap = noteWithinCap2;
140
+ exports.withRunNote = withRunNote2;
141
+ var node_dns_1 = __require("dns");
142
+ var node_net_1 = __importDefault(__require("net"));
143
+ var node_tls_1 = __importDefault(__require("tls"));
144
+ var public_address_1 = (init_public_address(), __toCommonJS(public_address_exports));
145
+ var cloud_runner_js_1 = require_cloud_runner();
146
+ var PROBE_TIMEOUT_MS = 5e3;
147
+ var CERT_ERROR_CODES = /* @__PURE__ */ new Set([
148
+ "CERT_HAS_EXPIRED",
149
+ "CERT_NOT_YET_VALID",
150
+ "CERT_REVOKED",
151
+ "CERT_REJECTED",
152
+ "CERT_UNTRUSTED",
153
+ "CERT_SIGNATURE_FAILURE",
154
+ "CERT_CHAIN_TOO_LONG",
155
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
156
+ "SELF_SIGNED_CERT_IN_CHAIN",
157
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
158
+ "UNABLE_TO_GET_ISSUER_CERT",
159
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
160
+ "UNABLE_TO_DECRYPT_CERT_SIGNATURE",
161
+ "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY",
162
+ "ERROR_IN_CERT_NOT_BEFORE_FIELD",
163
+ "ERROR_IN_CERT_NOT_AFTER_FIELD",
164
+ "INVALID_CA",
165
+ "INVALID_PURPOSE",
166
+ "PATH_LENGTH_EXCEEDED",
167
+ "HOSTNAME_MISMATCH",
168
+ "ERR_TLS_CERT_ALTNAME_INVALID"
169
+ ]);
170
+ async function probeTargetCertificate(baseUrl, opts = {}) {
171
+ let url;
172
+ try {
173
+ url = new URL(baseUrl);
174
+ } catch {
175
+ return null;
176
+ }
177
+ if (url.protocol !== "https:" || !url.hostname)
178
+ return null;
179
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
180
+ const port = url.port ? Number(url.port) : 443;
181
+ const shared = opts.shared ?? (0, cloud_runner_js_1.isSharedCloudRunner)();
182
+ const ipLiteral = node_net_1.default.isIP(hostname) !== 0;
183
+ if (shared && ipLiteral && (0, public_address_1.isPrivateOrLocalIpAddress)(hostname))
184
+ return null;
185
+ return new Promise((resolve3) => {
186
+ let settled = false;
187
+ let socket = null;
188
+ const finish = (finding) => {
189
+ if (settled)
190
+ return;
191
+ settled = true;
192
+ clearTimeout(timer);
193
+ socket?.destroy();
194
+ resolve3(finding);
195
+ };
196
+ const timer = setTimeout(() => finish(null), opts.timeoutMs ?? PROBE_TIMEOUT_MS);
197
+ try {
198
+ socket = node_tls_1.default.connect({
199
+ host: hostname,
200
+ port,
201
+ ...ipLiteral ? {} : { servername: hostname },
202
+ rejectUnauthorized: true,
203
+ ...opts.ca ? { ca: opts.ca } : {},
204
+ ...shared ? { lookup: (0, public_address_1.createPublicOnlyLookup)((name, o, cb) => (0, node_dns_1.lookup)(name, o, cb)) } : {},
205
+ ALPNProtocols: ["http/1.1"]
206
+ });
207
+ } catch {
208
+ finish(null);
209
+ return;
210
+ }
211
+ socket.once("secureConnect", () => finish(null));
212
+ socket.once("error", (err) => {
213
+ const code = typeof err.code === "string" ? err.code : "";
214
+ finish(CERT_ERROR_CODES.has(code) ? { host: url.host, code, detail: err.message.replace(/\s+/g, " ").trim().slice(0, 300) } : null);
215
+ });
216
+ });
217
+ }
218
+ function tlsCertFindingLine(finding) {
219
+ return `\u26A0\uFE0F TLS certificate: ${finding.host} presented a certificate that strict TLS rejects (${finding.code}: ${finding.detail}). The run went ahead because test browsers ignore HTTPS errors, so this is not a failure. A visitor's browser may show a certificate warning: this check does not fetch a missing intermediate certificate and does not use the operating system's trust store (a private CA the visitor's machine trusts), so a browser can accept a certificate it rejects.`;
220
+ }
221
+ async function targetCertificateNote2(baseUrl, opts = {}) {
222
+ try {
223
+ const finding = await probeTargetCertificate(baseUrl, opts);
224
+ return finding ? tlsCertFindingLine(finding) : null;
225
+ } catch {
226
+ return null;
227
+ }
228
+ }
229
+ exports.RUN_NOTE_WAIT_MS = 250;
230
+ async function noteWithinCap2(note, capMs = exports.RUN_NOTE_WAIT_MS) {
231
+ let timer;
232
+ const cap = new Promise((resolve3) => {
233
+ timer = setTimeout(() => resolve3(null), capMs);
234
+ });
235
+ try {
236
+ return await Promise.race([note.catch(() => null), cap]);
237
+ } finally {
238
+ clearTimeout(timer);
239
+ }
240
+ }
241
+ function withRunNote2(result, note) {
242
+ if (!note)
243
+ return result;
244
+ return { ...result, logs: result.logs ? `${note}
245
+ ${result.logs}` : note };
246
+ }
247
+ }
248
+ });
249
+
90
250
  // package.json
91
251
  var require_package = __commonJS({
92
252
  "package.json"(exports, module) {
93
253
  module.exports = {
94
254
  name: "@validate.qa/runner",
95
- version: "1.0.101",
255
+ version: "1.0.102",
96
256
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
97
257
  bin: {
98
258
  "validate-runner": "dist/cli.js"
@@ -100,7 +260,8 @@ var require_package = __commonJS({
100
260
  scripts: {
101
261
  build: "tsup",
102
262
  dev: "tsx src/cli.ts",
103
- test: "node --import tsx --test src/supervisor-child-argv.test.ts src/audit-scrub.test.ts src/retry-on-429.test.ts src/version-check.test.ts src/poll-interval.test.ts src/node-version-notice.test.ts src/services/auth-state.test.ts src/run-no-longer-active.test.ts src/mode-abort-no-verdict.test.ts src/discovery-render-type.test.ts src/batch-heal-no-start.test.ts src/heartbeat-active-runs.test.ts src/result-post-structure-413.test.ts src/result-shed-routes-413.test.ts src/result-post-retry.test.ts src/result-media-stale-epoch.test.ts src/run-write-guard-bundle.test.ts src/spec-driver-bundle.test.ts src/mcp-servers-bundle.test.ts src/codex-files-mcp-server-bundle.test.ts src/claim-nonce-not-logged.test.ts src/sign-in-capture.test.ts src/run-status-verdicts.test.ts src/sign-in-failure.test.ts src/mobile-ui-path-succeeded.test.ts src/supervisor-policy.test.ts src/login-check.test.ts src/heal-post-lost-ack.test.ts src/heal-log-stream-chunks.test.ts src/shutdown-claim-window.test.ts",
263
+ lint: "eslint .",
264
+ test: "node --import tsx --test src/supervisor-child-argv.test.ts src/audit-scrub.test.ts src/retry-on-429.test.ts src/version-check.test.ts src/poll-interval.test.ts src/node-version-notice.test.ts src/services/auth-state.test.ts src/run-no-longer-active.test.ts src/mode-abort-no-verdict.test.ts src/discovery-render-type.test.ts src/batch-heal-no-start.test.ts src/heartbeat-active-runs.test.ts src/result-post-structure-413.test.ts src/result-shed-routes-413.test.ts src/result-post-retry.test.ts src/result-media-stale-epoch.test.ts src/run-write-guard-bundle.test.ts src/spec-driver-bundle.test.ts src/mcp-servers-bundle.test.ts src/codex-files-mcp-server-bundle.test.ts src/claim-nonce-not-logged.test.ts src/sign-in-capture.test.ts src/run-status-verdicts.test.ts src/sign-in-failure.test.ts src/mobile-ui-path-succeeded.test.ts src/supervisor-policy.test.ts src/login-check.test.ts src/heal-post-lost-ack.test.ts src/ui-heal-result-video.test.ts src/heal-log-stream-chunks.test.ts src/shutdown-claim-window.test.ts src/result-post-redeploy-budget.test.ts src/shutdown-signals.test.ts",
104
265
  "test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/runner.mobile-heal-audit-wiring.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/runner-watchdog.test.ts src/result-payload-budget.test.ts src/result-media-upload.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
105
266
  "smoke:installed": "node scripts/installed-run-smoke.mjs",
106
267
  "check:packed-mcp": "node scripts/packed-install-check.mjs",
@@ -320,6 +481,7 @@ var import_grok_repo_enrichment_runner = __toESM(require_grok_repo_enrichment_ru
320
481
  var import_repo_brief_execution = __toESM(require_repo_brief_execution());
321
482
  var import_repo_read_log = __toESM(require_repo_read_log());
322
483
  var import_cli_session_failure = __toESM(require_cli_session_failure());
484
+ var import_playwright_signal_handlers = __toESM(require_playwright_signal_handlers());
323
485
  var import_cli_limit = __toESM(require_cli_limit());
324
486
  var import_fetch_cli_subscription = __toESM(require_fetch_cli_subscription());
325
487
  var import_codex_cli = __toESM(require_codex_cli());
@@ -381,6 +543,7 @@ var EXIT_TERMINAL = 64;
381
543
  var SUPERVISOR_WINDOW_MS = 36e5;
382
544
  var SUPERVISOR_HEALTHY_MS = 10 * 6e4;
383
545
  function nextSupervisorAction(input, state2, limits) {
546
+ if (input.signalled) return { action: "stop", exitCode: 0, reason: "clean" };
384
547
  if (input.code === 0) return { action: "stop", exitCode: 0, reason: "clean" };
385
548
  if (input.code === EXIT_TERMINAL) return { action: "stop", exitCode: EXIT_TERMINAL, reason: "terminal" };
386
549
  const restartTimes = [...state2.restartTimes.filter((t) => input.now - t < SUPERVISOR_WINDOW_MS), input.now];
@@ -532,6 +695,24 @@ function upgradeAction(activeRuns) {
532
695
  return activeRuns > 0 ? "drain" : "exit";
533
696
  }
534
697
 
698
+ // src/shutdown-signals.ts
699
+ var REPEAT_SIGNAL_WINDOW_MS = 1e3;
700
+ function installShutdownSignals(target, onShutdown, opts) {
701
+ const now = opts.now ?? Date.now;
702
+ const windowMs = opts.repeatWindowMs ?? REPEAT_SIGNAL_WINDOW_MS;
703
+ let firstAt = null;
704
+ const handle = () => {
705
+ if (firstAt === null) {
706
+ firstAt = now();
707
+ onShutdown();
708
+ return;
709
+ }
710
+ if (now() - firstAt > windowMs) opts.forceExit();
711
+ };
712
+ target.on("SIGINT", handle);
713
+ target.on("SIGTERM", handle);
714
+ }
715
+
535
716
  // src/runner.ts
536
717
  init_dist();
537
718
 
@@ -1627,7 +1808,8 @@ async function clearField(sessionPath, bounds) {
1627
1808
  } catch (err) {
1628
1809
  if (isAppiumDriverDeadError(err)) throw err;
1629
1810
  throw new Error(
1630
- `No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`
1811
+ `No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`,
1812
+ { cause: err }
1631
1813
  );
1632
1814
  }
1633
1815
  }
@@ -3857,7 +4039,7 @@ async function getWindowSize2(sessionId, retries = 3) {
3857
4039
  } catch (err) {
3858
4040
  if (attempt === retries - 1) {
3859
4041
  if (isTransportError(err)) throw err;
3860
- throw new Error(`Failed to get window size after ${retries} attempts: ${err instanceof Error ? err.message : String(err)}`);
4042
+ throw new Error(`Failed to get window size after ${retries} attempts: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
3861
4043
  }
3862
4044
  await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * 100));
3863
4045
  }
@@ -5224,7 +5406,7 @@ async function executeSwipe(sessionId, step) {
5224
5406
  screenH = windowSize.height;
5225
5407
  } catch (err) {
5226
5408
  if (isTransportError(err)) throw err;
5227
- throw new Error(`Swipe action failed: could not determine screen dimensions. ${err instanceof Error ? err.message : String(err)}`);
5409
+ throw new Error(`Swipe action failed: could not determine screen dimensions. ${err instanceof Error ? err.message : String(err)}`, { cause: err });
5228
5410
  }
5229
5411
  const { startX, startY, endX, endY } = computeMobileSwipePointers(screenW, screenH, direction, distance);
5230
5412
  await wdRequest(`/session/${sessionId}/actions`, {
@@ -5543,12 +5725,12 @@ async function executeMobileTest(options) {
5543
5725
  const base = err instanceof Error ? err.message : String(err);
5544
5726
  if (selectedDevice.isPhysical) {
5545
5727
  const guidance = platformFilter === "IOS" ? `Real iOS devices require a signed WebDriverAgent (configure signing in Xcode, or pass appium:xcodeOrgId / appium:xcodeSigningId) and the app (${options.target.appId}) must be installed and trusted on the device.` : `Ensure ${selectedDevice.name} is authorized for USB debugging and ${options.target.appId} is installed.`;
5546
- throw new Error(`${base} \u2014 could not start an Appium session on the physical device ${selectedDevice.name}. ${guidance}`);
5728
+ throw new Error(`${base} \u2014 could not start an Appium session on the physical device ${selectedDevice.name}. ${guidance}`, { cause: err });
5547
5729
  }
5548
5730
  const appListUnknown = !selectedDevice.installedApps || selectedDevice.installedApps.length === 0;
5549
5731
  if (appListUnknown) {
5550
5732
  const installHint = platformFilter === "IOS" ? `xcrun simctl install ${selectedDevice.id} <app-path>` : `adb -s ${selectedDevice.id} install <apk-path>`;
5551
- throw new Error(`${base} \u2014 could not confirm ${options.target.appId} is installed on ${selectedDevice.name}; if it is missing, install it first: ${installHint}`);
5733
+ throw new Error(`${base} \u2014 could not confirm ${options.target.appId} is installed on ${selectedDevice.name}; if it is missing, install it first: ${installHint}`, { cause: err });
5552
5734
  }
5553
5735
  throw err;
5554
5736
  }
@@ -5588,7 +5770,7 @@ async function executeMobileTest(options) {
5588
5770
  detail,
5589
5771
  recordedFromClearedState
5590
5772
  );
5591
- if (requirementError) throw new Error(requirementError);
5773
+ if (requirementError) throw new Error(requirementError, { cause: err });
5592
5774
  log2(`App-data clear failed (AUTO continues with plain fresh launch): ${detail}`);
5593
5775
  }
5594
5776
  } else {
@@ -5623,7 +5805,8 @@ async function executeMobileTest(options) {
5623
5805
  await openDeepLink(sessionId, options.target.platform, options.target.deeplink, options.target.appId);
5624
5806
  } catch (err) {
5625
5807
  throw new Error(
5626
- `Deep-link launch failed; refusing to replay from the wrong entry screen: ${err instanceof Error ? err.message : String(err)}`
5808
+ `Deep-link launch failed; refusing to replay from the wrong entry screen: ${err instanceof Error ? err.message : String(err)}`,
5809
+ { cause: err }
5627
5810
  );
5628
5811
  }
5629
5812
  }
@@ -5904,7 +6087,8 @@ async function createMobileDiscoverySession(target, options) {
5904
6087
  });
5905
6088
  releaseControlPortSlot(discoveryPortSlotDeviceId);
5906
6089
  throw new Error(
5907
- `Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`
6090
+ `Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`,
6091
+ { cause: err }
5908
6092
  );
5909
6093
  }
5910
6094
  }
@@ -6960,7 +7144,8 @@ async function startSession(validated, res) {
6960
7144
  } catch (err) {
6961
7145
  const detail = err instanceof Error ? err.message : String(err);
6962
7146
  throw new Error(
6963
- `Failed to open the configured deep link before recording (${detail}). The Appium session was closed; verify the URL and app deep-link configuration, then try again.`
7147
+ `Failed to open the configured deep link before recording (${detail}). The Appium session was closed; verify the URL and app deep-link configuration, then try again.`,
7148
+ { cause: err }
6964
7149
  );
6965
7150
  }
6966
7151
  }
@@ -8888,23 +9073,42 @@ function withMediaReferences(result, refs) {
8888
9073
  // src/result-post-retry.ts
8889
9074
  var RESULT_POST_MAX_ATTEMPTS = 3;
8890
9075
  var RESULT_POST_BACKOFF_BASE_MS = 1e3;
9076
+ var RESULT_POST_BACKOFF_CAP_MS = 15e3;
9077
+ var RESULT_POST_RETRY_BUDGET_DEFAULT_MS = 15e4;
9078
+ function resultPostRetryBudgetMs(env = process.env) {
9079
+ return Math.max(1e4, Number.parseInt(env.RUNNER_RESULT_RETRY_BUDGET_MS ?? "", 10) || RESULT_POST_RETRY_BUDGET_DEFAULT_MS);
9080
+ }
9081
+ function defaultResultPostBudgetMs(backoffBaseMs) {
9082
+ return backoffBaseMs > 0 ? resultPostRetryBudgetMs() : 0;
9083
+ }
9084
+ function nextResultPostDelay(attempt, opts) {
9085
+ const delay = Math.min(RESULT_POST_BACKOFF_CAP_MS, Math.pow(2, attempt) * opts.backoffBaseMs);
9086
+ if (attempt < opts.maxAttempts) return delay;
9087
+ const left = opts.startedAt + opts.budgetMs - (opts.now ?? Date.now());
9088
+ if (left <= 0) return null;
9089
+ return Math.min(delay, left);
9090
+ }
8891
9091
  function isTransientResultPostStatus(status) {
8892
9092
  return status === 408 || status === 429 || status >= 500;
8893
9093
  }
8894
- async function postRetryingTransient(send, label, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
9094
+ async function postRetryingTransient(send, label, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS, budgetMs = defaultResultPostBudgetMs(backoffBaseMs)) {
9095
+ const startedAt = Date.now();
8895
9096
  for (let attempt = 1; ; attempt++) {
8896
9097
  let failure;
9098
+ let delay;
8897
9099
  try {
8898
9100
  const res = await send();
8899
- if (!isTransientResultPostStatus(res.status) || attempt >= RESULT_POST_MAX_ATTEMPTS) return res;
9101
+ if (!isTransientResultPostStatus(res.status)) return res;
9102
+ delay = nextResultPostDelay(attempt, { maxAttempts: RESULT_POST_MAX_ATTEMPTS, startedAt, budgetMs, backoffBaseMs });
9103
+ if (delay === null) return res;
8900
9104
  await res.body?.cancel().catch(() => {
8901
9105
  });
8902
9106
  failure = `HTTP ${res.status}`;
8903
9107
  } catch (err) {
8904
- if (attempt >= RESULT_POST_MAX_ATTEMPTS) throw err;
9108
+ delay = nextResultPostDelay(attempt, { maxAttempts: RESULT_POST_MAX_ATTEMPTS, startedAt, budgetMs, backoffBaseMs });
9109
+ if (delay === null) throw err;
8905
9110
  failure = err instanceof Error ? err.message : String(err);
8906
9111
  }
8907
- const delay = Math.pow(2, attempt) * backoffBaseMs;
8908
9112
  console.warn(`[runner] ${label} post attempt ${attempt} failed (${failure}), retrying in ${delay}ms...`);
8909
9113
  await new Promise((resolve3) => setTimeout(resolve3, delay));
8910
9114
  }
@@ -8937,6 +9141,7 @@ function clampPollInterval(value) {
8937
9141
  init_dist();
8938
9142
  var import_ai_queue = __toESM(require_ai_queue());
8939
9143
  var import_base_url_check = __toESM(require_base_url_check());
9144
+ var import_tls_cert_probe = __toESM(require_tls_cert_probe());
8940
9145
  var import_rate_limit_detector = __toESM(require_rate_limit_detector());
8941
9146
  var import_resource_governor = __toESM(require_resource_governor());
8942
9147
  var import_process_inventory = __toESM(require_process_inventory());
@@ -9151,7 +9356,7 @@ async function claimRun(serverUrl, headers, runId) {
9151
9356
  if (typeof body.claimNonce === "string" && body.claimNonce) claimNonces.set(runId, body.claimNonce);
9152
9357
  return body.claimEpoch;
9153
9358
  }
9154
- async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAttemptsArg = 3, backoffBaseMs = 1e3) {
9359
+ async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAttemptsArg = 3, backoffBaseMs = 1e3, budgetMs = defaultResultPostBudgetMs(backoffBaseMs)) {
9155
9360
  let maxAttempts = maxAttemptsArg;
9156
9361
  let verdictOnlyApplied = false;
9157
9362
  let lastError;
@@ -9170,8 +9375,10 @@ async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAtte
9170
9375
  }
9171
9376
  let payload = budgeted.payload;
9172
9377
  let apiCallBodiesStripped = false;
9378
+ const startedAt = Date.now();
9173
9379
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
9174
9380
  attemptsMade = attempt;
9381
+ let transient = false;
9175
9382
  try {
9176
9383
  const res = await fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/result`, () => ({
9177
9384
  method: "POST",
@@ -9223,15 +9430,17 @@ async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAtte
9223
9430
  throw new RunNoLongerActiveError(`Run ${runId} is no longer active on the server (HTTP ${res.status})`);
9224
9431
  }
9225
9432
  lastError = new Error(`HTTP ${res.status}`);
9433
+ transient = isTransientResultPostStatus(res.status);
9226
9434
  } catch (err) {
9227
9435
  if (err instanceof RunNoLongerActiveError) throw err;
9228
9436
  lastError = err instanceof Error ? err : new Error(String(err));
9437
+ transient = true;
9229
9438
  }
9230
- if (attempt < maxAttempts) {
9231
- const delay = Math.pow(2, attempt) * backoffBaseMs;
9232
- console.warn(`[runner] postResult attempt ${attempt} failed (${lastError.message}), retrying in ${delay}ms...`);
9233
- await new Promise((resolve3) => setTimeout(resolve3, delay));
9234
- }
9439
+ const delay = nextResultPostDelay(attempt, { maxAttempts, startedAt, budgetMs: transient ? budgetMs : 0, backoffBaseMs });
9440
+ if (delay === null) break;
9441
+ if (attempt >= maxAttempts) maxAttempts++;
9442
+ console.warn(`[runner] postResult attempt ${attempt} failed (${lastError.message}), retrying in ${delay}ms...`);
9443
+ await new Promise((resolve3) => setTimeout(resolve3, delay));
9235
9444
  }
9236
9445
  throw new Error(`Failed to post result after ${attemptsMade} attempts: ${lastError?.message}`);
9237
9446
  }
@@ -9288,12 +9497,13 @@ async function postMobileHealResult(serverUrl, headers, runId, body, claimEpoch)
9288
9497
  function narrowMobileLaunchMode(mode) {
9289
9498
  return mode === "LAUNCH_APP" || mode === "DEEP_LINK" || mode === "RESUME" ? mode : void 0;
9290
9499
  }
9291
- async function postBatchHealResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
9500
+ async function postBatchHealResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS, claimEpoch) {
9292
9501
  const progress = { body, fromRung: 0 };
9293
9502
  const res = await postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/batch-heal-result`, () => ({
9294
9503
  method: "POST",
9295
9504
  headers,
9296
- body: JSON.stringify(payload),
9505
+ // At send time, not in the body: the verdict-only rung keeps a fixed list of fields and would drop it.
9506
+ body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
9297
9507
  signal: AbortSignal.timeout(12e4)
9298
9508
  })), progress.body, "batch heal result", import_result_shed.BATCH_HEAL_SHED_LADDER, progress), "batch heal result", backoffBaseMs);
9299
9509
  throwUnlessResultLanded(res, runId, "batch heal result");
@@ -9305,11 +9515,11 @@ function throwUnlessResultLanded(res, runId, label) {
9305
9515
  }
9306
9516
  throw new Error(`Failed to post ${label}: ${res.status}`);
9307
9517
  }
9308
- async function postBatchHealPartial(serverUrl, headers, runId, result) {
9518
+ async function postBatchHealPartial(serverUrl, headers, runId, result, claimEpoch) {
9309
9519
  const res = await postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/batch-heal-partial`, () => ({
9310
9520
  method: "POST",
9311
9521
  headers,
9312
- body: JSON.stringify(payload),
9522
+ body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
9313
9523
  // Fires per verdict: a slow server must not hold up the next test.
9314
9524
  signal: AbortSignal.timeout(3e4)
9315
9525
  })), { result }, "batch heal partial", import_result_shed.BATCH_HEAL_PARTIAL_SHED_LADDER);
@@ -9330,11 +9540,11 @@ async function fetchBatchHealAppliedIds(serverUrl, headers, runId, projectId) {
9330
9540
  return /* @__PURE__ */ new Set();
9331
9541
  }
9332
9542
  }
9333
- function batchHealPartialHooks(serverUrl, headers, runId, projectId, warn = (msg) => log.warn(msg)) {
9543
+ function batchHealPartialHooks(serverUrl, headers, runId, projectId, warn = (msg) => log.warn(msg), claimEpoch) {
9334
9544
  return {
9335
9545
  onPartialResult: async (result) => {
9336
9546
  try {
9337
- await postBatchHealPartial(serverUrl, headers, runId, result);
9547
+ await postBatchHealPartial(serverUrl, headers, runId, result, claimEpoch);
9338
9548
  } catch (err) {
9339
9549
  warn(`batch-heal partial post failed for ${result.testCaseId}: ${err instanceof Error ? err.message : String(err)}`);
9340
9550
  }
@@ -9430,18 +9640,25 @@ function createHealLogStreamer(serverUrl, headers, runId) {
9430
9640
  };
9431
9641
  return { onLog, flushNow };
9432
9642
  }
9433
- async function postShedOn413(send, body, label, ladder = import_result_shed.CAPTURE_SHED_LADDER, progress) {
9643
+ async function postShedOn413(send, body, label, ladder = import_result_shed.CAPTURE_SHED_LADDER, progress, onPlain413) {
9434
9644
  let current = progress?.body ?? body;
9435
9645
  let fromRung = progress?.fromRung ?? 0;
9436
9646
  let res = await send(current);
9437
- while (res.status === 413 && await (0, import_result_api_call_bodies2.isResultStructureTooCostlyResponse)(res)) {
9438
- const next = (0, import_result_shed.nextShedPayload)(current, ladder, fromRung);
9439
- if (!next) break;
9440
- console.warn(
9441
- `[runner] Server refused ${label} as too costly to parse (413 ${import_result_api_call_bodies2.RESULT_STRUCTURE_TOO_COSTLY}); retrying a lighter form (shed level ${next.level}, ${next.shed} item${next.shed === 1 ? "" : "s"} removed).`
9442
- );
9443
- current = next.payload;
9444
- fromRung = next.nextRung;
9647
+ while (res.status === 413) {
9648
+ if (await (0, import_result_api_call_bodies2.isResultStructureTooCostlyResponse)(res)) {
9649
+ const next = (0, import_result_shed.nextShedPayload)(current, ladder, fromRung);
9650
+ if (!next) break;
9651
+ console.warn(
9652
+ `[runner] Server refused ${label} as too costly to parse (413 ${import_result_api_call_bodies2.RESULT_STRUCTURE_TOO_COSTLY}); retrying a lighter form (shed level ${next.level}, ${next.shed} item${next.shed === 1 ? "" : "s"} removed).`
9653
+ );
9654
+ current = next.payload;
9655
+ fromRung = next.nextRung;
9656
+ } else {
9657
+ const lighter = onPlain413?.(current) ?? null;
9658
+ if (!lighter) break;
9659
+ console.warn(`[runner] Server refused ${label} as too large (413); retrying it without its media.`);
9660
+ current = lighter;
9661
+ }
9445
9662
  if (progress) {
9446
9663
  progress.body = current;
9447
9664
  progress.fromRung = fromRung;
@@ -9460,22 +9677,24 @@ async function postCaptureResult(serverUrl, headers, runId, result, backoffBaseM
9460
9677
  })), progress.body, "capture result", import_result_shed.CAPTURE_SHED_LADDER, progress), "capture result", backoffBaseMs);
9461
9678
  throwUnlessResultLanded(res, runId, "capture result");
9462
9679
  }
9463
- async function postRegressionResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
9464
- const progress = { body: withBrowsersPathHint(body), fromRung: 0 };
9680
+ async function postRegressionResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS, claimEpoch) {
9681
+ const fit = (0, import_result_shed.fitRegressionScreenshots)(withBrowsersPathHint(body));
9682
+ if (fit.dropped > 0) console.warn(`[runner] Regression result over the route's body limit: ${fit.dropped} failing spec(s)' screenshot left out.`);
9683
+ const progress = { body: fit.payload, fromRung: 0 };
9465
9684
  const res = await postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/regression-result`, () => ({
9466
9685
  method: "POST",
9467
9686
  headers,
9468
- body: JSON.stringify(payload),
9687
+ body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
9469
9688
  signal: AbortSignal.timeout(6e4)
9470
- })), progress.body, "regression result", import_result_shed.REGRESSION_SHED_LADDER, progress), "regression result", backoffBaseMs);
9689
+ })), progress.body, "regression result", import_result_shed.REGRESSION_SHED_LADDER, progress, import_result_shed.withoutRegressionScreenshots), "regression result", backoffBaseMs);
9471
9690
  throwUnlessResultLanded(res, runId, "regression result");
9472
9691
  }
9473
- function postLiveSuiteResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
9692
+ function postLiveSuiteResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS, claimEpoch) {
9474
9693
  const progress = { body, fromRung: 0 };
9475
9694
  return postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/live-suite-result`, () => ({
9476
9695
  method: "POST",
9477
9696
  headers,
9478
- body: JSON.stringify(payload),
9697
+ body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
9479
9698
  signal: AbortSignal.timeout(6e4)
9480
9699
  })), progress.body, "live suite result", import_result_shed.LIVE_SUITE_SHED_LADDER, progress), "live suite result", backoffBaseMs);
9481
9700
  }
@@ -9881,7 +10100,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
9881
10100
  const runLabel = run2.testCaseId ?? run2.testName ?? (runMode === "run" ? "auth-setup" : runMode);
9882
10101
  const modeLabel = runMode === "run" ? RUNNER_MODE : runMode;
9883
10102
  log.info(`Running test: ${runLabel} (run ${run2.runId}) [mode: ${modeLabel}]`);
9884
- const postRunResult = (result) => postResult(opts.serverUrl, headers, run2.runId, result, run2.claimEpoch);
10103
+ let tlsCertNote = Promise.resolve(null);
10104
+ const postRunResult = async (result) => postResult(opts.serverUrl, headers, run2.runId, (0, import_tls_cert_probe.withRunNote)(result, await (0, import_tls_cert_probe.noteWithinCap)(tlsCertNote)), run2.claimEpoch);
9885
10105
  const browserlessRepositoryRead = (0, import_live_browser_registry.isBrowserlessRepositoryRead)(run2.discoveryContext?.frameworkImport?.kind);
9886
10106
  const liveBrowserHandle = browserlessRepositoryRead ? import_live_browser_registry.NO_LIVE_BROWSER : import_live_browser_registry.liveBrowserRegistry.register({
9887
10107
  sessionId: `${run2.runId}`,
@@ -9929,6 +10149,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
9929
10149
  authStandingMode: authPlan.standingHeadersMode,
9930
10150
  storageStatePath: wrapperStorageStatePath,
9931
10151
  apiBaseUrl: run2.apiBaseUrl,
10152
+ // The run's own @api tag: an imported API spec's code may not say so (see executeNativeOnce's apiTest).
10153
+ apiTest: Array.isArray(run2.tags) && run2.tags.includes("@api"),
9932
10154
  ...nativeOptions,
9933
10155
  liveBrowserHandle,
9934
10156
  onLiveBrowserSnapshot: requestLiveBrowserHeartbeat,
@@ -10003,6 +10225,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10003
10225
  } else if (preflight.note) {
10004
10226
  log.info(preflight.note);
10005
10227
  }
10228
+ tlsCertNote = (0, import_tls_cert_probe.targetCertificateNote)(run2.baseUrl);
10229
+ void tlsCertNote.then((note) => {
10230
+ if (note) log.warn(note);
10231
+ });
10006
10232
  }
10007
10233
  if (runMode === "auth-setup") {
10008
10234
  if (!run2.playwrightCode) {
@@ -10083,7 +10309,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10083
10309
  log.info(`Heal engine: GROK BATCH (${tests.length} tests, healerModel=${run2.healerModel ?? "null"})`);
10084
10310
  const noStart = batchHealNoStart(tests, run2.baseUrl);
10085
10311
  if (noStart) {
10086
- await postBatchHealResult(opts.serverUrl, headers, run2.runId, noStart.body);
10312
+ await postBatchHealResult(opts.serverUrl, headers, run2.runId, noStart.body, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
10087
10313
  log[noStart.level](noStart.logLine);
10088
10314
  return;
10089
10315
  }
@@ -10120,7 +10346,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10120
10346
  // Returns HEAL_TIMEOUT: for the tests not reached instead of dying at the runner's batch-heal deadline.
10121
10347
  deadlineAt: (0, import_runner_core7.batchHealDeadlineAt)(startTime),
10122
10348
  onLog: healLogStream.onLog,
10123
- ...batchHealPartialHooks(opts.serverUrl, headers, run2.runId, run2.projectId || void 0)
10349
+ ...batchHealPartialHooks(opts.serverUrl, headers, run2.runId, run2.projectId || void 0, void 0, run2.claimEpoch)
10124
10350
  });
10125
10351
  await healLogStream.flushNow();
10126
10352
  try {
@@ -10135,7 +10361,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10135
10361
  systemicDiagnosis: batchResult.systemicDiagnosis,
10136
10362
  logs: batchResult.logs,
10137
10363
  durationMs: batchResult.totalDurationMs
10138
- });
10364
+ }, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
10139
10365
  log.ok((0, import_runner_core9.batchHealCompleteLine)(batchResult.perTest, tests.length, batchResult.totalDurationMs));
10140
10366
  } catch (err) {
10141
10367
  log.fail(`Failed to post batch heal result: ${err instanceof Error ? err.message : String(err)}`);
@@ -10289,7 +10515,7 @@ ${finalVerifyResult.logs ?? ""}`;
10289
10515
  const legacyCold = (0, import_runner_core8.apiHealColdSessionRelabel)("legacy", {
10290
10516
  tags: run2.tags ?? [],
10291
10517
  authStateExpected: apiHealAuthPlan.needsCookieState,
10292
- authStateInjected: !!apiHealStorageStatePath,
10518
+ authStateInjected: (0, import_runner_core8.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
10293
10519
  needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
10294
10520
  standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
10295
10521
  error: result3.bugDescription ?? null,
@@ -10312,14 +10538,15 @@ ${finalVerifyResult.logs ?? ""}`;
10312
10538
  }
10313
10539
  if (result3.verdict === "unfixable") {
10314
10540
  log.fail(` Unfixable: ${result3.reason}`);
10315
- finalUnfixableReason = result3.reason;
10541
+ finalUnfixableReason = result3.reason ?? "AI could not fix the test";
10316
10542
  break;
10317
10543
  }
10318
10544
  lastHealedCode = result3.playwrightCode ?? result3.healedCode;
10319
10545
  lastChangeDescription = result3.changeDescription;
10320
10546
  if (!lastHealedCode) {
10321
- log.fail(" AI returned fix verdict but no code");
10322
- break;
10547
+ log.warn(` Attempt ${attempt}: AI returned fix verdict but no code \u2014 retrying with prompt`);
10548
+ currentError = `Attempt ${attempt} failed: You described a fix but did not include the \`playwrightCode\` field. Please provide the complete updated test file as \`playwrightCode\`.`;
10549
+ continue;
10323
10550
  }
10324
10551
  finalVerifyResult = await runNativeWithLiveBrowser({
10325
10552
  playwrightCode: lastHealedCode,
@@ -10376,7 +10603,7 @@ ${finalVerifyResult.logs ?? ""}`;
10376
10603
  let apiHealStatus = passed ? "PASSED" : "FAILED";
10377
10604
  let apiHealError = composedError;
10378
10605
  if (!passed) {
10379
- if (finalUnfixableReason?.startsWith(import_runner_core7.HEAL_AI_NO_ANSWER_ERROR_PREFIX)) {
10606
+ if (finalUnfixableReason?.startsWith(import_runner_core7.HEAL_AI_NO_ANSWER_ERROR_PREFIX) || (0, import_runner_core7.isHealAiQuotaError)(finalUnfixableReason)) {
10380
10607
  apiHealStatus = "ERROR";
10381
10608
  apiHealError = finalUnfixableReason;
10382
10609
  } else if (isConfigLikeReason(classifyErrorMessage(composedError ?? null))) {
@@ -10417,7 +10644,7 @@ ${finalVerifyResult.logs ?? ""}`;
10417
10644
  const cold = (0, import_runner_core8.apiHealColdSessionRelabel)("verified", {
10418
10645
  tags: run2.tags ?? [],
10419
10646
  authStateExpected: apiHealAuthPlan.needsCookieState,
10420
- authStateInjected: !!apiHealStorageStatePath,
10647
+ authStateInjected: (0, import_runner_core8.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
10421
10648
  needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
10422
10649
  standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
10423
10650
  error: apiHealError ?? finalVerifyResult?.error,
@@ -10566,20 +10793,20 @@ ${finalVerifyResult.logs ?? ""}`;
10566
10793
  const isConfigIssue = !result2.passed && isConfigLikeReason(classifyErrorMessage(result2.error ?? null));
10567
10794
  let healStatus = (0, import_runner_core7.healRunStatus)({ passed: result2.passed, error: result2.error, isConfigIssue });
10568
10795
  let healError = result2.error;
10569
- if (!result2.passed && !isConfigIssue && !(0, import_runner_core7.isHealAiQuotaError)(result2.error)) {
10570
- const rlDetection = (0, import_rate_limit_detector.detectRateLimitBlock)({
10571
- testName: run2.testName ?? run2.testCaseId ?? void 0,
10572
- tags: run2.tags ?? [],
10573
- error: result2.error,
10574
- logs: result2.logs,
10575
- stepResults: result2.stepResults,
10576
- apiCalls: result2.apiCalls
10577
- });
10578
- if (rlDetection.detected) {
10579
- healStatus = "ERROR";
10580
- healError = `RATE_LIMIT_BLOCKED: Heal-mode rate-limit detected (overriding AI verdict). Retry after ${rlDetection.retryAfterSeconds}s. Evidence: ${rlDetection.evidence.join("; ")}`;
10581
- log.warn(`\u26A0 Heal rate-limit override \u2014 was ${result2.error?.slice(0, 60)}\u2026, now ERROR/RATE_LIMIT_BLOCKED`);
10582
- }
10796
+ const rateLimitRelabel = (0, import_runner_core8.uiHealRateLimitRelabel)({
10797
+ passed: result2.passed,
10798
+ isConfigIssue,
10799
+ testName: run2.testName ?? run2.testCaseId ?? void 0,
10800
+ tags: run2.tags ?? [],
10801
+ error: result2.error,
10802
+ logs: result2.logs,
10803
+ stepResults: result2.stepResults,
10804
+ apiCalls: result2.apiCalls
10805
+ });
10806
+ if (rateLimitRelabel) {
10807
+ healStatus = rateLimitRelabel.status;
10808
+ healError = rateLimitRelabel.error;
10809
+ log.warn(rateLimitRelabel.logLine);
10583
10810
  }
10584
10811
  const loginRelabel = (0, import_runner_core8.uiHealLoginRelabel)({
10585
10812
  status: healStatus,
@@ -10599,6 +10826,7 @@ ${finalVerifyResult.logs ?? ""}`;
10599
10826
  status: healStatus,
10600
10827
  stepResults: result2.stepResults,
10601
10828
  screenshots: result2.screenshots,
10829
+ video: result2.video,
10602
10830
  apiCalls: result2.apiCalls,
10603
10831
  logs: (result2.logs ?? "") + auditDrainError,
10604
10832
  error: healError,
@@ -10663,7 +10891,7 @@ ${finalVerifyResult.logs ?? ""}`;
10663
10891
  error: result2.error,
10664
10892
  testResults: result2.testResults,
10665
10893
  skippedSpecs: result2.skippedSpecs
10666
- });
10894
+ }, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
10667
10895
  log[result2.passed ? "ok" : "fail"](
10668
10896
  `Regression ${result2.passed ? "PASSED" : "FAILED"} (${result2.duration}ms) - ${result2.testResults.length} specs`
10669
10897
  );
@@ -10699,8 +10927,18 @@ ${finalVerifyResult.logs ?? ""}`;
10699
10927
  body: JSON.stringify({ error: String(failedTest.error ?? "").slice(0, 5e5), testCaseId: failedTest.testCaseId }),
10700
10928
  signal: AbortSignal.timeout(6e4)
10701
10929
  });
10702
- if (healRes.ok) {
10703
- const { healedCode } = await healRes.json();
10930
+ const healBody = healRes.ok ? await healRes.json() : null;
10931
+ if (healBody?.verdict === "unfixable" && (0, import_runner_core7.isHealAiQuotaError)(healBody.reason)) {
10932
+ results.push({
10933
+ testCaseId: failedTest.testCaseId,
10934
+ status: "unfixable",
10935
+ error: healBody.reason,
10936
+ diagnosis: healBody.reason,
10937
+ duration: Date.now() - testStart
10938
+ });
10939
+ log.warn(` Not healed (AI provider quota): ${failedTest.name}`);
10940
+ } else if (healBody) {
10941
+ const { healedCode } = healBody;
10704
10942
  const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
10705
10943
  const verifyResult = await runNativeWithLiveBrowser({
10706
10944
  playwrightCode: healedCode,
@@ -10828,10 +11066,11 @@ ${finalVerifyResult.logs ?? ""}`;
10828
11066
  log.ok(` Fixed: ${failedTest.name}`);
10829
11067
  } else {
10830
11068
  const quotaStopped = (0, import_runner_core7.isHealAiQuotaError)(healResult.error);
10831
- const isAppBug = !quotaStopped && (0, import_runner_core7.isAppBugHealError)(healResult.error);
11069
+ const timeoutStopped = (0, import_runner_core7.isHealTimeoutError)(healResult.error);
11070
+ const isAppBug = !quotaStopped && !timeoutStopped && (0, import_runner_core7.isAppBugHealError)(healResult.error);
10832
11071
  const isConfigIssue = isConfigLikeReason(classifyErrorMessage(healResult.error ?? null));
10833
11072
  const status = isAppBug ? "app-bug" : isConfigIssue ? "config-issue" : "unfixable";
10834
- const diagnosisMsg = isAppBug ? "Application error detected" : isConfigIssue ? "Configuration issue detected" : quotaStopped && healResult.error ? healResult.error : "Could not heal after AI attempts";
11073
+ const diagnosisMsg = isAppBug ? "Application error detected" : isConfigIssue ? "Configuration issue detected" : (quotaStopped || timeoutStopped) && healResult.error ? healResult.error : "Could not heal after AI attempts";
10835
11074
  results.push({
10836
11075
  testCaseId: failedTest.testCaseId,
10837
11076
  status,
@@ -10839,7 +11078,7 @@ ${finalVerifyResult.logs ?? ""}`;
10839
11078
  diagnosis: diagnosisMsg,
10840
11079
  duration: Date.now() - testStart
10841
11080
  });
10842
- log[isAppBug || isConfigIssue || quotaStopped ? "warn" : "fail"](` ${isAppBug ? "App bug" : isConfigIssue ? "Config issue" : quotaStopped ? "Not healed (AI provider quota)" : "Could not fix"}: ${failedTest.name}`);
11081
+ log[isAppBug || isConfigIssue || quotaStopped || timeoutStopped ? "warn" : "fail"](` ${isAppBug ? "App bug" : isConfigIssue ? "Config issue" : quotaStopped ? "Not healed (AI provider quota)" : timeoutStopped ? "Not healed (heal time budget)" : "Could not fix"}: ${failedTest.name}`);
10843
11082
  }
10844
11083
  } catch (err) {
10845
11084
  results.push({
@@ -10890,7 +11129,7 @@ ${finalVerifyResult.logs ?? ""}`;
10890
11129
  `Live suite heal: ${fixedCount} fixed, ${appBugCount} app bugs, ${configIssueCount} config issues, ${unfixableCount} unfixable`,
10891
11130
  auditDrainWarning
10892
11131
  ].filter(Boolean).join("\n")
10893
- });
11132
+ }, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
10894
11133
  if (!liveRes.ok) {
10895
11134
  log.warn(`Failed to post live-suite result: ${liveRes.status}`);
10896
11135
  }
@@ -11847,7 +12086,7 @@ ${line}`)
11847
12086
  log.warn("[runner] discovery-har POST failed - final /discovery-result will carry HAR fallback");
11848
12087
  }
11849
12088
  const finalBody = buildFinalBody(discoveryResult, void 0, discoveryAuditWarning || void 0, auditDroppedCount);
11850
- const postOk = await postDiscoveryFinalResult(opts.serverUrl, headers, run2.runId, finalBody);
12089
+ const postOk = await postDiscoveryFinalResult(opts.serverUrl, headers, run2.runId, withClaimEpoch(finalBody, run2.claimEpoch));
11851
12090
  if (!postOk) throw new Error("Failed to post discovery result (3 attempts, or refused with 413)");
11852
12091
  const discoveryError = discoveryResult.error ?? (discoveryResult.summary?.startsWith("Fatal error:") ? discoveryResult.summary : void 0);
11853
12092
  if (discoveryError) {
@@ -11894,7 +12133,7 @@ ${line}`)
11894
12133
  headers: { ...headers, "Content-Type": "application/json" },
11895
12134
  body: JSON.stringify(body),
11896
12135
  signal: AbortSignal.timeout(12e4)
11897
- })), errorBody, "discovery error result", import_result_shed.DISCOVERY_SHED_LADDER).catch(() => {
12136
+ })), withClaimEpoch(errorBody, run2.claimEpoch), "discovery error result", import_result_shed.DISCOVERY_SHED_LADDER).catch(() => {
11898
12137
  });
11899
12138
  } finally {
11900
12139
  if (poolBrowser) browserPool.release(poolBrowser);
@@ -12027,7 +12266,7 @@ ${line}`)
12027
12266
  if (mobileSteps.length === 0) {
12028
12267
  await postRunResult({
12029
12268
  status: "ERROR",
12030
- error: "No mobile test steps found."
12269
+ error: "CONFIG_ISSUE: No mobile test steps found."
12031
12270
  });
12032
12271
  log.fail("ERROR - No mobile steps");
12033
12272
  return;
@@ -12555,7 +12794,7 @@ ${result2.logs}`);
12555
12794
  stepResults: result.stepResults,
12556
12795
  apiCalls: result.apiCalls,
12557
12796
  authStateExpected,
12558
- authStateInjected: !!storageStatePath,
12797
+ authStateInjected: (0, import_runner_core8.storageStateDelivered)(storageStatePath, result),
12559
12798
  needsStandingHeaders: normalRunAuthPlan.needsStandingHeaders,
12560
12799
  standingHeadersInjected: !!result.authDelivery?.standingHeadersInjected
12561
12800
  });
@@ -12627,6 +12866,7 @@ ${result2.logs}`);
12627
12866
  }
12628
12867
  }
12629
12868
  async function startRunner(opts) {
12869
+ (0, import_playwright_signal_handlers.disablePlaywrightSignalHandlers)();
12630
12870
  const { serverUrl, authToken, projectId, enableMobile = false, enableMobileTunnel = false } = opts;
12631
12871
  const pollIntervalMs = clampPollInterval(opts.pollIntervalMs);
12632
12872
  process.env.SERVER_URL = serverUrl;
@@ -13151,11 +13391,13 @@ async function startRunner(opts) {
13151
13391
  log.info("Runner stopped.");
13152
13392
  process.exit(0);
13153
13393
  };
13154
- process.once("SIGINT", () => {
13155
- void cleanup();
13156
- });
13157
- process.once("SIGTERM", () => {
13394
+ installShutdownSignals(process, () => {
13158
13395
  void cleanup();
13396
+ }, {
13397
+ forceExit: () => {
13398
+ console.error("Second interrupt \u2014 forcing exit");
13399
+ process.exit(0);
13400
+ }
13159
13401
  });
13160
13402
  const PIDS_WATCHDOG_INTERVAL_MS = Math.max(5e3, envInt("RUNNER_PIDS_WATCHDOG_INTERVAL_MS", 3e4));
13161
13403
  const PIDS_SOFT_WARN_PCT = Math.max(0, Math.min(100, envInt("RUNNER_PIDS_SOFT_WARN_PCT", 70)));
@@ -13266,6 +13508,10 @@ async function startRunner(opts) {
13266
13508
  for (const run2 of pending) {
13267
13509
  if (shuttingDown) break;
13268
13510
  if (activeRuns >= MAX_CONCURRENT) break;
13511
+ if (activeWebRuns.has(run2.runId)) {
13512
+ logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (an earlier claim of it is still settling on this runner)`);
13513
+ continue;
13514
+ }
13269
13515
  const runMode = run2.mode ?? "run";
13270
13516
  const isMobileOp = enableMobile && isMobileDeviceOp(run2);
13271
13517
  const discoveryKind = isMobileOp ? discoveryKindOf(run2.discoveryContext) : null;
@@ -13854,7 +14100,9 @@ async function superviseRunnerStart(argv, authTokenForChild) {
13854
14100
  stdio: "inherit",
13855
14101
  env: childEnv
13856
14102
  });
14103
+ let signalled = false;
13857
14104
  const onSignal = (sig) => {
14105
+ signalled = true;
13858
14106
  try {
13859
14107
  child.kill(sig);
13860
14108
  } catch {
@@ -13866,19 +14114,19 @@ async function superviseRunnerStart(argv, authTokenForChild) {
13866
14114
  process.removeListener("SIGINT", onSignal);
13867
14115
  process.removeListener("SIGTERM", onSignal);
13868
14116
  if (signal === "SIGINT" || signal === "SIGTERM") {
13869
- resolve3(0);
14117
+ resolve3({ code: 0, signalled: true });
13870
14118
  return;
13871
14119
  }
13872
- resolve3(code ?? 1);
14120
+ resolve3({ code: code ?? 1, signalled });
13873
14121
  });
13874
14122
  });
13875
14123
  console.log("\u{1F6E1} Runner supervisor enabled \u2014 will auto-restart on crash/stuck exit");
13876
14124
  console.log(" (disable with --no-supervise or VALIDATE_RUNNER_SUPERVISE=0)");
13877
14125
  for (; ; ) {
13878
14126
  const startedAt = Date.now();
13879
- const code = await runOnce();
14127
+ const { code, signalled } = await runOnce();
13880
14128
  const next = nextSupervisorAction(
13881
- { code, ranMs: Date.now() - startedAt, now: Date.now() },
14129
+ { code, ranMs: Date.now() - startedAt, now: Date.now(), signalled },
13882
14130
  state2,
13883
14131
  { maxRestartsPerHour, minBackoffMs, maxBackoffMs }
13884
14132
  );
@@ -13959,7 +14207,7 @@ program.command("start").description("Start the local test runner").option("--to
13959
14207
  console.warn("\u26A0 --tunnel has no effect without --mobile (the mobile bridge it exposes only starts with --mobile). Ignoring --tunnel.");
13960
14208
  }
13961
14209
  {
13962
- const { hasLocalGoogleLogin, GOOGLE_LOGIN_COMMAND } = await import("./dist-VTPBY2C4.mjs");
14210
+ const { hasLocalGoogleLogin, GOOGLE_LOGIN_COMMAND } = await import("./dist-52M5RDZ6.mjs");
13963
14211
  if (process.stdout.isTTY && !await hasLocalGoogleLogin()) {
13964
14212
  console.log(`Tip: for apps with "Continue with Google", sign in to Google once on this machine: ${GOOGLE_LOGIN_COMMAND}`);
13965
14213
  }
@@ -13989,7 +14237,7 @@ async function askLine(question) {
13989
14237
  }
13990
14238
  }
13991
14239
  program.command("google-login").description('Sign in to a TEST Google account once on this machine, for tests of apps with "Continue with Google"').option("--site <url...>", "The app address(es) this sign-in may be used for, e.g. https://app.example.com").option("--allow-file-storage", "Keep the sign-in in a plain file (only you can read it) when the system keychain is not available").option("--yes", "Confirm without asking that this is a dedicated test Google account").action(async (opts) => {
13992
- const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-VTPBY2C4.mjs");
14240
+ const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-52M5RDZ6.mjs");
13993
14241
  console.log(LOCAL_GOOGLE_WARNING);
13994
14242
  if (!opts.yes) {
13995
14243
  const answer = await askLine('Is this a dedicated test Google account? Type "yes" to continue: ');
@@ -14024,7 +14272,7 @@ program.command("google-login").description('Sign in to a TEST Google account on
14024
14272
  }
14025
14273
  });
14026
14274
  program.command("google-allow-site <url>").description("Let the Google sign-in on this machine be used for one more app address").action(async (url) => {
14027
- const { allowLocalGoogleSites, grantedSite } = await import("./dist-VTPBY2C4.mjs");
14275
+ const { allowLocalGoogleSites, grantedSite } = await import("./dist-52M5RDZ6.mjs");
14028
14276
  const site = grantedSite(url);
14029
14277
  if (!site) {
14030
14278
  console.error("\u2717 Not a web address: give it like https://app.example.com");
@@ -14038,7 +14286,7 @@ program.command("google-allow-site <url>").description("Let the Google sign-in o
14038
14286
  console.log(`\u2713 Tests on this machine may now use the Google sign-in for: ${sites.join(", ")}`);
14039
14287
  });
14040
14288
  program.command("google-logout").description("Forget the Google sign-in saved on this machine").action(async () => {
14041
- const { deleteLocalGoogleLogin } = await import("./dist-VTPBY2C4.mjs");
14289
+ const { deleteLocalGoogleLogin } = await import("./dist-52M5RDZ6.mjs");
14042
14290
  await deleteLocalGoogleLogin();
14043
14291
  console.log("\u2713 Google sign-in removed from this machine");
14044
14292
  });