@validate.qa/runner 1.0.100 → 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-MBKGTELT.mjs → chunk-3JM6XTSZ.mjs} +1862 -530
  3. package/dist/{chunk-2XD63NU5.mjs → chunk-4WLP3RWN.mjs} +941 -86
  4. package/dist/{chunk-MHFP3QXU.mjs → chunk-5DFBEL72.mjs} +1 -1
  5. package/dist/{chunk-II7QW7MZ.mjs → chunk-BXLEPT4M.mjs} +31 -18
  6. package/dist/{chunk-MAO6ADAO.mjs → chunk-CBVHPYVJ.mjs} +62 -2
  7. package/dist/{chunk-5P4ANVDY.mjs → chunk-DN3O6EYT.mjs} +43073 -23357
  8. package/dist/{chunk-GIIIVO2W.mjs → chunk-E2RRAE27.mjs} +155 -11
  9. package/dist/chunk-E2VEMBQQ.mjs +291 -0
  10. package/dist/{chunk-HMUC3DKS.mjs → chunk-HJ6FKHMM.mjs} +2281 -2
  11. package/dist/{chunk-WDXLAZ2J.mjs → chunk-PWFWMVZU.mjs} +3 -2
  12. package/dist/{chunk-6NIT2CCH.mjs → chunk-ZZYBRIBT.mjs} +18 -12
  13. package/dist/cli.js +139723 -114754
  14. package/dist/cli.mjs +536 -247
  15. package/dist/credential-mcp-server.js +1761 -358
  16. package/dist/credential-mcp-server.mjs +3 -3
  17. package/dist/discovery-mcp-server.js +1874 -379
  18. package/dist/discovery-mcp-server.mjs +4 -4
  19. package/dist/dist-52M5RDZ6.mjs +17 -0
  20. package/dist/inbox-mcp-server.js +1902 -505
  21. package/dist/inbox-mcp-server.mjs +3 -3
  22. package/dist/route-seed-worker.js +3976 -356
  23. package/dist/route-seed-worker.mjs +14 -7
  24. package/dist/run-write-guard-preload.js +2908 -572
  25. package/dist/run-write-guard-preload.mjs +5 -285
  26. package/dist/run-write-guard.js +2908 -572
  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 +4 -3
  31. package/dist/dist-DFNLMH6L.mjs +0 -16
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,
@@ -12,6 +14,7 @@ import {
12
14
  require_failure_triage,
13
15
  require_fetch_cli_subscription,
14
16
  require_framework_import_runner,
17
+ require_google_local_signin,
15
18
  require_grok_repo_enrichment_runner,
16
19
  require_inbox_stamp,
17
20
  require_live_browser_registry,
@@ -30,21 +33,23 @@ import {
30
33
  require_resource_governor,
31
34
  require_result_api_call_bodies,
32
35
  require_result_shed
33
- } from "./chunk-5P4ANVDY.mjs";
36
+ } from "./chunk-DN3O6EYT.mjs";
34
37
  import {
35
38
  require_tar_isolated
36
39
  } from "./chunk-AAUHXGDS.mjs";
37
40
  import "./chunk-EXRFHATN.mjs";
38
41
  import {
39
- require_browser_config
40
- } from "./chunk-6NIT2CCH.mjs";
41
- import "./chunk-II7QW7MZ.mjs";
42
- import "./chunk-MHFP3QXU.mjs";
43
- import "./chunk-MAO6ADAO.mjs";
44
- import "./chunk-HMUC3DKS.mjs";
45
- import "./chunk-WDXLAZ2J.mjs";
46
- import "./chunk-2XD63NU5.mjs";
47
- import "./chunk-GIIIVO2W.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";
48
53
  import {
49
54
  LIVE_FRAME_IDLE_PUSH_INTERVAL_MS,
50
55
  MOBILE_COLD_BOOT_SETTLE_MS,
@@ -57,17 +62,12 @@ import {
57
62
  buildAppiumCode,
58
63
  clampMobileSwipeDistance,
59
64
  classifyErrorMessage,
60
- coldSessionConfigMessage,
61
65
  computeMobileSwipePointers,
62
66
  decideHarPosted,
63
67
  deriveAuthPlan,
64
- detectApiColdSessionBlock,
65
- detectAuthBlock,
66
- detectSkippedApiCoverage,
67
68
  hasPlatformLocator,
68
69
  init_dist,
69
70
  isAppOriginResolutionEnabled,
70
- isAuthReadinessGuardEnabled,
71
71
  isConfigLikeReason,
72
72
  isEditableMobileClassName,
73
73
  isRepositoryCli,
@@ -84,18 +84,175 @@ import {
84
84
  stepAppliesToPlatform,
85
85
  stepsMissingPlatformLocators,
86
86
  stepsNeedingReviewForPlatform
87
- } from "./chunk-MBKGTELT.mjs";
87
+ } from "./chunk-3JM6XTSZ.mjs";
88
88
  import {
89
89
  __commonJS,
90
+ __require,
91
+ __toCommonJS,
90
92
  __toESM
91
93
  } from "./chunk-WQXBGQU4.mjs";
92
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
+
93
250
  // package.json
94
251
  var require_package = __commonJS({
95
252
  "package.json"(exports, module) {
96
253
  module.exports = {
97
254
  name: "@validate.qa/runner",
98
- version: "1.0.100",
255
+ version: "1.0.102",
99
256
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
100
257
  bin: {
101
258
  "validate-runner": "dist/cli.js"
@@ -103,8 +260,9 @@ var require_package = __commonJS({
103
260
  scripts: {
104
261
  build: "tsup",
105
262
  dev: "tsx src/cli.ts",
106
- 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/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",
107
- "test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.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",
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",
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",
108
266
  "smoke:installed": "node scripts/installed-run-smoke.mjs",
109
267
  "check:packed-mcp": "node scripts/packed-install-check.mjs",
110
268
  prepublishOnly: "npm run build"
@@ -315,17 +473,19 @@ var import_playwright_native = __toESM(require_playwright_native());
315
473
  var import_runner_core = __toESM(require_dist());
316
474
 
317
475
  // src/runner.ts
318
- var import_runner_core6 = __toESM(require_dist());
319
476
  var import_runner_core7 = __toESM(require_dist());
477
+ var import_runner_core8 = __toESM(require_dist());
478
+ var import_runner_core9 = __toESM(require_dist());
320
479
  var import_framework_import_runner = __toESM(require_framework_import_runner());
321
480
  var import_grok_repo_enrichment_runner = __toESM(require_grok_repo_enrichment_runner());
322
481
  var import_repo_brief_execution = __toESM(require_repo_brief_execution());
323
482
  var import_repo_read_log = __toESM(require_repo_read_log());
324
483
  var import_cli_session_failure = __toESM(require_cli_session_failure());
484
+ var import_playwright_signal_handlers = __toESM(require_playwright_signal_handlers());
325
485
  var import_cli_limit = __toESM(require_cli_limit());
326
486
  var import_fetch_cli_subscription = __toESM(require_fetch_cli_subscription());
327
487
  var import_codex_cli = __toESM(require_codex_cli());
328
- var import_runner_core8 = __toESM(require_dist());
488
+ var import_runner_core10 = __toESM(require_dist());
329
489
 
330
490
  // src/services/regression-runner.ts
331
491
  var import_regression_runner = __toESM(require_regression_runner());
@@ -383,6 +543,7 @@ var EXIT_TERMINAL = 64;
383
543
  var SUPERVISOR_WINDOW_MS = 36e5;
384
544
  var SUPERVISOR_HEALTHY_MS = 10 * 6e4;
385
545
  function nextSupervisorAction(input, state2, limits) {
546
+ if (input.signalled) return { action: "stop", exitCode: 0, reason: "clean" };
386
547
  if (input.code === 0) return { action: "stop", exitCode: 0, reason: "clean" };
387
548
  if (input.code === EXIT_TERMINAL) return { action: "stop", exitCode: EXIT_TERMINAL, reason: "terminal" };
388
549
  const restartTimes = [...state2.restartTimes.filter((t) => input.now - t < SUPERVISOR_WINDOW_MS), input.now];
@@ -534,6 +695,24 @@ function upgradeAction(activeRuns) {
534
695
  return activeRuns > 0 ? "drain" : "exit";
535
696
  }
536
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
+
537
716
  // src/runner.ts
538
717
  init_dist();
539
718
 
@@ -680,6 +859,9 @@ function computeMobileUiPathSucceeded(stepResults, authoredSteps) {
680
859
  return passedBefore > 0;
681
860
  }
682
861
 
862
+ // src/post-heal-outcome.ts
863
+ var import_runner_core5 = __toESM(require_dist());
864
+
683
865
  // src/run-no-longer-active.ts
684
866
  var RunNoLongerActiveError = class extends Error {
685
867
  };
@@ -691,39 +873,19 @@ function withClaimEpoch(body, claimEpoch) {
691
873
  }
692
874
 
693
875
  // src/post-heal-outcome.ts
694
- async function postHealOutcome(options) {
695
- const { postRunResult, postHealResult: postHealResult2, passed, healedCode, testCaseId, log: log2 } = options;
696
- const tryPostPatch = async () => {
697
- if (passed && healedCode && testCaseId) {
698
- try {
699
- await postHealResult2(healedCode, testCaseId);
700
- log2?.ok?.(`\u{1FA7A} Healed code auto-patched for test ${testCaseId}`);
701
- } catch (patchErr) {
702
- const patchMsg = patchErr instanceof Error ? patchErr.message : String(patchErr);
703
- log2?.warn?.(`Failed to auto-patch healed code: ${patchMsg}`);
704
- }
705
- }
706
- };
707
- try {
708
- await postRunResult();
709
- } catch (err) {
710
- if (err instanceof RunNoLongerActiveError) {
711
- await tryPostPatch();
712
- }
713
- throw err;
714
- }
715
- await tryPostPatch();
876
+ function postHealOutcome(options) {
877
+ return (0, import_runner_core5.postHealOutcome)({ ...options, isRunGone: (err) => err instanceof RunNoLongerActiveError });
716
878
  }
717
879
 
718
880
  // src/run-status-verdicts.ts
719
- var import_runner_core5 = __toESM(require_dist());
881
+ var import_runner_core6 = __toESM(require_dist());
720
882
  function runStatusVerdicts(body, heldEpoch) {
721
883
  const rows = body?.runs;
722
884
  if (!Array.isArray(rows)) return [];
723
885
  const verdicts = [];
724
886
  for (const row of rows) {
725
887
  if (!row || typeof row.id !== "string" || typeof row.status !== "string") continue;
726
- const reason = (0, import_runner_core5.runStatusAbortReason)(row, heldEpoch(row.id));
888
+ const reason = (0, import_runner_core6.runStatusAbortReason)(row, heldEpoch(row.id));
727
889
  if (!reason) continue;
728
890
  verdicts.push({
729
891
  runId: row.id,
@@ -1646,7 +1808,8 @@ async function clearField(sessionPath, bounds) {
1646
1808
  } catch (err) {
1647
1809
  if (isAppiumDriverDeadError(err)) throw err;
1648
1810
  throw new Error(
1649
- `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 }
1650
1813
  );
1651
1814
  }
1652
1815
  }
@@ -3876,7 +4039,7 @@ async function getWindowSize2(sessionId, retries = 3) {
3876
4039
  } catch (err) {
3877
4040
  if (attempt === retries - 1) {
3878
4041
  if (isTransportError(err)) throw err;
3879
- 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 });
3880
4043
  }
3881
4044
  await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * 100));
3882
4045
  }
@@ -5243,7 +5406,7 @@ async function executeSwipe(sessionId, step) {
5243
5406
  screenH = windowSize.height;
5244
5407
  } catch (err) {
5245
5408
  if (isTransportError(err)) throw err;
5246
- 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 });
5247
5410
  }
5248
5411
  const { startX, startY, endX, endY } = computeMobileSwipePointers(screenW, screenH, direction, distance);
5249
5412
  await wdRequest(`/session/${sessionId}/actions`, {
@@ -5562,12 +5725,12 @@ async function executeMobileTest(options) {
5562
5725
  const base = err instanceof Error ? err.message : String(err);
5563
5726
  if (selectedDevice.isPhysical) {
5564
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.`;
5565
- 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 });
5566
5729
  }
5567
5730
  const appListUnknown = !selectedDevice.installedApps || selectedDevice.installedApps.length === 0;
5568
5731
  if (appListUnknown) {
5569
5732
  const installHint = platformFilter === "IOS" ? `xcrun simctl install ${selectedDevice.id} <app-path>` : `adb -s ${selectedDevice.id} install <apk-path>`;
5570
- 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 });
5571
5734
  }
5572
5735
  throw err;
5573
5736
  }
@@ -5607,7 +5770,7 @@ async function executeMobileTest(options) {
5607
5770
  detail,
5608
5771
  recordedFromClearedState
5609
5772
  );
5610
- if (requirementError) throw new Error(requirementError);
5773
+ if (requirementError) throw new Error(requirementError, { cause: err });
5611
5774
  log2(`App-data clear failed (AUTO continues with plain fresh launch): ${detail}`);
5612
5775
  }
5613
5776
  } else {
@@ -5642,7 +5805,8 @@ async function executeMobileTest(options) {
5642
5805
  await openDeepLink(sessionId, options.target.platform, options.target.deeplink, options.target.appId);
5643
5806
  } catch (err) {
5644
5807
  throw new Error(
5645
- `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 }
5646
5810
  );
5647
5811
  }
5648
5812
  }
@@ -5923,7 +6087,8 @@ async function createMobileDiscoverySession(target, options) {
5923
6087
  });
5924
6088
  releaseControlPortSlot(discoveryPortSlotDeviceId);
5925
6089
  throw new Error(
5926
- `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 }
5927
6092
  );
5928
6093
  }
5929
6094
  }
@@ -6979,7 +7144,8 @@ async function startSession(validated, res) {
6979
7144
  } catch (err) {
6980
7145
  const detail = err instanceof Error ? err.message : String(err);
6981
7146
  throw new Error(
6982
- `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 }
6983
7149
  );
6984
7150
  }
6985
7151
  }
@@ -8907,23 +9073,42 @@ function withMediaReferences(result, refs) {
8907
9073
  // src/result-post-retry.ts
8908
9074
  var RESULT_POST_MAX_ATTEMPTS = 3;
8909
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
+ }
8910
9091
  function isTransientResultPostStatus(status) {
8911
9092
  return status === 408 || status === 429 || status >= 500;
8912
9093
  }
8913
- 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();
8914
9096
  for (let attempt = 1; ; attempt++) {
8915
9097
  let failure;
9098
+ let delay;
8916
9099
  try {
8917
9100
  const res = await send();
8918
- 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;
8919
9104
  await res.body?.cancel().catch(() => {
8920
9105
  });
8921
9106
  failure = `HTTP ${res.status}`;
8922
9107
  } catch (err) {
8923
- 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;
8924
9110
  failure = err instanceof Error ? err.message : String(err);
8925
9111
  }
8926
- const delay = Math.pow(2, attempt) * backoffBaseMs;
8927
9112
  console.warn(`[runner] ${label} post attempt ${attempt} failed (${failure}), retrying in ${delay}ms...`);
8928
9113
  await new Promise((resolve3) => setTimeout(resolve3, delay));
8929
9114
  }
@@ -8956,6 +9141,7 @@ function clampPollInterval(value) {
8956
9141
  init_dist();
8957
9142
  var import_ai_queue = __toESM(require_ai_queue());
8958
9143
  var import_base_url_check = __toESM(require_base_url_check());
9144
+ var import_tls_cert_probe = __toESM(require_tls_cert_probe());
8959
9145
  var import_rate_limit_detector = __toESM(require_rate_limit_detector());
8960
9146
  var import_resource_governor = __toESM(require_resource_governor());
8961
9147
  var import_process_inventory = __toESM(require_process_inventory());
@@ -9170,7 +9356,7 @@ async function claimRun(serverUrl, headers, runId) {
9170
9356
  if (typeof body.claimNonce === "string" && body.claimNonce) claimNonces.set(runId, body.claimNonce);
9171
9357
  return body.claimEpoch;
9172
9358
  }
9173
- 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)) {
9174
9360
  let maxAttempts = maxAttemptsArg;
9175
9361
  let verdictOnlyApplied = false;
9176
9362
  let lastError;
@@ -9189,8 +9375,10 @@ async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAtte
9189
9375
  }
9190
9376
  let payload = budgeted.payload;
9191
9377
  let apiCallBodiesStripped = false;
9378
+ const startedAt = Date.now();
9192
9379
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
9193
9380
  attemptsMade = attempt;
9381
+ let transient = false;
9194
9382
  try {
9195
9383
  const res = await fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/result`, () => ({
9196
9384
  method: "POST",
@@ -9242,15 +9430,17 @@ async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAtte
9242
9430
  throw new RunNoLongerActiveError(`Run ${runId} is no longer active on the server (HTTP ${res.status})`);
9243
9431
  }
9244
9432
  lastError = new Error(`HTTP ${res.status}`);
9433
+ transient = isTransientResultPostStatus(res.status);
9245
9434
  } catch (err) {
9246
9435
  if (err instanceof RunNoLongerActiveError) throw err;
9247
9436
  lastError = err instanceof Error ? err : new Error(String(err));
9437
+ transient = true;
9248
9438
  }
9249
- if (attempt < maxAttempts) {
9250
- const delay = Math.pow(2, attempt) * backoffBaseMs;
9251
- console.warn(`[runner] postResult attempt ${attempt} failed (${lastError.message}), retrying in ${delay}ms...`);
9252
- await new Promise((resolve3) => setTimeout(resolve3, delay));
9253
- }
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));
9254
9444
  }
9255
9445
  throw new Error(`Failed to post result after ${attemptsMade} attempts: ${lastError?.message}`);
9256
9446
  }
@@ -9280,7 +9470,7 @@ async function postHealResult(serverUrl, headers, runId, healedCode, testCaseId,
9280
9470
  }
9281
9471
  throw new Error(`Failed to post heal result after ${MAX_ATTEMPTS} attempts: ${lastError?.message}`);
9282
9472
  }
9283
- async function postMobileHealResult(serverUrl, headers, runId, body) {
9473
+ async function postMobileHealResult(serverUrl, headers, runId, body, claimEpoch) {
9284
9474
  const MAX_ATTEMPTS = 3;
9285
9475
  let lastError;
9286
9476
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
@@ -9288,7 +9478,7 @@ async function postMobileHealResult(serverUrl, headers, runId, body) {
9288
9478
  const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/mobile-heal-result`, {
9289
9479
  method: "POST",
9290
9480
  headers,
9291
- body: JSON.stringify(body),
9481
+ body: JSON.stringify(withClaimEpoch(body, claimEpoch)),
9292
9482
  signal: AbortSignal.timeout(6e4)
9293
9483
  });
9294
9484
  if (res.ok) return;
@@ -9307,12 +9497,13 @@ async function postMobileHealResult(serverUrl, headers, runId, body) {
9307
9497
  function narrowMobileLaunchMode(mode) {
9308
9498
  return mode === "LAUNCH_APP" || mode === "DEEP_LINK" || mode === "RESUME" ? mode : void 0;
9309
9499
  }
9310
- 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) {
9311
9501
  const progress = { body, fromRung: 0 };
9312
9502
  const res = await postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/batch-heal-result`, () => ({
9313
9503
  method: "POST",
9314
9504
  headers,
9315
- 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)),
9316
9507
  signal: AbortSignal.timeout(12e4)
9317
9508
  })), progress.body, "batch heal result", import_result_shed.BATCH_HEAL_SHED_LADDER, progress), "batch heal result", backoffBaseMs);
9318
9509
  throwUnlessResultLanded(res, runId, "batch heal result");
@@ -9324,11 +9515,11 @@ function throwUnlessResultLanded(res, runId, label) {
9324
9515
  }
9325
9516
  throw new Error(`Failed to post ${label}: ${res.status}`);
9326
9517
  }
9327
- async function postBatchHealPartial(serverUrl, headers, runId, result) {
9518
+ async function postBatchHealPartial(serverUrl, headers, runId, result, claimEpoch) {
9328
9519
  const res = await postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/batch-heal-partial`, () => ({
9329
9520
  method: "POST",
9330
9521
  headers,
9331
- body: JSON.stringify(payload),
9522
+ body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
9332
9523
  // Fires per verdict: a slow server must not hold up the next test.
9333
9524
  signal: AbortSignal.timeout(3e4)
9334
9525
  })), { result }, "batch heal partial", import_result_shed.BATCH_HEAL_PARTIAL_SHED_LADDER);
@@ -9349,11 +9540,11 @@ async function fetchBatchHealAppliedIds(serverUrl, headers, runId, projectId) {
9349
9540
  return /* @__PURE__ */ new Set();
9350
9541
  }
9351
9542
  }
9352
- function batchHealPartialHooks(serverUrl, headers, runId, projectId, warn = (msg) => log.warn(msg)) {
9543
+ function batchHealPartialHooks(serverUrl, headers, runId, projectId, warn = (msg) => log.warn(msg), claimEpoch) {
9353
9544
  return {
9354
9545
  onPartialResult: async (result) => {
9355
9546
  try {
9356
- await postBatchHealPartial(serverUrl, headers, runId, result);
9547
+ await postBatchHealPartial(serverUrl, headers, runId, result, claimEpoch);
9357
9548
  } catch (err) {
9358
9549
  warn(`batch-heal partial post failed for ${result.testCaseId}: ${err instanceof Error ? err.message : String(err)}`);
9359
9550
  }
@@ -9361,6 +9552,28 @@ function batchHealPartialHooks(serverUrl, headers, runId, projectId, warn = (msg
9361
9552
  resumeLookup: () => fetchBatchHealAppliedIds(serverUrl, headers, runId, projectId)
9362
9553
  };
9363
9554
  }
9555
+ function batchHealNoStart(tests, baseUrl) {
9556
+ if (tests.length === 0) {
9557
+ return {
9558
+ body: { perTest: [], systemicDiagnosis: "batch-heal dispatched with empty tests list", logs: "no tests to heal", durationMs: 0 },
9559
+ level: "warn",
9560
+ logLine: "batch-heal: empty payload, posting no-op result"
9561
+ };
9562
+ }
9563
+ if (!baseUrl) {
9564
+ return {
9565
+ body: {
9566
+ perTest: [],
9567
+ systemicDiagnosis: "batch-heal aborted: BASE_URL missing for project (set BASE_URL in Test Data or FrameworkRepo.baseUrl)",
9568
+ logs: "batch-heal aborted: empty baseUrl",
9569
+ durationMs: 0
9570
+ },
9571
+ level: "fail",
9572
+ logLine: "batch-heal aborted \u2014 BASE_URL is empty in /pending payload"
9573
+ };
9574
+ }
9575
+ return null;
9576
+ }
9364
9577
  var LOG_APPEND_MAX_LINES = 100;
9365
9578
  var LOG_APPEND_MAX_LINE_LEN = 8 * 1024;
9366
9579
  var LOG_APPEND_MAX_BODY_BYTES = 200 * 1024;
@@ -9427,18 +9640,25 @@ function createHealLogStreamer(serverUrl, headers, runId) {
9427
9640
  };
9428
9641
  return { onLog, flushNow };
9429
9642
  }
9430
- 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) {
9431
9644
  let current = progress?.body ?? body;
9432
9645
  let fromRung = progress?.fromRung ?? 0;
9433
9646
  let res = await send(current);
9434
- while (res.status === 413 && await (0, import_result_api_call_bodies2.isResultStructureTooCostlyResponse)(res)) {
9435
- const next = (0, import_result_shed.nextShedPayload)(current, ladder, fromRung);
9436
- if (!next) break;
9437
- console.warn(
9438
- `[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).`
9439
- );
9440
- current = next.payload;
9441
- 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
+ }
9442
9662
  if (progress) {
9443
9663
  progress.body = current;
9444
9664
  progress.fromRung = fromRung;
@@ -9457,22 +9677,24 @@ async function postCaptureResult(serverUrl, headers, runId, result, backoffBaseM
9457
9677
  })), progress.body, "capture result", import_result_shed.CAPTURE_SHED_LADDER, progress), "capture result", backoffBaseMs);
9458
9678
  throwUnlessResultLanded(res, runId, "capture result");
9459
9679
  }
9460
- async function postRegressionResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
9461
- 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 };
9462
9684
  const res = await postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/regression-result`, () => ({
9463
9685
  method: "POST",
9464
9686
  headers,
9465
- body: JSON.stringify(payload),
9687
+ body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
9466
9688
  signal: AbortSignal.timeout(6e4)
9467
- })), 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);
9468
9690
  throwUnlessResultLanded(res, runId, "regression result");
9469
9691
  }
9470
- 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) {
9471
9693
  const progress = { body, fromRung: 0 };
9472
9694
  return postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/live-suite-result`, () => ({
9473
9695
  method: "POST",
9474
9696
  headers,
9475
- body: JSON.stringify(payload),
9697
+ body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
9476
9698
  signal: AbortSignal.timeout(6e4)
9477
9699
  })), progress.body, "live suite result", import_result_shed.LIVE_SUITE_SHED_LADDER, progress), "live suite result", backoffBaseMs);
9478
9700
  }
@@ -9688,6 +9910,12 @@ function pruneCollectedPageForPost(page) {
9688
9910
  if (Array.isArray(out.observations)) out.observations = out.observations.slice(-200).map((obs) => compactForAudit(obs));
9689
9911
  return out;
9690
9912
  }
9913
+ function discoveryRenderTypeFields(input) {
9914
+ if (input.err instanceof import_runner_core7.UnsupportedRendererError) {
9915
+ return { detectedAppRenderType: input.err.renderType, appRenderDetectionSignals: input.err.signals };
9916
+ }
9917
+ return input.appRenderType ? { detectedAppRenderType: input.appRenderType } : {};
9918
+ }
9691
9919
  function prepareDiscoveryResultForPost(result, logs2, options = {}) {
9692
9920
  const safe = { ...result, logs: truncateTextTail(logs2, RESULT_LOG_MAX_CHARS, "discovery result log") };
9693
9921
  if (Array.isArray(safe.collectedPages)) safe.collectedPages = safe.collectedPages.map(pruneCollectedPageForPost);
@@ -9864,15 +10092,16 @@ function toLiveBrowserMode(mode) {
9864
10092
  async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = () => {
9865
10093
  }, onMobileHealPending = () => {
9866
10094
  }, assignedDeviceId, mobileLeaseError, signal) {
9867
- if (signal && (0, import_runner_core7.currentRunAbortSignal)() !== signal) {
9868
- return (0, import_runner_core7.runWithAbortScope)({ signal, runId: run2.runId }, () => executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, onMobileHealPending, assignedDeviceId, mobileLeaseError, signal));
10095
+ if (signal && (0, import_runner_core9.currentRunAbortSignal)() !== signal) {
10096
+ return (0, import_runner_core9.runWithAbortScope)({ signal, runId: run2.runId }, () => executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, onMobileHealPending, assignedDeviceId, mobileLeaseError, signal));
9869
10097
  }
9870
10098
  const runMode = run2.mode ?? "run";
9871
10099
  const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
9872
10100
  const runLabel = run2.testCaseId ?? run2.testName ?? (runMode === "run" ? "auth-setup" : runMode);
9873
10101
  const modeLabel = runMode === "run" ? RUNNER_MODE : runMode;
9874
10102
  log.info(`Running test: ${runLabel} (run ${run2.runId}) [mode: ${modeLabel}]`);
9875
- 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);
9876
10105
  const browserlessRepositoryRead = (0, import_live_browser_registry.isBrowserlessRepositoryRead)(run2.discoveryContext?.frameworkImport?.kind);
9877
10106
  const liveBrowserHandle = browserlessRepositoryRead ? import_live_browser_registry.NO_LIVE_BROWSER : import_live_browser_registry.liveBrowserRegistry.register({
9878
10107
  sessionId: `${run2.runId}`,
@@ -9920,10 +10149,12 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
9920
10149
  authStandingMode: authPlan.standingHeadersMode,
9921
10150
  storageStatePath: wrapperStorageStatePath,
9922
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"),
9923
10154
  ...nativeOptions,
9924
10155
  liveBrowserHandle,
9925
10156
  onLiveBrowserSnapshot: requestLiveBrowserHeartbeat,
9926
- signal: (0, import_runner_core7.linkRunAbort)(nativeOptions.signal) ?? signal
10157
+ signal: (0, import_runner_core9.linkRunAbort)(nativeOptions.signal) ?? signal
9927
10158
  });
9928
10159
  const callbackContext = {
9929
10160
  runId: run2.runId,
@@ -9936,7 +10167,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
9936
10167
  };
9937
10168
  const startTime = Date.now();
9938
10169
  try {
9939
- (0, import_runner_core7.throwIfRunAborted)();
10170
+ (0, import_runner_core9.throwIfRunAborted)();
9940
10171
  if (mobileLeaseError) {
9941
10172
  await postRunResult({
9942
10173
  status: "ERROR",
@@ -9994,6 +10225,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
9994
10225
  } else if (preflight.note) {
9995
10226
  log.info(preflight.note);
9996
10227
  }
10228
+ tlsCertNote = (0, import_tls_cert_probe.targetCertificateNote)(run2.baseUrl);
10229
+ void tlsCertNote.then((note) => {
10230
+ if (note) log.warn(note);
10231
+ });
9997
10232
  }
9998
10233
  if (runMode === "auth-setup") {
9999
10234
  if (!run2.playwrightCode) {
@@ -10052,7 +10287,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10052
10287
  releaseAuthLock2
10053
10288
  );
10054
10289
  const duration2 = Date.now() - startTime;
10055
- (0, import_runner_core7.throwIfRunAborted)();
10290
+ (0, import_runner_core9.throwIfRunAborted)();
10056
10291
  await postRunResult({
10057
10292
  status: result2.passed ? "PASSED" : "FAILED",
10058
10293
  apiCalls: result2.apiCalls,
@@ -10072,14 +10307,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10072
10307
  if (runMode === "batch-heal") {
10073
10308
  const tests = run2.batchHealTests ?? [];
10074
10309
  log.info(`Heal engine: GROK BATCH (${tests.length} tests, healerModel=${run2.healerModel ?? "null"})`);
10075
- if (tests.length === 0) {
10076
- await postBatchHealResult(opts.serverUrl, headers, run2.runId, {
10077
- perTest: [],
10078
- systemicDiagnosis: "batch-heal dispatched with empty tests list",
10079
- logs: "no tests to heal",
10080
- durationMs: 0
10081
- });
10082
- log.warn("batch-heal: empty payload, posting no-op result");
10310
+ const noStart = batchHealNoStart(tests, run2.baseUrl);
10311
+ if (noStart) {
10312
+ await postBatchHealResult(opts.serverUrl, headers, run2.runId, noStart.body, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
10313
+ log[noStart.level](noStart.logLine);
10083
10314
  return;
10084
10315
  }
10085
10316
  const healLogStream = createHealLogStreamer(opts.serverUrl, headers, run2.runId);
@@ -10101,10 +10332,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10101
10332
  completionTokens: info.completionTokens,
10102
10333
  totalTokens: info.totalTokens,
10103
10334
  requestBody: { iteration: info.iteration, toolCalls: info.toolCalls, session: info.requestMessages?.[0] ?? null },
10104
- metadata: { phase: "heal", groupId: info.groupId, iteration: info.iteration, healMode: "batch", ...(0, import_runner_core7.cliAuditMetadata)(info) }
10335
+ metadata: { phase: "heal", groupId: info.groupId, iteration: info.iteration, healMode: "batch", ...(0, import_runner_core9.cliAuditMetadata)(info) }
10105
10336
  });
10106
10337
  };
10107
- const batchResult = await (0, import_runner_core7.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
10338
+ const batchResult = await (0, import_runner_core9.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
10108
10339
  onAICall: batchHealAudit,
10109
10340
  testVariables: run2.testVariables,
10110
10341
  runNativeTest: runNativeWithLiveBrowser,
@@ -10113,9 +10344,9 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10113
10344
  saveRefreshedLogin: grokLogin?.saveRefreshed,
10114
10345
  engineSpec: run2.healerModel,
10115
10346
  // Returns HEAL_TIMEOUT: for the tests not reached instead of dying at the runner's batch-heal deadline.
10116
- deadlineAt: (0, import_runner_core6.batchHealDeadlineAt)(startTime),
10347
+ deadlineAt: (0, import_runner_core7.batchHealDeadlineAt)(startTime),
10117
10348
  onLog: healLogStream.onLog,
10118
- ...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)
10119
10350
  });
10120
10351
  await healLogStream.flushNow();
10121
10352
  try {
@@ -10123,15 +10354,15 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10123
10354
  } catch (drainErr) {
10124
10355
  console.warn(`[batch-heal] audit flush failed: ${drainErr instanceof Error ? drainErr.message : String(drainErr)}`);
10125
10356
  }
10126
- (0, import_runner_core7.throwIfRunAborted)();
10357
+ (0, import_runner_core9.throwIfRunAborted)();
10127
10358
  try {
10128
10359
  await postBatchHealResult(opts.serverUrl, headers, run2.runId, {
10129
10360
  perTest: batchResult.perTest,
10130
10361
  systemicDiagnosis: batchResult.systemicDiagnosis,
10131
10362
  logs: batchResult.logs,
10132
10363
  durationMs: batchResult.totalDurationMs
10133
- });
10134
- log.ok((0, import_runner_core7.batchHealCompleteLine)(batchResult.perTest, tests.length, batchResult.totalDurationMs));
10364
+ }, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
10365
+ log.ok((0, import_runner_core9.batchHealCompleteLine)(batchResult.perTest, tests.length, batchResult.totalDurationMs));
10135
10366
  } catch (err) {
10136
10367
  log.fail(`Failed to post batch heal result: ${err instanceof Error ? err.message : String(err)}`);
10137
10368
  }
@@ -10164,6 +10395,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
10164
10395
  }, run2.runId) : void 0;
10165
10396
  const apiHealStorageStatePath = apiHealAuthPaths ? (0, import_local_auth_state.getValidLocalAuthState)(apiHealAuthPaths.cachePath) : void 0;
10166
10397
  if (apiHealStorageStatePath) log.info("API heal verification is using saved auth state");
10398
+ const apiHealAuthSession = (0, import_runner_core8.sharedAuthSessionStamp)(apiHealStorageStatePath, false, sharedAuthInjectEnabled());
10399
+ const apiHealAuthPlan = authPlanFor(run2.tags, run2.playwrightCode);
10167
10400
  const stickyAppBugs = /* @__PURE__ */ new Map();
10168
10401
  const lastChangeByTitle = /* @__PURE__ */ new Map();
10169
10402
  let currentError = run2.lastFailureError ?? "Test failed";
@@ -10278,25 +10511,42 @@ ${finalVerifyResult.logs ?? ""}`;
10278
10511
  continue;
10279
10512
  }
10280
10513
  if (result3.verdict === "app_bug") {
10281
- log.warn(` APP BUG detected (legacy schema): ${result3.bugDescription}`);
10282
10514
  const duration4 = Date.now() - startTime;
10515
+ const legacyCold = (0, import_runner_core8.apiHealColdSessionRelabel)("legacy", {
10516
+ tags: run2.tags ?? [],
10517
+ authStateExpected: apiHealAuthPlan.needsCookieState,
10518
+ authStateInjected: (0, import_runner_core8.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
10519
+ needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
10520
+ standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
10521
+ error: result3.bugDescription ?? null,
10522
+ logs: finalVerifyResult?.logs ?? null,
10523
+ apiCalls: null
10524
+ });
10525
+ if (legacyCold) {
10526
+ log.warn(legacyCold.logLine);
10527
+ await postRunResult({ status: legacyCold.status, error: legacyCold.error, duration: duration4, authSession: apiHealAuthSession });
10528
+ return;
10529
+ }
10530
+ log.warn(` APP BUG detected (legacy schema): ${result3.bugDescription}`);
10283
10531
  await postRunResult({
10284
10532
  status: "FAILED",
10285
10533
  error: `APP_BUG: ${result3.bugDescription}`,
10286
- duration: duration4
10534
+ duration: duration4,
10535
+ authSession: apiHealAuthSession
10287
10536
  });
10288
10537
  return;
10289
10538
  }
10290
10539
  if (result3.verdict === "unfixable") {
10291
10540
  log.fail(` Unfixable: ${result3.reason}`);
10292
- finalUnfixableReason = result3.reason;
10541
+ finalUnfixableReason = result3.reason ?? "AI could not fix the test";
10293
10542
  break;
10294
10543
  }
10295
10544
  lastHealedCode = result3.playwrightCode ?? result3.healedCode;
10296
10545
  lastChangeDescription = result3.changeDescription;
10297
10546
  if (!lastHealedCode) {
10298
- log.fail(" AI returned fix verdict but no code");
10299
- 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;
10300
10550
  }
10301
10551
  finalVerifyResult = await runNativeWithLiveBrowser({
10302
10552
  playwrightCode: lastHealedCode,
@@ -10353,7 +10603,7 @@ ${finalVerifyResult.logs ?? ""}`;
10353
10603
  let apiHealStatus = passed ? "PASSED" : "FAILED";
10354
10604
  let apiHealError = composedError;
10355
10605
  if (!passed) {
10356
- if (finalUnfixableReason?.startsWith(import_runner_core6.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)) {
10357
10607
  apiHealStatus = "ERROR";
10358
10608
  apiHealError = finalUnfixableReason;
10359
10609
  } else if (isConfigLikeReason(classifyErrorMessage(composedError ?? null))) {
@@ -10390,6 +10640,23 @@ ${finalVerifyResult.logs ?? ""}`;
10390
10640
  }
10391
10641
  }
10392
10642
  }
10643
+ if (apiHealStatus === "FAILED") {
10644
+ const cold = (0, import_runner_core8.apiHealColdSessionRelabel)("verified", {
10645
+ tags: run2.tags ?? [],
10646
+ authStateExpected: apiHealAuthPlan.needsCookieState,
10647
+ authStateInjected: (0, import_runner_core8.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
10648
+ needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
10649
+ standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
10650
+ error: apiHealError ?? finalVerifyResult?.error,
10651
+ logs: finalVerifyResult?.logs,
10652
+ apiCalls: rawSteps.flatMap((s) => s.apiCalls ?? [])
10653
+ });
10654
+ if (cold) {
10655
+ apiHealStatus = cold.status;
10656
+ apiHealError = cold.error;
10657
+ log.warn(`${cold.logLine} (was "${composedError?.slice(0, 60)}\u2026")`);
10658
+ }
10659
+ }
10393
10660
  await postHealOutcome({
10394
10661
  postRunResult: () => postRunResult({
10395
10662
  status: apiHealStatus,
@@ -10401,9 +10668,10 @@ ${finalVerifyResult.logs ?? ""}`;
10401
10668
  // Preserve healedCode on ALL heal outcomes - including app-bug-only -
10402
10669
  // so the file with test.fixme wraps gets committed and audited.
10403
10670
  healedCode: lastHealedCode,
10404
- duration: duration3
10671
+ duration: duration3,
10672
+ authSession: apiHealAuthSession
10405
10673
  }),
10406
- postHealResult: (healedCode, testCaseId) => postHealResult(opts.serverUrl, headers, run2.runId, healedCode, testCaseId),
10674
+ postHealResult: (healedCode, testCaseId) => postHealResult(opts.serverUrl, headers, run2.runId, healedCode, testCaseId, void 0, void 0, run2.claimEpoch),
10407
10675
  passed,
10408
10676
  healedCode: lastHealedCode,
10409
10677
  testCaseId: run2.testCaseId,
@@ -10439,11 +10707,11 @@ ${finalVerifyResult.logs ?? ""}`;
10439
10707
  completionTokens: info.completionTokens,
10440
10708
  totalTokens: info.totalTokens,
10441
10709
  ...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
10442
- metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, ...(0, import_runner_core7.cliAuditMetadata)(info) }
10710
+ metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, ...(0, import_runner_core9.cliAuditMetadata)(info) }
10443
10711
  });
10444
10712
  };
10445
10713
  const envOverride = (process.env.USE_GROK_PER_TEST_HEAL ?? "").trim().toLowerCase();
10446
- const useGrokHeal = envOverride === "true" ? true : envOverride === "false" ? false : (0, import_runner_core7.getAgenticEngine)(run2.healerModel) === "grok-build";
10714
+ const useGrokHeal = envOverride === "true" ? true : envOverride === "false" ? false : (0, import_runner_core9.getAgenticEngine)(run2.healerModel) === "grok-build";
10447
10715
  const healOpts = {
10448
10716
  testName: run2.testName ?? run2.testCaseId ?? void 0,
10449
10717
  tags: run2.tags ?? [],
@@ -10466,19 +10734,19 @@ ${finalVerifyResult.logs ?? ""}`;
10466
10734
  // Whole-heal deadline: the runner's heal deadline minus a margin for the result
10467
10735
  // post (heal-deadline.ts). The healer stops both phases inside it and returns a
10468
10736
  // HEAL_TIMEOUT: result instead of dying as a reclaimed task.
10469
- deadlineAt: (0, import_runner_core6.healDeadlineAt)(startTime)
10737
+ deadlineAt: (0, import_runner_core7.healDeadlineAt)(startTime)
10470
10738
  };
10471
10739
  if (useGrokHeal) {
10472
10740
  const source = envOverride === "true" ? "env USE_GROK_PER_TEST_HEAL=true" : `healerModel=${run2.healerModel ?? "null"}`;
10473
10741
  log.info(`Heal engine: GROK (${source})`);
10474
10742
  }
10475
10743
  const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
10476
- const httpFallback = run2.healerFallbackModel && (0, import_runner_core7.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
10744
+ const httpFallback = run2.healerFallbackModel && (0, import_runner_core9.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
10477
10745
  let result2;
10478
10746
  if (useGrokHeal) {
10479
10747
  try {
10480
10748
  const grokLogin = await (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: "grok-build", onLog: grokHealLogStream?.onLog });
10481
- result2 = await (0, import_runner_core7.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
10749
+ result2 = await (0, import_runner_core9.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
10482
10750
  ...healOpts,
10483
10751
  subscriptionSecret: grokLogin?.value,
10484
10752
  saveRefreshedLogin: grokLogin?.saveRefreshed,
@@ -10523,28 +10791,42 @@ ${finalVerifyResult.logs ?? ""}`;
10523
10791
  console.warn(`[runner]${auditDrainError}`);
10524
10792
  }
10525
10793
  const isConfigIssue = !result2.passed && isConfigLikeReason(classifyErrorMessage(result2.error ?? null));
10526
- let healStatus = (0, import_runner_core6.healRunStatus)({ passed: result2.passed, error: result2.error, isConfigIssue });
10794
+ let healStatus = (0, import_runner_core7.healRunStatus)({ passed: result2.passed, error: result2.error, isConfigIssue });
10527
10795
  let healError = result2.error;
10528
- if (!result2.passed && !isConfigIssue && !(0, import_runner_core6.isHealAiQuotaError)(result2.error)) {
10529
- const rlDetection = (0, import_rate_limit_detector.detectRateLimitBlock)({
10530
- testName: run2.testName ?? run2.testCaseId ?? void 0,
10531
- tags: run2.tags ?? [],
10532
- error: result2.error,
10533
- logs: result2.logs,
10534
- stepResults: result2.stepResults,
10535
- apiCalls: result2.apiCalls
10536
- });
10537
- if (rlDetection.detected) {
10538
- healStatus = "ERROR";
10539
- healError = `RATE_LIMIT_BLOCKED: Heal-mode rate-limit detected (overriding AI verdict). Retry after ${rlDetection.retryAfterSeconds}s. Evidence: ${rlDetection.evidence.join("; ")}`;
10540
- log.warn(`\u26A0 Heal rate-limit override \u2014 was ${result2.error?.slice(0, 60)}\u2026, now ERROR/RATE_LIMIT_BLOCKED`);
10541
- }
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);
10810
+ }
10811
+ const loginRelabel = (0, import_runner_core8.uiHealLoginRelabel)({
10812
+ status: healStatus,
10813
+ testName: run2.testName ?? run2.testCaseId ?? void 0,
10814
+ tags: run2.tags ?? [],
10815
+ error: healError,
10816
+ logs: result2.logs,
10817
+ stepResults: result2.stepResults
10818
+ });
10819
+ if (loginRelabel) {
10820
+ healStatus = loginRelabel.status;
10821
+ healError = loginRelabel.error;
10822
+ log.warn(loginRelabel.logLine);
10542
10823
  }
10543
10824
  await postHealOutcome({
10544
10825
  postRunResult: () => postRunResult({
10545
10826
  status: healStatus,
10546
10827
  stepResults: result2.stepResults,
10547
10828
  screenshots: result2.screenshots,
10829
+ video: result2.video,
10548
10830
  apiCalls: result2.apiCalls,
10549
10831
  logs: (result2.logs ?? "") + auditDrainError,
10550
10832
  error: healError,
@@ -10565,7 +10847,7 @@ ${finalVerifyResult.logs ?? ""}`;
10565
10847
  });
10566
10848
  if (result2.logs) {
10567
10849
  const phaseLines = result2.logs.split("\n").filter(
10568
- (l) => l.includes("Phase 1: Script-driven") || l.includes("Phase 2: Escalating") || l.includes("Phase 2: Script + MCP") || l.includes("Phase 1 succeeded") || l.includes("Phase 2 iteration") || l.includes("\u{1F916} Verdict:") || l.includes("\u{1F916} finish_fix:") || l.includes("native verification PASSED") || l.includes("native verification FAILED") || l.includes("Native run PASSED") || l.includes("Native run FAILED") || l.includes("APP_BUG") || l.includes("CONFIG_ISSUE") || l.includes("needs_browser") || l.includes("--- Attempt") || l.includes("Proven commands") || l.includes("Proven fix:") || l.includes("Same error repeated") || l.includes("aborting heal")
10850
+ (l) => l.includes("Phase 1: Script-driven") || l.includes("Phase 2: Escalating") || l.includes("Phase 2: Script + MCP") || l.includes("Phase 1 succeeded") || l.includes("Phase 2 iteration") || l.includes("\u{1F916} Verdict:") || l.includes("\u{1F916} finish_fix:") || l.includes("Preflight:") || l.includes("native verification PASSED") || l.includes("native verification FAILED") || l.includes("Native run PASSED") || l.includes("Native run FAILED") || l.includes("APP_BUG") || l.includes("CONFIG_ISSUE") || l.includes("needs_browser") || l.includes("--- Attempt") || l.includes("Proven commands") || l.includes("Proven fix:") || l.includes("Same error repeated") || l.includes("aborting heal")
10569
10851
  );
10570
10852
  if (phaseLines.length > 0) {
10571
10853
  console.log(`${C2.gray}\u2500\u2500 Heal summary \u2500\u2500${C2.reset}`);
@@ -10578,7 +10860,7 @@ ${finalVerifyResult.logs ?? ""}`;
10578
10860
  );
10579
10861
  return;
10580
10862
  }
10581
- if (runMode === "regression-suite") {
10863
+ if (runMode === "regression-suite" || runMode === "system-regression-suite") {
10582
10864
  if (!run2.repoOwner || !run2.repoName) {
10583
10865
  await postRunResult({ status: "ERROR", error: "No framework repo configured" });
10584
10866
  log.fail("ERROR - No framework repo configured");
@@ -10600,8 +10882,8 @@ ${finalVerifyResult.logs ?? ""}`;
10600
10882
  // A cancel, a newer claim or a shutdown kills the suite instead of letting it run on for up to 85 min.
10601
10883
  signal
10602
10884
  });
10603
- const overall = result2.error && result2.testResults.length === 0 ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
10604
- (0, import_runner_core7.throwIfRunAborted)();
10885
+ const overall = (0, import_regression_runner.regressionSuiteOverall)(result2);
10886
+ (0, import_runner_core9.throwIfRunAborted)();
10605
10887
  await postRegressionResult(opts.serverUrl, headers, run2.runId, {
10606
10888
  overall,
10607
10889
  logs: result2.logs,
@@ -10609,7 +10891,7 @@ ${finalVerifyResult.logs ?? ""}`;
10609
10891
  error: result2.error,
10610
10892
  testResults: result2.testResults,
10611
10893
  skippedSpecs: result2.skippedSpecs
10612
- });
10894
+ }, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
10613
10895
  log[result2.passed ? "ok" : "fail"](
10614
10896
  `Regression ${result2.passed ? "PASSED" : "FAILED"} (${result2.duration}ms) - ${result2.testResults.length} specs`
10615
10897
  );
@@ -10631,9 +10913,9 @@ ${finalVerifyResult.logs ?? ""}`;
10631
10913
  `${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
10632
10914
  headers
10633
10915
  );
10634
- const suiteQuotaFallback = (0, import_runner_core6.createQuotaFallbackState)(run2.quotaFallbackModel);
10916
+ const suiteQuotaFallback = (0, import_runner_core7.createQuotaFallbackState)(run2.quotaFallbackModel);
10635
10917
  for (const failedTest of failedTests) {
10636
- (0, import_runner_core7.throwIfRunAborted)();
10918
+ (0, import_runner_core9.throwIfRunAborted)();
10637
10919
  const testStart = Date.now();
10638
10920
  log.info(` Healing: ${failedTest.name ?? failedTest.testCaseId}`);
10639
10921
  const isApiTest = Array.isArray(failedTest.tags) && failedTest.tags.includes("@api");
@@ -10645,8 +10927,18 @@ ${finalVerifyResult.logs ?? ""}`;
10645
10927
  body: JSON.stringify({ error: String(failedTest.error ?? "").slice(0, 5e5), testCaseId: failedTest.testCaseId }),
10646
10928
  signal: AbortSignal.timeout(6e4)
10647
10929
  });
10648
- if (healRes.ok) {
10649
- 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;
10650
10942
  const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
10651
10943
  const verifyResult = await runNativeWithLiveBrowser({
10652
10944
  playwrightCode: healedCode,
@@ -10701,12 +10993,12 @@ ${finalVerifyResult.logs ?? ""}`;
10701
10993
  completionTokens: info.completionTokens,
10702
10994
  totalTokens: info.totalTokens,
10703
10995
  ...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
10704
- metadata: { groupId: info.groupId, testCaseId: failedTest.testCaseId, ...(0, import_runner_core7.cliAuditMetadata)(info) }
10996
+ metadata: { groupId: info.groupId, testCaseId: failedTest.testCaseId, ...(0, import_runner_core9.cliAuditMetadata)(info) }
10705
10997
  });
10706
10998
  };
10707
10999
  try {
10708
11000
  const envOverrideSuite = (process.env.USE_GROK_PER_TEST_HEAL ?? "").trim().toLowerCase();
10709
- const useGrokHealSuite = envOverrideSuite === "true" ? true : envOverrideSuite === "false" ? false : (0, import_runner_core7.getAgenticEngine)(run2.healerModel) === "grok-build";
11001
+ const useGrokHealSuite = envOverrideSuite === "true" ? true : envOverrideSuite === "false" ? false : (0, import_runner_core9.getAgenticEngine)(run2.healerModel) === "grok-build";
10710
11002
  const suiteHealOpts = {
10711
11003
  testName: failedTest.name ?? failedTest.testCaseId,
10712
11004
  tags: failedTest.tags ?? [],
@@ -10714,6 +11006,10 @@ ${finalVerifyResult.logs ?? ""}`;
10714
11006
  healingContext: failedTest.healingContext ?? null,
10715
11007
  selectorRegistryContext: failedTest.selectorRegistryContext ?? null,
10716
11008
  lastFailureError: failedTest.error,
11009
+ // A suite heal reports fixed / app-bug / config-issue / unfixable only (/live-suite-result has no
11010
+ // entry for a test that passed unchanged), so the original re-run preflight stays off here: a
11011
+ // TRANSIENT_PASS would otherwise be posted as "unfixable". Heal mode keeps the preflight.
11012
+ originalRerun: false,
10717
11013
  testVariables: run2.testVariables,
10718
11014
  onAICall: healAudit,
10719
11015
  modelOverride: run2.healerModel,
@@ -10724,12 +11020,12 @@ ${finalVerifyResult.logs ?? ""}`;
10724
11020
  liveBrowserHandle,
10725
11021
  runNativeTest: runNativeWithLiveBrowser
10726
11022
  };
10727
- const suiteHttpFallback = run2.healerFallbackModel && (0, import_runner_core7.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
11023
+ const suiteHttpFallback = run2.healerFallbackModel && (0, import_runner_core9.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
10728
11024
  let healResult;
10729
11025
  if (useGrokHealSuite) {
10730
11026
  try {
10731
11027
  const grokLogin = await (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: "grok-build", onLog: log.warn });
10732
- healResult = await (0, import_runner_core7.executeGrokPerTestHeal)(
11028
+ healResult = await (0, import_runner_core9.executeGrokPerTestHeal)(
10733
11029
  failedTest.playwrightCode,
10734
11030
  failedTest.steps ?? [],
10735
11031
  run2.baseUrl,
@@ -10769,11 +11065,12 @@ ${finalVerifyResult.logs ?? ""}`;
10769
11065
  });
10770
11066
  log.ok(` Fixed: ${failedTest.name}`);
10771
11067
  } else {
10772
- const quotaStopped = (0, import_runner_core6.isHealAiQuotaError)(healResult.error);
10773
- const isAppBug = !quotaStopped && (0, import_runner_core6.isAppBugHealError)(healResult.error);
11068
+ const quotaStopped = (0, import_runner_core7.isHealAiQuotaError)(healResult.error);
11069
+ const timeoutStopped = (0, import_runner_core7.isHealTimeoutError)(healResult.error);
11070
+ const isAppBug = !quotaStopped && !timeoutStopped && (0, import_runner_core7.isAppBugHealError)(healResult.error);
10774
11071
  const isConfigIssue = isConfigLikeReason(classifyErrorMessage(healResult.error ?? null));
10775
11072
  const status = isAppBug ? "app-bug" : isConfigIssue ? "config-issue" : "unfixable";
10776
- 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";
10777
11074
  results.push({
10778
11075
  testCaseId: failedTest.testCaseId,
10779
11076
  status,
@@ -10781,7 +11078,7 @@ ${finalVerifyResult.logs ?? ""}`;
10781
11078
  diagnosis: diagnosisMsg,
10782
11079
  duration: Date.now() - testStart
10783
11080
  });
10784
- 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}`);
10785
11082
  }
10786
11083
  } catch (err) {
10787
11084
  results.push({
@@ -10794,7 +11091,7 @@ ${finalVerifyResult.logs ?? ""}`;
10794
11091
  log.fail(` Heal crashed: ${failedTest.name}`);
10795
11092
  }
10796
11093
  }
10797
- (0, import_runner_core7.throwIfRunAborted)();
11094
+ (0, import_runner_core9.throwIfRunAborted)();
10798
11095
  const lastResult = results[results.length - 1];
10799
11096
  if (lastResult) {
10800
11097
  fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/live-suite-progress`, {
@@ -10823,7 +11120,7 @@ ${finalVerifyResult.logs ?? ""}`;
10823
11120
  auditDrainWarning = drainErr instanceof Error ? drainErr.message : String(drainErr);
10824
11121
  console.warn(`[runner] ${auditDrainWarning}`);
10825
11122
  }
10826
- (0, import_runner_core7.throwIfRunAborted)();
11123
+ (0, import_runner_core9.throwIfRunAborted)();
10827
11124
  const liveRes = await postLiveSuiteResult(opts.serverUrl, headers, run2.runId, {
10828
11125
  overall,
10829
11126
  duration: totalDuration,
@@ -10832,7 +11129,7 @@ ${finalVerifyResult.logs ?? ""}`;
10832
11129
  `Live suite heal: ${fixedCount} fixed, ${appBugCount} app bugs, ${configIssueCount} config issues, ${unfixableCount} unfixable`,
10833
11130
  auditDrainWarning
10834
11131
  ].filter(Boolean).join("\n")
10835
- });
11132
+ }, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
10836
11133
  if (!liveRes.ok) {
10837
11134
  log.warn(`Failed to post live-suite result: ${liveRes.status}`);
10838
11135
  }
@@ -10914,7 +11211,8 @@ ${finalVerifyResult.logs ?? ""}`;
10914
11211
  const res = await fetchRetrying429(`${opts.serverUrl}/api/runner/runs/${run2.runId}/locator-discovery-result`, () => ({
10915
11212
  method: "POST",
10916
11213
  headers: { ...headers, "Content-Type": "application/json" },
10917
- body: JSON.stringify({
11214
+ // The epoch /claim returned: the server refuses a harvest whose run was re-claimed since (stale worker).
11215
+ body: JSON.stringify(withClaimEpoch({
10918
11216
  bundles: harvest.bundles,
10919
11217
  // A divergence mark is durable (it blocks auto re-harvest and can
10920
11218
  // flip the test to needs_manual_authoring), so an environmentally
@@ -10932,7 +11230,7 @@ ${finalVerifyResult.logs ?? ""}`;
10932
11230
  reachedOrder: harvest.reachedOrder,
10933
11231
  logs: harvest.logs,
10934
11232
  ...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
10935
- }),
11233
+ }, run2.claimEpoch)),
10936
11234
  signal: AbortSignal.timeout(3e4)
10937
11235
  }));
10938
11236
  if (res.ok) {
@@ -10997,7 +11295,7 @@ ${finalVerifyResult.logs ?? ""}`;
10997
11295
  }
10998
11296
  return await res.json();
10999
11297
  };
11000
- ctx.smsReserve = (0, import_runner_core7.createHttpSmsReserveTransport)({ serverUrl: opts.serverUrl, headers, runId: run2.runId });
11298
+ ctx.smsReserve = (0, import_runner_core9.createHttpSmsReserveTransport)({ serverUrl: opts.serverUrl, headers, runId: run2.runId });
11001
11299
  }
11002
11300
  const canonicalBase = await (0, import_base_url_check.canonicalDiscoveryBaseUrl)(
11003
11301
  { runMode, baseUrl: ctx.baseUrl, native: ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE" || run2.framework === "APPIUM" }
@@ -11059,7 +11357,7 @@ ${finalVerifyResult.logs ?? ""}`;
11059
11357
  ...info.exitReason ? { exitReason: info.exitReason } : {},
11060
11358
  // Engine cost, engine id, step, cache + reasoning tokens: same helper as the server's
11061
11359
  // Fargate path (execute-run.ts), so the two rows agree.
11062
- ...(0, import_runner_core7.cliAuditMetadata)(info),
11360
+ ...(0, import_runner_core9.cliAuditMetadata)(info),
11063
11361
  ...ledgerAudit ? {
11064
11362
  ledger: ledgerAudit.entries,
11065
11363
  ledgerEntryCount: ledgerAudit.originalLength,
@@ -11143,7 +11441,8 @@ ${finalVerifyResult.logs ?? ""}`;
11143
11441
  headers: { ...headers, "Content-Type": "application/json" },
11144
11442
  // Framework-import runs span MANY features in one Session, so each test
11145
11443
  // carries its own suiteName; per-feature deep runs fall back to ctx.featureName.
11146
- body: JSON.stringify({ ...test, suiteName: test.suiteName ?? ctx.featureName, phase }),
11444
+ // claimEpoch: the server fences each save to this claim (a re-claimed run refuses it with 409).
11445
+ body: JSON.stringify(withClaimEpoch({ ...test, suiteName: test.suiteName ?? ctx.featureName, phase }, run2.claimEpoch)),
11147
11446
  signal: AbortSignal.timeout(3e4)
11148
11447
  });
11149
11448
  if (res.status === 402) throw new import_discovery.PlanLimitReached();
@@ -11300,7 +11599,8 @@ ${finalVerifyResult.logs ?? ""}`;
11300
11599
  healthObservations: rest.healthObservations
11301
11600
  },
11302
11601
  // Only send tests that were NOT confirmed streamed to avoid duplicates
11303
- tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t)))
11602
+ tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t))),
11603
+ ...discoveryRenderTypeFields({ appRenderType: ctx.appRenderType })
11304
11604
  }, logs2, { omitScreenshots: isNativeDiscovery });
11305
11605
  };
11306
11606
  const acquireLoginBrowser = async () => {
@@ -11602,7 +11902,7 @@ ${line}`)
11602
11902
  };
11603
11903
  } else if (ctx.frameworkImport?.kind === "app-map") {
11604
11904
  log.info("Building the app map.");
11605
- discoveryResult = await (0, import_runner_core7.runAppMapOnRunner)(ctx, { onLog: (line) => log.info(line) });
11905
+ discoveryResult = await (0, import_runner_core9.runAppMapOnRunner)(ctx, { onLog: (line) => log.info(line) });
11606
11906
  } else if (ctx.frameworkImport?.kind === "repo-enrichment") {
11607
11907
  const frameworkImport = ctx.frameworkImport;
11608
11908
  const primaryCli = isRepositoryCli(frameworkImport.repoHuntCli) ? frameworkImport.repoHuntCli : "claude-code";
@@ -11627,7 +11927,7 @@ ${line}`)
11627
11927
  })
11628
11928
  });
11629
11929
  } else {
11630
- const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core7.runRepoAnalysisOnRunner : import_framework_import_runner.runFrameworkImportOnRunner;
11930
+ const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core9.runRepoAnalysisOnRunner : import_framework_import_runner.runFrameworkImportOnRunner;
11631
11931
  discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
11632
11932
  archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
11633
11933
  archiveAuthHeader: headers.Authorization,
@@ -11689,7 +11989,7 @@ ${line}`)
11689
11989
  }
11690
11990
  const resolveCliSubscription = (engine, logLine) => (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: engine, onLog: logLine });
11691
11991
  const claimNonce = claimNonceFor(run2.runId);
11692
- savedSignIn = ["survey", "deep", "full"].includes(ctx.mode) && claimNonce && typeof run2.claimEpoch === "number" ? await (0, import_runner_core7.openSavedSignInSession)((0, import_runner_core7.createSavedSignInClient)({
11992
+ savedSignIn = ["survey", "deep", "full"].includes(ctx.mode) && claimNonce && typeof run2.claimEpoch === "number" ? await (0, import_runner_core9.openSavedSignInSession)((0, import_runner_core9.createSavedSignInClient)({
11693
11993
  serverUrl: opts.serverUrl,
11694
11994
  runId: run2.runId,
11695
11995
  claimEpoch: run2.claimEpoch,
@@ -11709,7 +12009,7 @@ ${line}`)
11709
12009
  if (ctx.repoBrief) discoveryResult.repoBrief = ctx.repoBrief;
11710
12010
  if (ctx.repoRouteManifest) discoveryResult.repoRouteManifest = ctx.repoRouteManifest;
11711
12011
  }
11712
- discoveryResult = (0, import_runner_core7.redactDiscoveryCaptureNeedles)(discoveryResult, {
12012
+ discoveryResult = (0, import_runner_core9.redactDiscoveryCaptureNeedles)(discoveryResult, {
11713
12013
  ...run2.testVariables,
11714
12014
  // The saved sign-in's values, with every one it rotated to during the run.
11715
12015
  ...savedSignIn?.loaded ? savedSignIn.secrets() : {}
@@ -11786,7 +12086,7 @@ ${line}`)
11786
12086
  log.warn("[runner] discovery-har POST failed - final /discovery-result will carry HAR fallback");
11787
12087
  }
11788
12088
  const finalBody = buildFinalBody(discoveryResult, void 0, discoveryAuditWarning || void 0, auditDroppedCount);
11789
- const postOk = await postDiscoveryFinalResult(opts.serverUrl, headers, run2.runId, finalBody);
12089
+ const postOk = await postDiscoveryFinalResult(opts.serverUrl, headers, run2.runId, withClaimEpoch(finalBody, run2.claimEpoch));
11790
12090
  if (!postOk) throw new Error("Failed to post discovery result (3 attempts, or refused with 413)");
11791
12091
  const discoveryError = discoveryResult.error ?? (discoveryResult.summary?.startsWith("Fatal error:") ? discoveryResult.summary : void 0);
11792
12092
  if (discoveryError) {
@@ -11825,14 +12125,15 @@ ${line}`)
11825
12125
  cliLimit: (0, import_cli_limit.cliLimitFromError)(err),
11826
12126
  streamedTestCount: streamedTestKeys.size,
11827
12127
  streamedTestCaseIds: Array.from(streamedTestCaseIds),
11828
- exploreAlreadySaved: explorePosted
12128
+ exploreAlreadySaved: explorePosted,
12129
+ ...discoveryRenderTypeFields({ err })
11829
12130
  }, `Fatal error: ${msg}${drainMsg}`);
11830
12131
  await postShedOn413((body) => fetchRetrying429(`${opts.serverUrl}/api/runner/runs/${run2.runId}/discovery-result`, () => ({
11831
12132
  method: "POST",
11832
12133
  headers: { ...headers, "Content-Type": "application/json" },
11833
12134
  body: JSON.stringify(body),
11834
12135
  signal: AbortSignal.timeout(12e4)
11835
- })), 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(() => {
11836
12137
  });
11837
12138
  } finally {
11838
12139
  if (poolBrowser) browserPool.release(poolBrowser);
@@ -11866,7 +12167,7 @@ ${line}`)
11866
12167
  completionTokens: info.completionTokens,
11867
12168
  totalTokens: info.totalTokens,
11868
12169
  ...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
11869
- metadata: { groupId: info.groupId, iteration: info.iteration, phase: "capture", featureName: ctx.featureName, ...(0, import_runner_core7.cliAuditMetadata)(info) }
12170
+ metadata: { groupId: info.groupId, iteration: info.iteration, phase: "capture", featureName: ctx.featureName, ...(0, import_runner_core9.cliAuditMetadata)(info) }
11870
12171
  });
11871
12172
  };
11872
12173
  const logBuffer = [];
@@ -11965,7 +12266,7 @@ ${line}`)
11965
12266
  if (mobileSteps.length === 0) {
11966
12267
  await postRunResult({
11967
12268
  status: "ERROR",
11968
- error: "No mobile test steps found."
12269
+ error: "CONFIG_ISSUE: No mobile test steps found."
11969
12270
  });
11970
12271
  log.fail("ERROR - No mobile steps");
11971
12272
  return;
@@ -12041,7 +12342,7 @@ ${line}`)
12041
12342
  verdict: "fix",
12042
12343
  recovered: true,
12043
12344
  priorStepsHash: run2.priorStepsHash
12044
- });
12345
+ }, run2.claimEpoch);
12045
12346
  log.ok("\u{1FA7A} Reproduce PASSED \u2014 test recovered (flaky); promotion posted");
12046
12347
  } catch (patchErr) {
12047
12348
  log.warn(`Failed to post recovered heal result: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`);
@@ -12083,7 +12384,7 @@ ${line}`)
12083
12384
  completionTokens: info.completionTokens,
12084
12385
  totalTokens: info.totalTokens,
12085
12386
  ...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
12086
- metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, platform: "mobile", ...(0, import_runner_core7.cliAuditMetadata)(info) }
12387
+ metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, platform: "mobile", ...(0, import_runner_core9.cliAuditMetadata)(info) }
12087
12388
  });
12088
12389
  },
12089
12390
  runLiveHeal: async (a) => {
@@ -12278,7 +12579,7 @@ ${line}`)
12278
12579
  extraTags: heal.extraTags,
12279
12580
  removeTags: heal.removeTags,
12280
12581
  priorStepsHash: run2.priorStepsHash
12281
- });
12582
+ }, run2.claimEpoch);
12282
12583
  log.ok(`\u{1FA7A} Mobile heal patch posted for test ${run2.testCaseId ?? "(unknown)"} (verdict: ${heal.verdict})`);
12283
12584
  } catch (patchErr) {
12284
12585
  log.warn(`Failed to post mobile heal patch: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`);
@@ -12425,6 +12726,7 @@ ${result2.logs}`);
12425
12726
  if (storageStatePath) {
12426
12727
  log.info(`Using auth state for project ${projectId}`);
12427
12728
  }
12729
+ const authSession = (0, import_runner_core8.sharedAuthSessionStamp)(storageStatePath, captureAuth, sharedAuthInjectEnabled());
12428
12730
  const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
12429
12731
  const result = await (async () => {
12430
12732
  try {
@@ -12483,49 +12785,25 @@ ${result2.logs}`);
12483
12785
  runStatus = "ERROR";
12484
12786
  log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
12485
12787
  }
12486
- if (isAuthReadinessGuardEnabled()) {
12487
- if (runStatus === "FAILED") {
12488
- const ab = detectAuthBlock({
12489
- testName: run2.testName ?? run2.testCaseId ?? void 0,
12490
- tags: run2.tags ?? [],
12491
- error: runError,
12492
- logs: result.logs,
12493
- stepResults: result.stepResults
12494
- });
12495
- if (ab.detected) {
12496
- runStatus = "ERROR";
12497
- runError = `CONFIG_ISSUE: Login prerequisite failed (${ab.reason}) \u2014 authed tests can't run until credentials/verification are fixed. Evidence: ${ab.evidence.join("; ")}`;
12498
- log.warn(`\u26A0 Auth-block detected - posting ERROR/CONFIG_ISSUE (not FAILED) to skip heal + avoid login hammering`);
12499
- }
12500
- }
12501
- if (runStatus === "FAILED") {
12502
- const cold = detectApiColdSessionBlock({
12503
- tags: run2.tags ?? [],
12504
- authStateExpected,
12505
- authStateInjected: !!storageStatePath,
12506
- needsStandingHeaders: normalRunAuthPlan.needsStandingHeaders,
12507
- standingHeadersInjected: !!result.authDelivery?.standingHeadersInjected,
12508
- error: runError,
12509
- logs: result.logs,
12510
- stepResults: result.stepResults,
12511
- apiCalls: result.apiCalls
12512
- });
12513
- if (cold.detected) {
12514
- runStatus = "ERROR";
12515
- runError = coldSessionConfigMessage("Authenticated @api test ran", cold.reason, cold.evidence);
12516
- log.warn(`\u26A0 Cold @api session detected - posting ERROR/CONFIG_ISSUE (not FAILED) to skip heal + avoid a false app_bug`);
12517
- }
12518
- }
12519
- if (runStatus === "PASSED" && Array.isArray(run2.tags) && run2.tags.includes("@api")) {
12520
- const skipCoverage = detectSkippedApiCoverage(result.stepResults ?? []);
12521
- if (skipCoverage.detected) {
12522
- runStatus = "ERROR";
12523
- runError = skipCoverage.error;
12524
- log.warn(`\u26A0 ${skipCoverage.logLine}`);
12525
- }
12526
- }
12788
+ const authRelabel = (0, import_runner_core8.authReadinessRelabel)({
12789
+ status: runStatus,
12790
+ error: runError,
12791
+ testName: run2.testName ?? run2.testCaseId ?? void 0,
12792
+ tags: run2.tags ?? [],
12793
+ logs: result.logs,
12794
+ stepResults: result.stepResults,
12795
+ apiCalls: result.apiCalls,
12796
+ authStateExpected,
12797
+ authStateInjected: (0, import_runner_core8.storageStateDelivered)(storageStatePath, result),
12798
+ needsStandingHeaders: normalRunAuthPlan.needsStandingHeaders,
12799
+ standingHeadersInjected: !!result.authDelivery?.standingHeadersInjected
12800
+ });
12801
+ if (authRelabel) {
12802
+ runStatus = authRelabel.status;
12803
+ runError = authRelabel.error;
12804
+ log.warn(authRelabel.logLine);
12527
12805
  }
12528
- (0, import_runner_core7.throwIfRunAborted)();
12806
+ (0, import_runner_core9.throwIfRunAborted)();
12529
12807
  await postRunResult({
12530
12808
  status: runStatus,
12531
12809
  stepResults: result.stepResults,
@@ -12542,7 +12820,8 @@ ${result2.logs}`);
12542
12820
  expectedFailure: result.passed ? result.expectedFailure === true : void 0,
12543
12821
  expectedFailureReason: result.passed && result.expectedFailure ? result.expectedFailureReason : void 0,
12544
12822
  unexpectedPass: result.passed ? void 0 : result.unexpectedPass === true,
12545
- bugTestBroken: result.passed ? void 0 : result.bugTestBroken === true
12823
+ bugTestBroken: result.passed ? void 0 : result.bugTestBroken === true,
12824
+ authSession
12546
12825
  });
12547
12826
  if (result.passed) {
12548
12827
  log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
@@ -12558,14 +12837,14 @@ ${result2.logs}`);
12558
12837
  }
12559
12838
  } catch (err) {
12560
12839
  if (err instanceof RunNoLongerActiveError) throw err;
12561
- const abortOutcome = (0, import_runner_core7.runAbortOutcome)(err, signal);
12840
+ const abortOutcome = (0, import_runner_core9.runAbortOutcome)(err, signal);
12562
12841
  if (abortOutcome === "discard") {
12563
- throw new RunNoLongerActiveError((0, import_runner_core7.isSupersededRunAbort)(signal) ? `Run ${run2.runId} was superseded by a newer claim` : `Run ${run2.runId} was cancelled on the server`);
12842
+ throw new RunNoLongerActiveError((0, import_runner_core9.isSupersededRunAbort)(signal) ? `Run ${run2.runId} was superseded by a newer claim` : `Run ${run2.runId} was cancelled on the server`);
12564
12843
  }
12565
12844
  if (abortOutcome === "interrupt") {
12566
12845
  await postRunResult({
12567
12846
  status: "ERROR",
12568
- error: import_runner_core7.RUNNER_SHUTDOWN_INTERRUPT_ERROR,
12847
+ error: import_runner_core9.RUNNER_SHUTDOWN_INTERRUPT_ERROR,
12569
12848
  duration: Date.now() - startTime
12570
12849
  });
12571
12850
  throw err;
@@ -12587,6 +12866,7 @@ ${result2.logs}`);
12587
12866
  }
12588
12867
  }
12589
12868
  async function startRunner(opts) {
12869
+ (0, import_playwright_signal_handlers.disablePlaywrightSignalHandlers)();
12590
12870
  const { serverUrl, authToken, projectId, enableMobile = false, enableMobileTunnel = false } = opts;
12591
12871
  const pollIntervalMs = clampPollInterval(opts.pollIntervalMs);
12592
12872
  process.env.SERVER_URL = serverUrl;
@@ -13042,7 +13322,7 @@ async function startRunner(opts) {
13042
13322
  }
13043
13323
  });
13044
13324
  };
13045
- const removeSupersededRunFetchFence = (0, import_runner_core7.installSupersededRunFetchFence)(serverUrl);
13325
+ const removeSupersededRunFetchFence = (0, import_runner_core9.installSupersededRunFetchFence)(serverUrl);
13046
13326
  const heartbeat = setInterval(sendHeartbeat, 3e4);
13047
13327
  sendHeartbeat();
13048
13328
  let shuttingDown = false;
@@ -13054,7 +13334,7 @@ async function startRunner(opts) {
13054
13334
  headers: { ...headers, "Content-Type": "application/json" },
13055
13335
  body: JSON.stringify(withClaimEpoch({
13056
13336
  status: "ERROR",
13057
- error: import_runner_core7.RUNNER_SHUTDOWN_INTERRUPT_ERROR
13337
+ error: import_runner_core9.RUNNER_SHUTDOWN_INTERRUPT_ERROR
13058
13338
  }, claimEpoch)),
13059
13339
  signal: AbortSignal.timeout(5e3)
13060
13340
  }));
@@ -13070,7 +13350,7 @@ async function startRunner(opts) {
13070
13350
  setTimeout(() => {
13071
13351
  console.error("Shutdown grace elapsed \u2014 forcing exit");
13072
13352
  process.exit(0);
13073
- }, (0, import_runner_core7.shutdownForceExitMs)(SHUTDOWN_FINISH_MS, SHUTDOWN_GRACE_MS)).unref();
13353
+ }, (0, import_runner_core9.shutdownForceExitMs)(SHUTDOWN_FINISH_MS, SHUTDOWN_GRACE_MS)).unref();
13074
13354
  clearInterval(heartbeat);
13075
13355
  heartbeatKeepalive.stop();
13076
13356
  if (liveBrowserHeartbeatTimer) {
@@ -13088,7 +13368,7 @@ async function startRunner(opts) {
13088
13368
  if (claimPending) {
13089
13369
  await Promise.race([claimPending, new Promise((resolve3) => setTimeout(resolve3, SHUTDOWN_FINISH_MS))]);
13090
13370
  }
13091
- await (0, import_runner_core7.drainAbortedRuns)({
13371
+ await (0, import_runner_core9.drainAbortedRuns)({
13092
13372
  finishMs: SHUTDOWN_FINISH_MS,
13093
13373
  graceMs: SHUTDOWN_GRACE_MS,
13094
13374
  inFlight: activeWebRuns,
@@ -13105,17 +13385,19 @@ async function startRunner(opts) {
13105
13385
  await stopAppiumServer();
13106
13386
  }
13107
13387
  await browserPool.shutdown();
13108
- await (0, import_runner_core8.closeLocalGoogleBroker)().catch(() => {
13388
+ await (0, import_runner_core10.closeLocalGoogleBroker)().catch(() => {
13109
13389
  });
13110
13390
  removeSupersededRunFetchFence();
13111
13391
  log.info("Runner stopped.");
13112
13392
  process.exit(0);
13113
13393
  };
13114
- process.once("SIGINT", () => {
13115
- void cleanup();
13116
- });
13117
- process.once("SIGTERM", () => {
13394
+ installShutdownSignals(process, () => {
13118
13395
  void cleanup();
13396
+ }, {
13397
+ forceExit: () => {
13398
+ console.error("Second interrupt \u2014 forcing exit");
13399
+ process.exit(0);
13400
+ }
13119
13401
  });
13120
13402
  const PIDS_WATCHDOG_INTERVAL_MS = Math.max(5e3, envInt("RUNNER_PIDS_WATCHDOG_INTERVAL_MS", 3e4));
13121
13403
  const PIDS_SOFT_WARN_PCT = Math.max(0, Math.min(100, envInt("RUNNER_PIDS_SOFT_WARN_PCT", 70)));
@@ -13167,7 +13449,7 @@ async function startRunner(opts) {
13167
13449
  const abortSupersededRun = (runId, status, serverEpoch) => {
13168
13450
  const controller = activeWebRuns.get(runId);
13169
13451
  const already = !controller || controller.signal.aborted;
13170
- if (controller) (0, import_runner_core7.abortRun)(controller, "superseded", runId);
13452
+ if (controller) (0, import_runner_core9.abortRun)(controller, "superseded", runId);
13171
13453
  activeMobileRuns.get(runId)?.triggerAbort(`was superseded by a newer claim (epoch ${serverEpoch ?? "?"}, status ${status})`);
13172
13454
  if (!already) {
13173
13455
  log.warn(`Run ${runId} was superseded by a newer claim (server epoch ${serverEpoch ?? "?"}, ours ${claimedRunEpochs.get(runId) ?? "?"}) \u2014 stopping it; this runner posts nothing more for it.`);
@@ -13183,7 +13465,7 @@ async function startRunner(opts) {
13183
13465
  (runId, status) => {
13184
13466
  const controller = activeWebRuns.get(runId);
13185
13467
  const already = !controller || controller.signal.aborted;
13186
- if (controller) (0, import_runner_core7.abortRun)(controller, "cancelled", runId);
13468
+ if (controller) (0, import_runner_core9.abortRun)(controller, "cancelled", runId);
13187
13469
  activeMobileRuns.get(runId)?.triggerAbort(`was cancelled server-side (status ${status})`);
13188
13470
  if (!already) log.warn(`Run ${runId} was cancelled server-side (status ${status}) \u2014 stopping it.`);
13189
13471
  }
@@ -13226,6 +13508,10 @@ async function startRunner(opts) {
13226
13508
  for (const run2 of pending) {
13227
13509
  if (shuttingDown) break;
13228
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
+ }
13229
13515
  const runMode = run2.mode ?? "run";
13230
13516
  const isMobileOp = enableMobile && isMobileDeviceOp(run2);
13231
13517
  const discoveryKind = isMobileOp ? discoveryKindOf(run2.discoveryContext) : null;
@@ -13437,9 +13723,9 @@ async function startRunner(opts) {
13437
13723
  runWatchdog.start();
13438
13724
  }
13439
13725
  activeWebRuns.set(run2.runId, runAbort);
13440
- const runWritePolicy = (0, import_runner_core7.parseRunWritePolicy)(run2.runWritePolicy);
13441
- const runSignInSecrets = runWritePolicy ? (0, import_runner_core7.signInSecretsOf)({ ...run2.discoveryContext?.variableContext?.allTestVariables ?? {}, ...run2.testVariables ?? {} }) : [];
13442
- (0, import_runner_core7.withRunWritePolicyScope)(runWritePolicy, () => (0, import_runner_core8.withLocalGoogleScope)({ baseUrl: run2.baseUrl, runId: run2.runId }, () => executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, (testCaseId) => {
13726
+ const runWritePolicy = (0, import_runner_core9.parseRunWritePolicy)(run2.runWritePolicy);
13727
+ const runSignInSecrets = runWritePolicy ? (0, import_runner_core9.signInSecretsOf)({ ...run2.discoveryContext?.variableContext?.allTestVariables ?? {}, ...run2.testVariables ?? {} }) : [];
13728
+ (0, import_runner_core9.withRunWritePolicyScope)(runWritePolicy, () => (0, import_runner_core10.withLocalGoogleScope)({ baseUrl: run2.baseUrl, runId: run2.runId }, () => executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, (testCaseId) => {
13443
13729
  if (mobilePlatform) {
13444
13730
  mobileHealHold.set(testCaseId, { expiry: Date.now() + MOBILE_HEAL_HOLD_MS, platform: mobilePlatform });
13445
13731
  }
@@ -13447,11 +13733,11 @@ async function startRunner(opts) {
13447
13733
  runsCompleted++;
13448
13734
  }).catch((err) => {
13449
13735
  const abortReason = runAbort.signal.reason;
13450
- if ((0, import_runner_core7.isRunAbortError)(abortReason) && abortReason.reason === "shutdown") {
13736
+ if ((0, import_runner_core9.isRunAbortError)(abortReason) && abortReason.reason === "shutdown") {
13451
13737
  log.info(`Run ${run2.runId} was interrupted by runner shutdown.`);
13452
13738
  return;
13453
13739
  }
13454
- if ((0, import_runner_core7.isSupersededRunAbort)(runAbort.signal)) {
13740
+ if ((0, import_runner_core9.isSupersededRunAbort)(runAbort.signal)) {
13455
13741
  log.info(`Run ${run2.runId} was superseded by a newer claim; this runner posted nothing more for it.`);
13456
13742
  return;
13457
13743
  }
@@ -13489,7 +13775,7 @@ async function startRunner(opts) {
13489
13775
  const action = authRefusalAction({
13490
13776
  consecutive: consecutiveAuthRefusals,
13491
13777
  activeRuns,
13492
- cloud: process.env.RUNNER_CLOUD === "true"
13778
+ cloud: IS_CLOUD
13493
13779
  });
13494
13780
  if (action === "exit") {
13495
13781
  log.fail(`${message} (the server refused it ${consecutiveAuthRefusals} times in a row)`);
@@ -13545,14 +13831,15 @@ async function checkRunnerLogin(serverUrl, token, fetchImpl = fetch) {
13545
13831
 
13546
13832
  // src/services/sign-in-capture.ts
13547
13833
  var import_browser_config3 = __toESM(require_browser_config());
13834
+ var import_google_local_signin = __toESM(require_google_local_signin());
13548
13835
  import { chromium } from "playwright";
13549
13836
  async function captureSignInInteractively(url, ready, opts = {}) {
13550
13837
  const target = new URL(url);
13551
13838
  let browser;
13552
13839
  try {
13553
- browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], channel: "chrome", env: (0, import_browser_config3.browserLaunchEnv)() });
13840
+ browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], args: [...import_google_local_signin.SIGN_IN_WINDOW_ARGS], channel: "chrome", env: (0, import_browser_config3.browserLaunchEnv)() });
13554
13841
  } catch {
13555
- browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], env: (0, import_browser_config3.browserLaunchEnv)() });
13842
+ browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], args: [...import_google_local_signin.SIGN_IN_WINDOW_ARGS], env: (0, import_browser_config3.browserLaunchEnv)() });
13556
13843
  }
13557
13844
  return captureFromBrowser(browser, target, ready, opts.timeoutMs ?? SIGN_IN_TIMEOUT_MS);
13558
13845
  }
@@ -13813,7 +14100,9 @@ async function superviseRunnerStart(argv, authTokenForChild) {
13813
14100
  stdio: "inherit",
13814
14101
  env: childEnv
13815
14102
  });
14103
+ let signalled = false;
13816
14104
  const onSignal = (sig) => {
14105
+ signalled = true;
13817
14106
  try {
13818
14107
  child.kill(sig);
13819
14108
  } catch {
@@ -13825,19 +14114,19 @@ async function superviseRunnerStart(argv, authTokenForChild) {
13825
14114
  process.removeListener("SIGINT", onSignal);
13826
14115
  process.removeListener("SIGTERM", onSignal);
13827
14116
  if (signal === "SIGINT" || signal === "SIGTERM") {
13828
- resolve3(0);
14117
+ resolve3({ code: 0, signalled: true });
13829
14118
  return;
13830
14119
  }
13831
- resolve3(code ?? 1);
14120
+ resolve3({ code: code ?? 1, signalled });
13832
14121
  });
13833
14122
  });
13834
14123
  console.log("\u{1F6E1} Runner supervisor enabled \u2014 will auto-restart on crash/stuck exit");
13835
14124
  console.log(" (disable with --no-supervise or VALIDATE_RUNNER_SUPERVISE=0)");
13836
14125
  for (; ; ) {
13837
14126
  const startedAt = Date.now();
13838
- const code = await runOnce();
14127
+ const { code, signalled } = await runOnce();
13839
14128
  const next = nextSupervisorAction(
13840
- { code, ranMs: Date.now() - startedAt, now: Date.now() },
14129
+ { code, ranMs: Date.now() - startedAt, now: Date.now(), signalled },
13841
14130
  state2,
13842
14131
  { maxRestartsPerHour, minBackoffMs, maxBackoffMs }
13843
14132
  );
@@ -13918,7 +14207,7 @@ program.command("start").description("Start the local test runner").option("--to
13918
14207
  console.warn("\u26A0 --tunnel has no effect without --mobile (the mobile bridge it exposes only starts with --mobile). Ignoring --tunnel.");
13919
14208
  }
13920
14209
  {
13921
- const { hasLocalGoogleLogin, GOOGLE_LOGIN_COMMAND } = await import("./dist-DFNLMH6L.mjs");
14210
+ const { hasLocalGoogleLogin, GOOGLE_LOGIN_COMMAND } = await import("./dist-52M5RDZ6.mjs");
13922
14211
  if (process.stdout.isTTY && !await hasLocalGoogleLogin()) {
13923
14212
  console.log(`Tip: for apps with "Continue with Google", sign in to Google once on this machine: ${GOOGLE_LOGIN_COMMAND}`);
13924
14213
  }
@@ -13948,7 +14237,7 @@ async function askLine(question) {
13948
14237
  }
13949
14238
  }
13950
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) => {
13951
- const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-DFNLMH6L.mjs");
14240
+ const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-52M5RDZ6.mjs");
13952
14241
  console.log(LOCAL_GOOGLE_WARNING);
13953
14242
  if (!opts.yes) {
13954
14243
  const answer = await askLine('Is this a dedicated test Google account? Type "yes" to continue: ');
@@ -13983,7 +14272,7 @@ program.command("google-login").description('Sign in to a TEST Google account on
13983
14272
  }
13984
14273
  });
13985
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) => {
13986
- const { allowLocalGoogleSites, grantedSite } = await import("./dist-DFNLMH6L.mjs");
14275
+ const { allowLocalGoogleSites, grantedSite } = await import("./dist-52M5RDZ6.mjs");
13987
14276
  const site = grantedSite(url);
13988
14277
  if (!site) {
13989
14278
  console.error("\u2717 Not a web address: give it like https://app.example.com");
@@ -13997,7 +14286,7 @@ program.command("google-allow-site <url>").description("Let the Google sign-in o
13997
14286
  console.log(`\u2713 Tests on this machine may now use the Google sign-in for: ${sites.join(", ")}`);
13998
14287
  });
13999
14288
  program.command("google-logout").description("Forget the Google sign-in saved on this machine").action(async () => {
14000
- const { deleteLocalGoogleLogin } = await import("./dist-DFNLMH6L.mjs");
14289
+ const { deleteLocalGoogleLogin } = await import("./dist-52M5RDZ6.mjs");
14001
14290
  await deleteLocalGoogleLogin();
14002
14291
  console.log("\u2713 Google sign-in removed from this machine");
14003
14292
  });