@validate.qa/runner 1.0.55 → 1.0.57

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 (3) hide show
  1. package/dist/cli.js +1547 -570
  2. package/dist/cli.mjs +1547 -570
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1767,7 +1767,12 @@ var init_constants = __esm({
1767
1767
  "VALIDATE_TASK_ARN",
1768
1768
  "VALIDATE_RUNNER_AUTH",
1769
1769
  "VALIDATE_SERVER_URL",
1770
- "VALIDATE_LIVE_BROWSER_STATE_PATH"
1770
+ "VALIDATE_LIVE_BROWSER_STATE_PATH",
1771
+ // Register-and-verify test identity (parent-minted per run; see runner-core
1772
+ // test-inbox-broker.ts). A customer variable must never spoof the address a
1773
+ // generated test registers with or the derived account password.
1774
+ "VQA_TEST_INBOX_EMAIL",
1775
+ "VQA_TEST_ACCOUNT_PASSWORD"
1771
1776
  ]);
1772
1777
  SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
1773
1778
  // Runtime essentials - Node.js and Playwright need these to function
@@ -2253,7 +2258,8 @@ function selectorCandidatesForStep(step) {
2253
2258
  const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
2254
2259
  const pushValueCandidates = (value) => {
2255
2260
  candidates.push(selectorForValue(value));
2256
- if (!hasExplicitResourceId && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
2261
+ const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
2262
+ if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
2257
2263
  candidates.push(`id=${value}`);
2258
2264
  }
2259
2265
  };
@@ -3863,8 +3869,9 @@ function inlineLocatorBindings(stmts) {
3863
3869
  function stripLoginScaffolding(code) {
3864
3870
  const out = [];
3865
3871
  let inAuth = false;
3872
+ let inInbox = false;
3866
3873
  for (const ln of code.split("\n")) {
3867
- if (!inAuth && AUTH_START.test(ln)) {
3874
+ if (!inAuth && !inInbox && AUTH_START.test(ln)) {
3868
3875
  inAuth = true;
3869
3876
  continue;
3870
3877
  }
@@ -3873,6 +3880,15 @@ function stripLoginScaffolding(code) {
3873
3880
  inAuth = false;
3874
3881
  continue;
3875
3882
  }
3883
+ if (!inInbox && INBOX_START.test(ln)) {
3884
+ inInbox = true;
3885
+ continue;
3886
+ }
3887
+ if (inInbox) {
3888
+ if (INBOX_END.test(ln))
3889
+ inInbox = false;
3890
+ continue;
3891
+ }
3876
3892
  if (LOGIN_CALL_LINE.test(ln))
3877
3893
  continue;
3878
3894
  out.push(ln);
@@ -4261,7 +4277,7 @@ function validateAssertionStrength(code, opts = {}) {
4261
4277
  signals
4262
4278
  };
4263
4279
  }
4264
- var ERROR_BANNER, OUTCOME_ALWAYS, FORM_INPUT, ANY_CLICK, NAV_LOCATOR, URL_PROOF, RELOAD, EXPECT_STMT, BLOCK_SCAFFOLD, LOCATOR_BINDING, STATE_MATCHER, CHROME_LOCATOR, CONTENT_ROLE, GET_BY_TEXT, HAS_TEXT_FILTER, OUTCOME_TEXT, MESSAGE_SURFACE, TEXT_CONTENT_MATCHER, VISIBILITY_MATCHER, ADDITIVE_AFFORDANCE, EXCLUDE_AFFORDANCE, EMPTY_STATE_TEXT, AUTH_START, AUTH_END, LOGIN_CALL_LINE, NEGATED_ASSERT, TYPED_INPUT, TITLE_LINE, ROLE_NAME_RE, INTERACT_VERB, ROLE_SIG_RE, TRANSIENT_CONTROL_ROLES;
4280
+ var ERROR_BANNER, OUTCOME_ALWAYS, FORM_INPUT, ANY_CLICK, NAV_LOCATOR, URL_PROOF, RELOAD, EXPECT_STMT, BLOCK_SCAFFOLD, LOCATOR_BINDING, STATE_MATCHER, CHROME_LOCATOR, CONTENT_ROLE, GET_BY_TEXT, HAS_TEXT_FILTER, OUTCOME_TEXT, MESSAGE_SURFACE, TEXT_CONTENT_MATCHER, VISIBILITY_MATCHER, ADDITIVE_AFFORDANCE, EXCLUDE_AFFORDANCE, EMPTY_STATE_TEXT, AUTH_START, AUTH_END, LOGIN_CALL_LINE, INBOX_START, INBOX_END, NEGATED_ASSERT, TYPED_INPUT, TITLE_LINE, ROLE_NAME_RE, INTERACT_VERB, ROLE_SIG_RE, TRANSIENT_CONTROL_ROLES;
4265
4281
  var init_assertion_strength = __esm({
4266
4282
  "../shared/dist/assertion-strength.js"() {
4267
4283
  "use strict";
@@ -4311,6 +4327,8 @@ var init_assertion_strength = __esm({
4311
4327
  AUTH_START = /^\s*\/\/\s*>>>\s*VALIDATE_QA_AUTH/;
4312
4328
  AUTH_END = /^\s*\/\/\s*<<<\s*VALIDATE_QA_AUTH/;
4313
4329
  LOGIN_CALL_LINE = /^\s*await\s+__vqaLogin\s*\(\s*page\s*\)\s*;?\s*$/;
4330
+ INBOX_START = /^\s*\/\/\s*>>>\s*VALIDATE_QA_INBOX/;
4331
+ INBOX_END = /^\s*\/\/\s*<<<\s*VALIDATE_QA_INBOX/;
4314
4332
  NEGATED_ASSERT = /\.not\.(toBeVisible|toContainText|toHaveText|toBeChecked)\b|\.toBeHidden\s*\(/;
4315
4333
  TYPED_INPUT = /\.(fill|type|pressSequentially|setInputFiles)\s*\(/;
4316
4334
  TITLE_LINE = /^\s*(?:test|describe)(?:\.\w+)?\s*\(/;
@@ -4353,10 +4371,7 @@ function isEnvironmentalError(run2) {
4353
4371
  return false;
4354
4372
  if (run2.status !== "ERROR")
4355
4373
  return false;
4356
- const err = run2.error;
4357
- if (typeof err !== "string")
4358
- return false;
4359
- return ENVIRONMENTAL_ERROR_PREFIXES.some((p) => err.startsWith(p));
4374
+ return classifyErrorMessage(run2.error) !== null;
4360
4375
  }
4361
4376
  function isCountedFailure(run2) {
4362
4377
  if (!run2)
@@ -4375,9 +4390,10 @@ function environmentalReason(run2) {
4375
4390
  function classifyErrorMessage(error2) {
4376
4391
  if (typeof error2 !== "string")
4377
4392
  return null;
4378
- if (error2.startsWith("RATE_LIMIT_BLOCKED:"))
4393
+ const normalized = error2.replace(/^\s+/, "").replace(/^Error:\s*/i, "");
4394
+ if (normalized.startsWith("RATE_LIMIT_BLOCKED:"))
4379
4395
  return "rate_limit";
4380
- if (error2.startsWith("CONFIG_ISSUE:"))
4396
+ if (normalized.startsWith("CONFIG_ISSUE:"))
4381
4397
  return "config_issue";
4382
4398
  return null;
4383
4399
  }
@@ -7783,12 +7799,16 @@ var require_mobile_keyboard = __commonJS({
7783
7799
  "use strict";
7784
7800
  Object.defineProperty(exports2, "__esModule", { value: true });
7785
7801
  exports2.shouldRetryTapAfterKeyboardDismiss = shouldRetryTapAfterKeyboardDismiss;
7802
+ exports2.hasFocusedEditableElement = hasFocusedEditableElement;
7786
7803
  exports2.dismissKeyboardForRetry = dismissKeyboardForRetry;
7787
7804
  var EDITABLE_FOCUSED_ROLE_RE = /(EditText|TextField|SecureTextField|SearchField|TextView)$/i;
7788
7805
  var OUTSIDE_VIEWPORT_ERROR_RE = /(outside|out of).*?(bounds|viewport|visible|screen|window)|coordinate.*?(invalid|outside)/i;
7789
7806
  function shouldRetryTapAfterKeyboardDismiss(error2, elements) {
7790
7807
  if (!error2 || !OUTSIDE_VIEWPORT_ERROR_RE.test(error2))
7791
7808
  return false;
7809
+ return hasFocusedEditableElement(elements);
7810
+ }
7811
+ function hasFocusedEditableElement(elements) {
7792
7812
  return elements.some((el) => el.focused === true && EDITABLE_FOCUSED_ROLE_RE.test(el.role));
7793
7813
  }
7794
7814
  async function dismissKeyboardForRetry(driver) {
@@ -72595,7 +72615,7 @@ var require_verification_inbox = __commonJS({
72595
72615
  };
72596
72616
  })();
72597
72617
  Object.defineProperty(exports2, "__esModule", { value: true });
72598
- exports2.VerificationInboxReader = void 0;
72618
+ exports2.VerificationInboxReader = exports2.InboxAuthError = void 0;
72599
72619
  exports2.isVerificationInboxConfigured = isVerificationInboxConfigured;
72600
72620
  exports2.describeVerificationInboxConfigGap = describeVerificationInboxConfigGap;
72601
72621
  exports2.loadVerificationInboxConfig = loadVerificationInboxConfig;
@@ -72607,8 +72627,10 @@ var require_verification_inbox = __commonJS({
72607
72627
  exports2.applyPlusTag = applyPlusTag;
72608
72628
  exports2.deriveRegistrationPassword = deriveRegistrationPassword;
72609
72629
  exports2.extractVerificationCode = extractVerificationCode;
72630
+ exports2.extractVerificationCodeDetailed = extractVerificationCodeDetailed;
72610
72631
  exports2.extractVerificationLink = extractVerificationLink;
72611
72632
  exports2.collectMessageUrls = collectMessageUrls;
72633
+ exports2.isInboxAuthFailure = isInboxAuthFailure;
72612
72634
  var node_crypto_1 = require("crypto");
72613
72635
  var DEFAULT_MAILBOX = "INBOX";
72614
72636
  var DEFAULT_GMAIL_SPAM_MAILBOX = "[Gmail]/Spam";
@@ -72781,6 +72803,9 @@ var require_verification_inbox = __commonJS({
72781
72803
  var CODE_TOKEN = new RegExp(`\\b(${CODE_ALT})\\b`, "gi");
72782
72804
  var YEAR_CONTEXT = /(©|\(c\)|copyright|all rights reserved|®)/i;
72783
72805
  function extractVerificationCode(text, html, subject) {
72806
+ return extractVerificationCodeDetailed(text, html, subject).code;
72807
+ }
72808
+ function extractVerificationCodeDetailed(text, html, subject) {
72784
72809
  const subj = collapseSpacedDigits(subject || "");
72785
72810
  const bodies = [text || "", stripHtml(html || "")].map(collapseSpacedDigits);
72786
72811
  const withSubject = [subj, ...bodies];
@@ -72789,7 +72814,7 @@ var require_verification_inbox = __commonJS({
72789
72814
  if (lone) {
72790
72815
  const c = cleanCode(lone[1]);
72791
72816
  if (c)
72792
- return c;
72817
+ return { code: c, confidence: "subject" };
72793
72818
  }
72794
72819
  }
72795
72820
  for (const body of withSubject) {
@@ -72801,26 +72826,28 @@ var require_verification_inbox = __commonJS({
72801
72826
  continue;
72802
72827
  const m = matchBestCode(line);
72803
72828
  if (m)
72804
- return m;
72829
+ return { code: m, confidence: "keyword" };
72805
72830
  }
72806
72831
  const windowed = body.match(new RegExp(`${CODE_KEYWORDS.source}[\\s\\S]{0,40}?\\b(${CODE_ALT})\\b`, "i"));
72807
72832
  if (windowed && windowed[1]) {
72808
72833
  const cleaned = cleanCode(windowed[1]);
72809
72834
  if (cleaned && !looksLikeYearOrDate(cleaned))
72810
- return cleaned;
72835
+ return { code: cleaned, confidence: "keyword" };
72811
72836
  }
72812
72837
  }
72838
+ const distinct = /* @__PURE__ */ new Set();
72813
72839
  for (const body of bodies) {
72814
72840
  if (!body)
72815
72841
  continue;
72816
- const candidates = body.match(/\b[0-9]{4,8}\b/g);
72817
- if (!candidates)
72818
- continue;
72819
- const real = candidates.find((n) => !looksLikeYearOrDate(n));
72820
- if (real)
72821
- return real;
72842
+ for (const n of body.match(/\b[0-9]{4,8}\b/g) ?? []) {
72843
+ if (!looksLikeYearOrDate(n))
72844
+ distinct.add(n);
72845
+ }
72822
72846
  }
72823
- return null;
72847
+ if (distinct.size === 1) {
72848
+ return { code: distinct.values().next().value ?? null, confidence: "fallback" };
72849
+ }
72850
+ return { code: null, confidence: null };
72824
72851
  }
72825
72852
  function matchBestCode(line) {
72826
72853
  const hasYearContext = YEAR_CONTEXT.test(line);
@@ -72942,6 +72969,28 @@ var require_verification_inbox = __commonJS({
72942
72969
  }
72943
72970
  var MAX_WAIT_CEILING_MS = 18e4;
72944
72971
  var MIN_POLL_MS = 2e3;
72972
+ var InboxAuthError = class extends Error {
72973
+ constructor(message) {
72974
+ super(message);
72975
+ this.name = "InboxAuthError";
72976
+ }
72977
+ };
72978
+ exports2.InboxAuthError = InboxAuthError;
72979
+ function isInboxAuthFailure(error2) {
72980
+ if (!error2)
72981
+ return false;
72982
+ if (error2 instanceof InboxAuthError)
72983
+ return true;
72984
+ const e = error2;
72985
+ if (e.authenticationFailed === true)
72986
+ return true;
72987
+ const hay = `${e.responseText ?? ""} ${e.message ?? ""}`;
72988
+ return /\bAUTHENTICATIONFAILED\b|\bInvalid credentials\b/i.test(hay);
72989
+ }
72990
+ function isPlusTagged(address) {
72991
+ const at = address.indexOf("@");
72992
+ return at > 0 && address.slice(0, at).includes("+");
72993
+ }
72945
72994
  var VerificationInboxReader = class {
72946
72995
  config;
72947
72996
  client = null;
@@ -72971,7 +73020,14 @@ var require_verification_inbox = __commonJS({
72971
73020
  });
72972
73021
  client.on("error", () => {
72973
73022
  });
72974
- await client.connect();
73023
+ try {
73024
+ await client.connect();
73025
+ } catch (error2) {
73026
+ if (isInboxAuthFailure(error2)) {
73027
+ throw new InboxAuthError(`IMAP authentication rejected for ${this.config.user} \u2014 the inbox app password is revoked or wrong`);
73028
+ }
73029
+ throw error2;
73030
+ }
72975
73031
  this.client = client;
72976
73032
  return client;
72977
73033
  })();
@@ -73016,7 +73072,10 @@ var require_verification_inbox = __commonJS({
73016
73072
  } finally {
73017
73073
  lock.release();
73018
73074
  }
73019
- } catch {
73075
+ } catch (error2) {
73076
+ if (isInboxAuthFailure(error2)) {
73077
+ throw error2 instanceof InboxAuthError ? error2 : new InboxAuthError(`IMAP authentication rejected for ${this.config.user} \u2014 the inbox app password is revoked or wrong`);
73078
+ }
73020
73079
  return { uid: 0, uidValidity: 0 };
73021
73080
  }
73022
73081
  }
@@ -73067,23 +73126,52 @@ var require_verification_inbox = __commonJS({
73067
73126
  }
73068
73127
  }
73069
73128
  /**
73070
- * Poll until a matching message arrives or the deadline passes. Returns the
73071
- * newest matching message or null on timeout.
73129
+ * Poll until a message with a STRONG verification signal (subject/keyword
73130
+ * code or a positive-scoring link) arrives, or the deadline passes.
73131
+ *
73132
+ * Signal-less mail addressed to the same recipient (a welcome email, a
73133
+ * receipt) is remembered but polled PAST — returning it immediately used to
73134
+ * hand welcome-mail order numbers to the caller as "the code" whenever the
73135
+ * welcome email out-raced the verification email. Only at the deadline is
73136
+ * the newest signal-less message surfaced (better than pretending the
73137
+ * mailbox is empty: the caller can still read it and decide).
73072
73138
  */
73073
73139
  async waitForMessage(opts) {
73074
73140
  const maxWaitMs = Math.min(opts.maxWaitMs ?? 9e4, MAX_WAIT_CEILING_MS);
73075
73141
  const pollIntervalMs = Math.max(opts.pollIntervalMs ?? 5e3, MIN_POLL_MS);
73076
73142
  const deadline = startTime() + maxWaitMs;
73143
+ const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
73144
+ const satisfies = (m) => {
73145
+ if (opts.require === "code")
73146
+ return m.code !== null && m.codeConfidence !== "fallback";
73147
+ if (opts.require === "link")
73148
+ return m.link !== null;
73149
+ return m.hasVerificationSignal;
73150
+ };
73151
+ let weakest = null;
73152
+ const check2 = async () => {
73153
+ if (opts.stats)
73154
+ opts.stats.filteredOut = 0;
73155
+ const messages = await this.fetchMatching({ ...opts, limit }, true);
73156
+ if (!opts.require)
73157
+ return messages[0] ?? null;
73158
+ const strong = messages.find((m) => satisfies(m) && !m.senderMismatch) ?? messages.find(satisfies);
73159
+ if (strong)
73160
+ return strong;
73161
+ if (messages[0])
73162
+ weakest = messages[0];
73163
+ return null;
73164
+ };
73077
73165
  do {
73078
- const latest = await this.getLatest(opts);
73079
- if (latest)
73080
- return latest;
73166
+ const found = await check2();
73167
+ if (found)
73168
+ return found;
73081
73169
  const remaining = deadline - startTime();
73082
73170
  if (remaining <= 0)
73083
73171
  break;
73084
73172
  await sleep(Math.min(pollIntervalMs, remaining));
73085
73173
  } while (startTime() < deadline);
73086
- return this.getLatest(opts);
73174
+ return await check2() ?? weakest;
73087
73175
  }
73088
73176
  async fetchMatching(opts, withBody) {
73089
73177
  const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
@@ -73097,7 +73185,7 @@ var require_verification_inbox = __commonJS({
73097
73185
  throw e;
73098
73186
  }
73099
73187
  }
73100
- results.sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
73188
+ results.sort((a, b) => Number(a.senderMismatch ?? false) - Number(b.senderMismatch ?? false) || Date.parse(b.date) - Date.parse(a.date));
73101
73189
  return results.slice(0, limit);
73102
73190
  }
73103
73191
  async fetchMatchingInMailbox(opts, withBody, mailbox, limit) {
@@ -73105,6 +73193,7 @@ var require_verification_inbox = __commonJS({
73105
73193
  const since = new Date(Math.max(0, opts.sinceMs));
73106
73194
  const lock = await client.getMailboxLock(mailbox);
73107
73195
  const results = [];
73196
+ const taggedRecipient = isPlusTagged(opts.toAddress);
73108
73197
  try {
73109
73198
  try {
73110
73199
  await client.noop();
@@ -73148,17 +73237,23 @@ var require_verification_inbox = __commonJS({
73148
73237
  const receivedMs = (parsed.date ?? msg.envelope?.date ?? /* @__PURE__ */ new Date(0)).getTime();
73149
73238
  if (!useUid && receivedMs < opts.sinceMs - 6e4)
73150
73239
  continue;
73151
- if (opts.fromMatch && !matchField(parsed.from?.text, opts.fromMatch))
73240
+ const filterActive = Boolean(opts.fromMatch || opts.subjectMatch);
73241
+ const filterMatched = (!opts.fromMatch || matchField(parsed.from?.text, opts.fromMatch)) && (!opts.subjectMatch || matchField(parsed.subject, opts.subjectMatch));
73242
+ if (filterActive && !filterMatched && !taggedRecipient) {
73243
+ if (opts.stats)
73244
+ opts.stats.filteredOut += 1;
73152
73245
  continue;
73153
- if (opts.subjectMatch && !matchField(parsed.subject, opts.subjectMatch))
73154
- continue;
73155
- results.push({ uid: msg.uid, parsed, receivedMs });
73246
+ }
73247
+ results.push({ uid: msg.uid, parsed, receivedMs, senderMismatch: filterActive && !filterMatched });
73156
73248
  }
73157
73249
  } finally {
73158
73250
  lock.release();
73159
73251
  }
73160
- results.sort((a, b) => b.receivedMs - a.receivedMs);
73161
- return results.slice(0, limit).map((r) => toMessage(messageId(mailbox, String(r.uid), this.config.mailbox), r.parsed, withBody, mailbox));
73252
+ results.sort((a, b) => Number(a.senderMismatch) - Number(b.senderMismatch) || b.receivedMs - a.receivedMs);
73253
+ return results.slice(0, limit).map((r) => ({
73254
+ ...toMessage(messageId(mailbox, String(r.uid), this.config.mailbox), r.parsed, withBody, mailbox),
73255
+ ...r.senderMismatch ? { senderMismatch: true } : {}
73256
+ }));
73162
73257
  }
73163
73258
  async close() {
73164
73259
  const client = this.client;
@@ -73189,12 +73284,18 @@ var require_verification_inbox = __commonJS({
73189
73284
  const text = parsed.text || "";
73190
73285
  const html = typeof parsed.html === "string" ? parsed.html : "";
73191
73286
  const summary = toSummary({ uid, parsed, mailbox });
73287
+ const { code, confidence } = extractVerificationCodeDetailed(text, html, parsed.subject || "");
73288
+ const link = extractVerificationLink(text, html);
73192
73289
  return {
73193
73290
  ...summary,
73194
73291
  text: withBody ? text : "",
73195
73292
  html: withBody ? html : "",
73196
- code: extractVerificationCode(text, html, parsed.subject || ""),
73197
- link: extractVerificationLink(text, html)
73293
+ code,
73294
+ link,
73295
+ codeConfidence: confidence,
73296
+ // A fallback-confidence code is NOT a signal: it is exactly the shape a
73297
+ // welcome/receipt email produces (one stray number, no keyword context).
73298
+ hasVerificationSignal: code !== null && confidence !== "fallback" || link !== null
73198
73299
  };
73199
73300
  }
73200
73301
  function toSummary(input) {
@@ -73392,7 +73493,11 @@ var require_inbox_tools = __commonJS({
73392
73493
  function err(message) {
73393
73494
  return { content: JSON.stringify({ error: message }), didMutate: false };
73394
73495
  }
73496
+ var INBOX_AUTH_DEAD_GUIDANCE = "the inbox authentication is PERMANENTLY rejected (revoked/wrong app password) \u2014 STOP calling inbox tools; none of them can succeed this run. Treat email verification as UNAVAILABLE: hard-stop at any emailed code/link wall, NEVER type a guessed code, and report verification-gated flows as BLOCKED (blockedReason: inbox authentication failed). Continue with publicly reachable coverage.";
73395
73497
  async function dispatchInboxTool(toolName, toolArgs, ctx) {
73498
+ if (ctx.state.authDead) {
73499
+ return err(`${toolName} skipped: ${INBOX_AUTH_DEAD_GUIDANCE}`);
73500
+ }
73396
73501
  try {
73397
73502
  switch (toolName) {
73398
73503
  case "provision_identity":
@@ -73414,6 +73519,10 @@ var require_inbox_tools = __commonJS({
73414
73519
  }
73415
73520
  } catch (e) {
73416
73521
  const message = e instanceof Error ? e.message : String(e);
73522
+ if ((0, verification_inbox_js_1.isInboxAuthFailure)(e)) {
73523
+ ctx.state.authDead = true;
73524
+ return err(`${toolName} failed: ${INBOX_AUTH_DEAD_GUIDANCE}`);
73525
+ }
73417
73526
  return err(`${toolName} failed: ${message}`);
73418
73527
  }
73419
73528
  }
@@ -73471,6 +73580,7 @@ var require_inbox_tools = __commonJS({
73471
73580
  return notReady;
73472
73581
  const maxWaitSeconds = clampNumber(args.maxWaitSeconds, 90, 5, 180);
73473
73582
  const { toAddress, altAddress } = inboxRecipients(ctx.state);
73583
+ const stats = { filteredOut: 0 };
73474
73584
  const msg = await ctx.reader.waitForMessage({
73475
73585
  toAddress,
73476
73586
  altAddress,
@@ -73479,13 +73589,16 @@ var require_inbox_tools = __commonJS({
73479
73589
  sinceUidValidity: ctx.state.sinceUidValidity,
73480
73590
  fromMatch: asStr(args.senderMatch),
73481
73591
  subjectMatch: asStr(args.subjectMatch),
73482
- maxWaitMs: maxWaitSeconds * 1e3
73592
+ maxWaitMs: maxWaitSeconds * 1e3,
73593
+ stats
73483
73594
  });
73484
73595
  if (!msg) {
73596
+ const filterHint = stats.filteredOut > 0 ? ` NOTE: ${stats.filteredOut} email(s) addressed to you WERE found but were excluded by your senderMatch/subjectMatch filter \u2014 transactional mail often comes from a delivery provider (mailgun/sendgrid/amazonses), so call wait_for_verification again WITHOUT senderMatch.` : "";
73485
73597
  return ok({
73486
73598
  found: false,
73487
73599
  waitedSeconds: maxWaitSeconds,
73488
- hint: 'No verification email yet \u2014 usually still in transit, not necessarily a failure. Call wait_for_verification AGAIN (safe to repeat; allow 30-90s total) and you may click an in-app "Resend" control AT MOST ONCE. Do NOT re-submit the signup form, do NOT create another account, and do not immediately assume your credentials are wrong \u2014 your account most likely already exists. BUT if you have already re-polled several times AND resent once and still nothing arrives, the signup may have genuinely failed: check the page for an error/validation message and report it (finish / needs-review) rather than re-polling forever \u2014 and still do not re-register. Use check_inbox to see what has arrived.'
73600
+ ...stats.filteredOut > 0 ? { filteredOut: stats.filteredOut } : {},
73601
+ hint: `No verification email yet \u2014 usually still in transit, not necessarily a failure.${filterHint} Call wait_for_verification AGAIN (safe to repeat; allow 30-90s total) and you may click an in-app "Resend" control AT MOST ONCE. Do NOT re-submit the signup form, do NOT create another account, and do not immediately assume your credentials are wrong \u2014 your account most likely already exists. BUT if you have already re-polled several times AND resent once and still nothing arrives, the signup may have genuinely failed: check the page for an error/validation message and report it (finish / needs-review) rather than re-polling forever \u2014 and still do not re-register. Use check_inbox to see what has arrived.`
73489
73602
  }, ctx.maxResultChars);
73490
73603
  }
73491
73604
  rememberLink(ctx.state, msg);
@@ -73497,6 +73610,8 @@ var require_inbox_tools = __commonJS({
73497
73610
  subject: msg.subject,
73498
73611
  from: msg.from,
73499
73612
  code: msg.code,
73613
+ ...msg.codeConfidence === "fallback" ? { codeConfidence: "low", codeNote: "This code was the only number in the email but had no verification keyword next to it \u2014 verify against read_verification_email if entering it fails." } : {},
73614
+ ...msg.senderMismatch ? { senderMismatch: true, senderNote: "This email is addressed to your exact registration address but its sender did not match your senderMatch \u2014 the recipient match is authoritative for your plus-tagged address." } : {},
73500
73615
  link,
73501
73616
  next: msg.code && link ? "Both a code and a confirmation link were found. Prefer typing the code into the verification field; if the app instead wants you to click, call open_verification_link with the link (and if the page it lands on then asks for the code, enter the code there)." : msg.code ? "Type this code into the verification field." : link ? "Call open_verification_link with this link to confirm. It navigates your current tab to the verify page; after it confirms, return to the app to keep exploring." : msg.link ? "A confirmation link was found but points at a private/internal host and was withheld for safety. Read the email body if you need to inspect it." : "No code/link auto-detected \u2014 call read_verification_email to read the body."
73502
73617
  }, ctx.maxResultChars);
@@ -73630,20 +73745,42 @@ var require_inbox_tools = __commonJS({
73630
73745
  return false;
73631
73746
  if (host === "::1" || host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd"))
73632
73747
  return false;
73633
- const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
73634
- if (m) {
73635
- const [a, b] = [Number(m[1]), Number(m[2])];
73636
- if (a === 127 || a === 0 || a === 10)
73748
+ if (host.includes(":")) {
73749
+ if (host === "::" || host === "::ffff:0:0")
73637
73750
  return false;
73638
- if (a === 169 && b === 254)
73639
- return false;
73640
- if (a === 192 && b === 168)
73641
- return false;
73642
- if (a === 172 && b >= 16 && b <= 31)
73643
- return false;
73644
- if (a >= 224)
73751
+ const dotted = host.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
73752
+ if (dotted && !ipv4IsPublic(dotted[1]))
73645
73753
  return false;
73754
+ const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
73755
+ if (mapped) {
73756
+ const hi = parseInt(mapped[1], 16);
73757
+ const lo = parseInt(mapped[2], 16);
73758
+ const ipv4 = `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
73759
+ if (!ipv4IsPublic(ipv4))
73760
+ return false;
73761
+ }
73646
73762
  }
73763
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) && !ipv4IsPublic(host))
73764
+ return false;
73765
+ return true;
73766
+ }
73767
+ function ipv4IsPublic(ipv4) {
73768
+ const m = ipv4.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
73769
+ if (!m)
73770
+ return true;
73771
+ const [a, b] = [Number(m[1]), Number(m[2])];
73772
+ if (a > 255 || b > 255 || Number(m[3]) > 255 || Number(m[4]) > 255)
73773
+ return false;
73774
+ if (a === 127 || a === 0 || a === 10)
73775
+ return false;
73776
+ if (a === 169 && b === 254)
73777
+ return false;
73778
+ if (a === 192 && b === 168)
73779
+ return false;
73780
+ if (a === 172 && b >= 16 && b <= 31)
73781
+ return false;
73782
+ if (a >= 224)
73783
+ return false;
73647
73784
  return true;
73648
73785
  }
73649
73786
  function clampNumber(v, dflt, min, max) {
@@ -75293,6 +75430,9 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
75293
75430
  let totalBreadthContinuationGranted = 0;
75294
75431
  let progressMarkAtLastBreadthContinuation = null;
75295
75432
  let lastPostSubmitConfirmationToken = null;
75433
+ let keyboardRecoveryScreenId = null;
75434
+ let keyboardRecoverySameScreenTurns = 0;
75435
+ let lastKeyboardRecoveryIteration = -Infinity;
75296
75436
  const maybeGrantBreadthContinuation = (iteration, breadthCoverage) => {
75297
75437
  if (mode !== "survey" || breadthCoverage.complete || iteration < Math.floor(maxIterations * 0.8))
75298
75438
  return 0;
@@ -75363,6 +75503,29 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
75363
75503
  log2(` Post-submit confirmation snapshot failed non-fatally: ${error2 instanceof Error ? error2.message : String(error2)}`);
75364
75504
  }
75365
75505
  }
75506
+ {
75507
+ const rawScreenId = state2.getCurrentScreenId();
75508
+ const webIdx = rawScreenId ? rawScreenId.indexOf(".web-") : -1;
75509
+ const hashIdx = rawScreenId ? rawScreenId.indexOf("#") : -1;
75510
+ const currentScreenId = rawScreenId && webIdx >= 0 ? `${rawScreenId.slice(0, hashIdx >= 0 && hashIdx < webIdx ? hashIdx : webIdx)}${rawScreenId.slice(webIdx)}` : rawScreenId;
75511
+ if (currentScreenId && currentScreenId === keyboardRecoveryScreenId) {
75512
+ keyboardRecoverySameScreenTurns += 1;
75513
+ } else {
75514
+ keyboardRecoveryScreenId = currentScreenId ?? null;
75515
+ keyboardRecoverySameScreenTurns = 0;
75516
+ }
75517
+ const keyboardLikelyOpen = (0, mobile_keyboard_js_1.hasFocusedEditableElement)(getLatest()?.screen.elements ?? []);
75518
+ if (keyboardRecoverySameScreenTurns >= 4 && keyboardLikelyOpen && iteration - lastKeyboardRecoveryIteration > 4) {
75519
+ lastKeyboardRecoveryIteration = iteration;
75520
+ try {
75521
+ await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
75522
+ log2(" Auto-dismissed the on-screen keyboard (several turns stalled on this screen with a focused input).");
75523
+ transientDirectives.push("HARNESS NOTE: the on-screen keyboard was just auto-dismissed because multiple turns stalled on this screen \u2014 controls it occluded (e.g. the form submit button) may now be visible. Call mobile_snapshot, then TAP the commit/submit control instead of re-filling fields.");
75524
+ } catch (error2) {
75525
+ (0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(error2);
75526
+ }
75527
+ }
75528
+ }
75366
75529
  if (iteration > 6 && state2.isStaleProgress()) {
75367
75530
  transientDirectives.push("You have made no screen-graph progress recently. If the accessible content is covered, call finish_exploration. Otherwise swipe to reveal off-screen content or tap a not-yet-explored element.");
75368
75531
  }
@@ -79945,10 +80108,34 @@ ${constraints.join("\n")}`);
79945
80108
  const via = triggerLabel ? ` via ${JSON.stringify(triggerLabel)}` : "";
79946
80109
  group.flows?.push(`${nameById.get(transition.fromScreenId) ?? transition.fromScreenId} \u2192 ${nameById.get(transition.toScreenId) ?? transition.toScreenId} (${transition.action}${via})`);
79947
80110
  }
79948
- return [...groups.values()].map(({ realm: _realm, ...group }) => ({
79949
- ...group,
79950
- flows: [...new Set(group.flows ?? [])]
79951
- }));
80111
+ return [...groups.values()].map(({ realm, ...group }) => {
80112
+ const seenPageKeys = /* @__PURE__ */ new Set();
80113
+ const routes = [];
80114
+ const pages = [];
80115
+ group.routes.forEach((route2, index) => {
80116
+ const webIdx = route2.indexOf(".web-");
80117
+ const hashIdx = route2.indexOf("#");
80118
+ const key = webIdx >= 0 ? `${route2.slice(0, hashIdx >= 0 && hashIdx < webIdx ? hashIdx : webIdx)}${route2.slice(webIdx)}` : route2;
80119
+ if (seenPageKeys.has(key))
80120
+ return;
80121
+ seenPageKeys.add(key);
80122
+ routes.push(route2);
80123
+ const page = group.pages?.[index];
80124
+ if (page)
80125
+ pages.push(page);
80126
+ });
80127
+ const flows = [...new Set(group.flows ?? [])];
80128
+ const screenNames = [...new Set(routes.map((route2) => nameById.get(route2) ?? route2))];
80129
+ const preview = screenNames.slice(0, 3).join(", ");
80130
+ const description = `${realm === "authenticated" ? "Authenticated" : "Public"} mobile area "${group.name}" \u2014 ${routes.length} screen(s)${preview ? ` (${preview}${screenNames.length > 3 ? ", \u2026" : ""})` : ""}${flows.length > 0 ? `, ${flows.length} observed transition(s)` : ""}.`;
80131
+ return {
80132
+ ...group,
80133
+ routes,
80134
+ pages,
80135
+ description,
80136
+ flows
80137
+ };
80138
+ });
79952
80139
  }
79953
80140
  function normalizeMobileSurveyFeatureGroups(groups, graph, appId, existingFeatureNames = []) {
79954
80141
  const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration || a.screenId.localeCompare(b.screenId));
@@ -82130,6 +82317,9 @@ ${statePacket}`, redactionCredentials) },
82130
82317
  function weakAssertionTargetReason(element) {
82131
82318
  if (isAlwaysPresentContainer(element))
82132
82319
  return "always-present app/system container";
82320
+ const hasIdentitySignal = Boolean(nonEmpty3(element.accessibilityId) || nonEmpty3(element.resourceId) || nonEmpty3(element.text) || nonEmpty3(element.label));
82321
+ if (!hasIdentitySignal)
82322
+ return "no identity locator (accessibility id / resource id / text) for the assert gate";
82133
82323
  const structuralIdentity = [
82134
82324
  element.accessibilityId,
82135
82325
  element.resourceId,
@@ -83632,6 +83822,94 @@ For a fix, return the COMPLETE steps array (not a diff).`;
83632
83822
  function detectInvalidMobileAuthSetup(steps, error2) {
83633
83823
  return detectInvalidExternalAuthSetup(steps, error2) ?? detectInvalidAuthValidationSetup(steps, error2);
83634
83824
  }
83825
+ var IDENTITY_GATE_ERROR_RE = /No identity locator \(accessibility id \/ resource id \/ text\) available for this step/;
83826
+ function isIdentityEqualityXpath(value) {
83827
+ return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
83828
+ }
83829
+ function stepLacksIdentityAnchor(step) {
83830
+ const metadata = step.metadata;
83831
+ if (metadata && (metadata.appContext === "WEBVIEW" || typeof metadata.webCss === "string"))
83832
+ return false;
83833
+ const target = step.target;
83834
+ if (!target)
83835
+ return true;
83836
+ const bundle = target.locatorBundle;
83837
+ if (bundle?.accessibilityId || bundle?.resourceId)
83838
+ return false;
83839
+ if (bundle?.text?.trim() || bundle?.contentDesc?.trim())
83840
+ return false;
83841
+ if (bundle?.xpath && isIdentityEqualityXpath(bundle.xpath))
83842
+ return false;
83843
+ for (const raw of [target.primary, ...target.fallbacks ?? []]) {
83844
+ if (typeof raw !== "string")
83845
+ continue;
83846
+ const value = raw.trim();
83847
+ if (!value)
83848
+ continue;
83849
+ if (value.startsWith("css="))
83850
+ continue;
83851
+ if (value.startsWith("~") && value.length > 1)
83852
+ return false;
83853
+ if (value.startsWith("id=") && value.length > 3)
83854
+ return false;
83855
+ if (value.startsWith("//") || value.startsWith("(")) {
83856
+ if (isIdentityEqualityXpath(value))
83857
+ return false;
83858
+ continue;
83859
+ }
83860
+ if (value.startsWith("/"))
83861
+ continue;
83862
+ return false;
83863
+ }
83864
+ return true;
83865
+ }
83866
+ async function tryIdentityGateStaticRepair(steps, target, ctx, options) {
83867
+ if (!ctx.error || !IDENTITY_GATE_ERROR_RE.test(ctx.error))
83868
+ return null;
83869
+ const identityless = steps.filter(stepLacksIdentityAnchor);
83870
+ if (identityless.length === 0)
83871
+ return null;
83872
+ const isAssert = (step) => /^assert/i.test(step.action);
83873
+ if (!identityless.every(isAssert)) {
83874
+ return {
83875
+ passed: false,
83876
+ verdict: "unhealable_dynamic",
83877
+ healReason: "identity gate: an action step carries no identity locator (accessibility id / resource id / text); the recording lacks the anchors replay requires \u2014 re-record the step"
83878
+ };
83879
+ }
83880
+ const pruned = steps.filter((step) => !stepLacksIdentityAnchor(step));
83881
+ if (pruned.length === 0 || !pruned.some(isAssert) || !options.runNativeTest) {
83882
+ return {
83883
+ passed: false,
83884
+ verdict: "unhealable_dynamic",
83885
+ healReason: "identity gate: the only assertion(s) carry no identity locator, so the test cannot prove its outcome as recorded \u2014 re-record with an identifiable anchor"
83886
+ };
83887
+ }
83888
+ const reindexed = pruned.map((step, order) => ({ ...step, order }));
83889
+ try {
83890
+ const verify = await options.runNativeTest({ steps: reindexed, target, testVariables: ctx.testVariables });
83891
+ if (!verify.passed)
83892
+ return null;
83893
+ let healedAppiumCode;
83894
+ if (ctx.testName) {
83895
+ try {
83896
+ healedAppiumCode = (0, shared_1.buildAppiumCode)(ctx.testName, toDeterministicTarget(target), reindexed);
83897
+ } catch {
83898
+ }
83899
+ }
83900
+ return {
83901
+ passed: true,
83902
+ verdict: "fix",
83903
+ healedSteps: reindexed,
83904
+ ...healedAppiumCode ? { healedAppiumCode } : {},
83905
+ healReason: `identity gate: dropped ${identityless.length} assert step(s) recorded without any identity locator; pruned test verified green in one replay`,
83906
+ stepResults: verify.stepResults,
83907
+ logs: verify.logs
83908
+ };
83909
+ } catch {
83910
+ return null;
83911
+ }
83912
+ }
83635
83913
  function stripCodeFences(text) {
83636
83914
  const trimmed = text.trim();
83637
83915
  const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/m.exec(trimmed);
@@ -83972,7 +84250,17 @@ ${args.directive}`);
83972
84250
  break;
83973
84251
  }
83974
84252
  }
84253
+ if (options.deadlineAt && Date.now() >= options.deadlineAt) {
84254
+ return {
84255
+ passed: false,
84256
+ verdict: "config_issue",
84257
+ healReason: `mobile heal deadline exceeded during Phase 1 (after ${attempt} attempt(s)) \u2014 aborting before further model turns`,
84258
+ attemptsUsed: attempt,
84259
+ lastError
84260
+ };
84261
+ }
83975
84262
  attempt += 1;
84263
+ options.onProgress?.(`Phase 1 attempt ${attempt}/${attemptCap} \u2014 asking the model for a script fix...`);
83976
84264
  const userContent = buildUserContent({
83977
84265
  attempt,
83978
84266
  baseSteps,
@@ -83999,6 +84287,7 @@ ${args.directive}`);
83999
84287
  }), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true });
84000
84288
  content = response?.choices?.[0]?.message?.content ?? "";
84001
84289
  } catch {
84290
+ options.onProgress?.(`Phase 1 attempt ${attempt}: model call failed in transport \u2014 granting a recovery turn`);
84002
84291
  markRecoverableTurn();
84003
84292
  pendingDirective = "The previous attempt did not reach the model. Re-analyze and return one valid JSON verdict.";
84004
84293
  continue;
@@ -84173,6 +84462,9 @@ ${args.directive}`);
84173
84462
  const preflight = await tryRegistryPreflight(steps, target, ctx, options);
84174
84463
  if (preflight)
84175
84464
  return preflight;
84465
+ const identityRepair = await tryIdentityGateStaticRepair(steps, target, ctx, options);
84466
+ if (identityRepair)
84467
+ return identityRepair;
84176
84468
  const script = await executeMobileScriptDrivenHeal(steps, target, ctx, options);
84177
84469
  if (script.providerUnavailable) {
84178
84470
  const fallback = await tryDeterministicProviderFallback({ steps, target, ctx, options });
@@ -84346,6 +84638,14 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
84346
84638
  let bestStepResults;
84347
84639
  let aiResponseCount = 0;
84348
84640
  for (let pass = 1; pass <= maxPasses; pass += 1) {
84641
+ if (options.deadlineAt && Date.now() >= options.deadlineAt) {
84642
+ return {
84643
+ passed: false,
84644
+ verdict: "config_issue",
84645
+ healReason: `mobile heal deadline exceeded during live (Phase 2) healing at pass ${pass}/${maxPasses}`
84646
+ };
84647
+ }
84648
+ options.onProgress?.(`Phase 2 pass ${pass}/${maxPasses} \u2014 observing the live screen...`);
84349
84649
  let snap;
84350
84650
  try {
84351
84651
  snap = await liveDriver.snapshot();
@@ -85549,12 +85849,14 @@ var require_run_callback_proxy = __commonJS({
85549
85849
  }
85550
85850
  const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
85551
85851
  const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
85852
+ const localPaths = new Map(Object.entries(input.localRoutes ?? {}).map(([suffix, handler]) => [`${runPath}/${suffix}`, handler]));
85552
85853
  const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
85553
85854
  const authHeader = `Bearer ${token}`;
85554
85855
  const server3 = (0, node_http_1.createServer)(async (req, res) => {
85555
85856
  try {
85556
85857
  const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
85557
- if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) || requestUrl.search) {
85858
+ const localHandler = localPaths.get(requestUrl.pathname);
85859
+ if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) && !localHandler || requestUrl.search) {
85558
85860
  sendJson(res, 404, { error: "Callback route not available" });
85559
85861
  return;
85560
85862
  }
@@ -85573,6 +85875,11 @@ var require_run_callback_proxy = __commonJS({
85573
85875
  sendJson(res, 400, { error: "Callback body must be a JSON object" });
85574
85876
  return;
85575
85877
  }
85878
+ if (localHandler) {
85879
+ const out = await localHandler(parsedBody);
85880
+ sendJson(res, out.status, out.body);
85881
+ return;
85882
+ }
85576
85883
  const upstreamBody = JSON.stringify({
85577
85884
  ...parsedBody,
85578
85885
  ...input.taskArn ? { taskArn: input.taskArn } : {},
@@ -85631,6 +85938,668 @@ var require_run_callback_proxy = __commonJS({
85631
85938
  }
85632
85939
  });
85633
85940
 
85941
+ // ../runner-core/dist/services/discovery-lint-flags.js
85942
+ var require_discovery_lint_flags = __commonJS({
85943
+ "../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
85944
+ "use strict";
85945
+ Object.defineProperty(exports2, "__esModule", { value: true });
85946
+ exports2.structuralLocatorLintMode = structuralLocatorLintMode;
85947
+ exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
85948
+ exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
85949
+ exports2.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
85950
+ function strictOn(value) {
85951
+ return value === "true" || value === "1";
85952
+ }
85953
+ function structuralLocatorLintMode() {
85954
+ const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
85955
+ if (v === "warn")
85956
+ return "warn";
85957
+ if (v === "true" || v === "1" || v === "enforce")
85958
+ return "enforce";
85959
+ return "off";
85960
+ }
85961
+ function isPwMethodPreflightEnabled() {
85962
+ return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
85963
+ }
85964
+ function isVqaLoginDedupeEnabled() {
85965
+ return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
85966
+ }
85967
+ function isTestgenEmailVerifyEnabled() {
85968
+ return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
85969
+ }
85970
+ }
85971
+ });
85972
+
85973
+ // ../runner-core/dist/services/auth-recipe.js
85974
+ var require_auth_recipe = __commonJS({
85975
+ "../runner-core/dist/services/auth-recipe.js"(exports2) {
85976
+ "use strict";
85977
+ Object.defineProperty(exports2, "__esModule", { value: true });
85978
+ exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = void 0;
85979
+ exports2.computeRecipeHash = computeRecipeHash;
85980
+ exports2.buildAuthRecipe = buildAuthRecipe;
85981
+ exports2.authProofLocator = authProofLocator;
85982
+ exports2.urlProofPredicateExpr = urlProofPredicateExpr;
85983
+ exports2.renderAuthProofAssertion = renderAuthProofAssertion;
85984
+ exports2.renderProofBaselineCheck = renderProofBaselineCheck;
85985
+ exports2.renderAuthFn = renderAuthFn;
85986
+ exports2.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
85987
+ exports2.absoluteRoute = absoluteRoute;
85988
+ exports2.renderReplayClickArrow = renderReplayClickArrow;
85989
+ exports2.renderReplaySettleArrow = renderReplaySettleArrow;
85990
+ exports2.renderAuthUrlWaitLine = renderAuthUrlWaitLine;
85991
+ exports2.commandToReplayStep = commandToReplayStep;
85992
+ exports2.buildReplayPlan = buildReplayPlan;
85993
+ exports2.extractAuthRegion = extractAuthRegion;
85994
+ exports2.stripAuthRegions = stripAuthRegions;
85995
+ exports2.ensureExpectImported = ensureExpectImported;
85996
+ exports2.insertAfterPwImport = insertAfterPwImport;
85997
+ exports2.ensureLoginCall = ensureLoginCall;
85998
+ exports2.upsertAuthBlock = upsertAuthBlock;
85999
+ exports2.buildHealAuthStamp = buildHealAuthStamp;
86000
+ var crypto_1 = require("crypto");
86001
+ var shared_1 = (init_dist(), __toCommonJS(dist_exports));
86002
+ var discovery_lint_flags_js_1 = require_discovery_lint_flags();
86003
+ exports2.AUTH_MARKER_VERSION = "v3";
86004
+ var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
86005
+ var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
86006
+ var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
86007
+ var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
86008
+ function computeRecipeHash(input) {
86009
+ const canonical = JSON.stringify({
86010
+ preNavRoute: input.preNavRoute,
86011
+ preNavCmds: input.preNavCmds,
86012
+ route: input.route,
86013
+ cmds: input.cmds,
86014
+ redirectRoute: input.redirectRoute,
86015
+ authProof: input.authProof
86016
+ });
86017
+ return (0, crypto_1.createHash)("sha256").update(canonical).digest("hex").slice(0, 12);
86018
+ }
86019
+ function buildAuthRecipe(login, authProof, verifiedAt) {
86020
+ const preNavRoute = login.preNavRoute ?? "/";
86021
+ const preNavCmds = login.preNavCmds ?? [];
86022
+ const recipeHash = computeRecipeHash({
86023
+ preNavRoute,
86024
+ preNavCmds,
86025
+ route: login.route,
86026
+ cmds: login.cmds,
86027
+ redirectRoute: login.redirectRoute,
86028
+ authProof
86029
+ });
86030
+ return {
86031
+ version: 1,
86032
+ recipeHash,
86033
+ preNavRoute,
86034
+ preNavCmds,
86035
+ route: login.route,
86036
+ cmds: login.cmds,
86037
+ redirectRoute: login.redirectRoute,
86038
+ authProof,
86039
+ replayPlan: login.replayPlan ?? [],
86040
+ verifiedAt
86041
+ };
86042
+ }
86043
+ function authProofLocator(proof) {
86044
+ if (proof.kind === "url" || proof.kind === "endpoint") {
86045
+ throw new Error(`authProofLocator: ${proof.kind} proof has no element locator; callers must branch on proof.kind`);
86046
+ }
86047
+ if (proof.kind === "testid") {
86048
+ return `page.getByTestId(${JSON.stringify(proof.testId)})`;
86049
+ }
86050
+ if (proof.name) {
86051
+ return `page.getByRole(${JSON.stringify(proof.role)}, { name: ${JSON.stringify(proof.name)} })`;
86052
+ }
86053
+ return `page.getByRole(${JSON.stringify(proof.role)}).first()`;
86054
+ }
86055
+ function stripToPathname(route2) {
86056
+ return route2.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/";
86057
+ }
86058
+ function urlProofPredicateExpr(route2, exact = false) {
86059
+ const p = stripToPathname(route2);
86060
+ if (p === "/")
86061
+ return `() => false`;
86062
+ const r = JSON.stringify(p);
86063
+ if (exact)
86064
+ return `(u) => u.pathname === ${r} || u.pathname === ${r} + '/'`;
86065
+ return `(u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/')`;
86066
+ }
86067
+ function renderStableExactUrlProof(pred, tabs) {
86068
+ return [
86069
+ `await page.waitForURL(${pred}, { timeout: 15000 });`,
86070
+ `if (await page.waitForURL((u) => !(${pred})(u), { timeout: 1500 }).then(() => true, () => false)) throw new Error('__vqa auth-proof: authenticated route was transient (rebounded to a logged-out route)');`,
86071
+ `if (await page.locator('input[type="password"]').first().isVisible().catch(() => false)) throw new Error('__vqa auth-proof: a login form is visible on the authenticated route \u2014 logged out');`
86072
+ ].join(`
86073
+ ${tabs}`);
86074
+ }
86075
+ var ENDPOINT_ORIGIN_EXPR = `(page.url().match(/^https?:\\/\\/[^/]+/) || [''])[0]`;
86076
+ function renderEndpointProbe(url, tabs, mode, originMode) {
86077
+ const u = JSON.stringify(url);
86078
+ const target = originMode === "pageurl" ? `${ENDPOINT_ORIGIN_EXPR} + ${u}` : u;
86079
+ const lines = [
86080
+ `const __vqaResp = await page.request.get(${target});`,
86081
+ `const __vqaJson = __vqaResp.ok() ? await __vqaResp.json().catch(() => null) : null;`,
86082
+ `const __vqaUser = __vqaJson && (__vqaJson.user ?? __vqaJson.data?.user ?? __vqaJson);`,
86083
+ `const __vqaAuthed = !!(__vqaUser && (__vqaUser.id || __vqaUser.email || __vqaUser.sub));`,
86084
+ mode === "authed" ? `if (!__vqaAuthed) throw new Error('__vqa auth-proof: identity endpoint shows no authenticated user');` : `if (__vqaAuthed) throw new Error('__vqa auth-baseline: identity endpoint shows an authenticated user while logged out');`
86085
+ ];
86086
+ return lines.join(`
86087
+ ${tabs}`);
86088
+ }
86089
+ function renderAuthProofAssertion(proof) {
86090
+ if (proof.kind === "endpoint") {
86091
+ return renderEndpointProbe(proof.url, " ", "authed", "baseurl");
86092
+ }
86093
+ if (proof.kind === "url") {
86094
+ const pred = urlProofPredicateExpr(proof.route, proof.exact);
86095
+ if (proof.exact)
86096
+ return renderStableExactUrlProof(pred, " ");
86097
+ return `await page.waitForURL(${pred}, { timeout: 15000 });`;
86098
+ }
86099
+ return `await expect(${authProofLocator(proof)}).toBeVisible({ timeout: 15000 });`;
86100
+ }
86101
+ var BASELINE_VISIBLE_TIMEOUT_MS = 15e3;
86102
+ function renderProofBaselineCheck(proof, tabs) {
86103
+ if (proof.kind === "endpoint") {
86104
+ return renderEndpointProbe(proof.url, tabs, "anon", "baseurl");
86105
+ }
86106
+ if (proof.kind === "url") {
86107
+ const pred = urlProofPredicateExpr(proof.route, proof.exact);
86108
+ return `if (await page.waitForURL(${pred}, { timeout: 2500 }).then(() => true, () => false)) throw new Error('__vqa auth-baseline: authed route reachable while logged out \u2014 not a discriminating proof');`;
86109
+ }
86110
+ const loc = authProofLocator(proof);
86111
+ return [
86112
+ `if (await ${loc}.waitFor({ state: 'visible', timeout: ${BASELINE_VISIBLE_TIMEOUT_MS} }).then(() => true, () => false)) {`,
86113
+ ` throw new Error('__vqa auth-baseline: authed-only proof visible while logged out \u2014 not a discriminating proof');`,
86114
+ `}`
86115
+ ].join(`
86116
+ ${tabs}`);
86117
+ }
86118
+ function normalizeCmd(cmd) {
86119
+ let c = cmd.trim();
86120
+ if (c.endsWith(";"))
86121
+ c = c.slice(0, -1);
86122
+ return ` ${c};`;
86123
+ }
86124
+ function renderAuthFn(recipe, opts) {
86125
+ const lines = [];
86126
+ lines.push(START_MARKER);
86127
+ lines.push(`async function __vqaLogin(page) {`);
86128
+ if (opts?.warmGuard) {
86129
+ const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
86130
+ const loginRoute = recipe.route ?? recipe.preNavRoute;
86131
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
86132
+ lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
86133
+ if (recipe.authProof.kind === "endpoint") {
86134
+ lines.push(` try {`);
86135
+ lines.push(` ${renderEndpointProbe(recipe.authProof.url, " ", "authed", "baseurl")}`);
86136
+ lines.push(` return;`);
86137
+ lines.push(` } catch {}`);
86138
+ } else if (recipe.authProof.kind === "url") {
86139
+ const pred = urlProofPredicateExpr(recipe.authProof.route, recipe.authProof.exact);
86140
+ lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
86141
+ lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
86142
+ } else if (loginRoute) {
86143
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
86144
+ } else {
86145
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
86146
+ }
86147
+ lines.push(` }`);
86148
+ }
86149
+ lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
86150
+ for (const cmd of recipe.preNavCmds)
86151
+ lines.push(normalizeCmd(cmd));
86152
+ if (recipe.preNavCmds.length === 0 && recipe.route && recipe.route !== recipe.preNavRoute) {
86153
+ lines.push(` await page.goto(${JSON.stringify(recipe.route)}, { waitUntil: 'domcontentloaded' });`);
86154
+ }
86155
+ for (const cmd of recipe.cmds)
86156
+ lines.push(normalizeCmd(cmd));
86157
+ lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
86158
+ lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
86159
+ if (opts?.warmGuard) {
86160
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
86161
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_CLAIM_EPOCH && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
86162
+ lines.push(` try {`);
86163
+ lines.push(` const __vqaState = await page.context().storageState();`);
86164
+ lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN, claimEpoch: Number(process.env.VALIDATE_CLAIM_EPOCH) }, timeout: 10000 });`);
86165
+ lines.push(` } catch {}`);
86166
+ lines.push(` }`);
86167
+ }
86168
+ lines.push(`}`);
86169
+ lines.push(END_MARKER);
86170
+ return lines.join("\n");
86171
+ }
86172
+ exports2.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
86173
+ "AUTH_FILE",
86174
+ "SHARED_AUTH_INJECT",
86175
+ "VALIDATE_RUN_ID",
86176
+ "VALIDATE_CLAIM_EPOCH",
86177
+ "VALIDATE_AUTH_CONFIG_HASH",
86178
+ "VALIDATE_TASK_ARN",
86179
+ "VALIDATE_RUNNER_AUTH",
86180
+ "VALIDATE_SERVER_URL",
86181
+ // Register-and-verify tests (VALIDATE_QA_INBOX region, inbox-stamp.ts): the
86182
+ // per-run minted registration address + its DERIVED throwaway-app password,
86183
+ // injected by the parent's test-inbox-broker — never project variables.
86184
+ "VQA_TEST_INBOX_EMAIL",
86185
+ "VQA_TEST_ACCOUNT_PASSWORD"
86186
+ ]);
86187
+ function isReservedRunnerEnvVar(key) {
86188
+ return exports2.RESERVED_RUNNER_ENV_VARS.has(key);
86189
+ }
86190
+ function absoluteRoute(route2, baseUrl) {
86191
+ try {
86192
+ return new URL(route2, baseUrl).href;
86193
+ } catch {
86194
+ return route2;
86195
+ }
86196
+ }
86197
+ function renderReplayClickArrow(step) {
86198
+ const loc = step.locator.trim();
86199
+ if (step.kind === "wait") {
86200
+ return `async (page) => {
86201
+ await ${loc}.waitFor({ state: 'visible', timeout: 15000 });
86202
+ }`;
86203
+ }
86204
+ if (step.kind === "press") {
86205
+ return `async (page) => {
86206
+ await ${loc}.press(${JSON.stringify(step.key ?? "Enter")});
86207
+ }`;
86208
+ }
86209
+ return `async (page) => {
86210
+ await ${loc}.click();
86211
+ }`;
86212
+ }
86213
+ function renderReplaySettleArrow(proof) {
86214
+ if (proof.kind === "endpoint") {
86215
+ return `async (page) => {
86216
+ ${renderEndpointProbe(proof.url, " ", "authed", "pageurl")}
86217
+ }`;
86218
+ }
86219
+ if (proof.kind === "url") {
86220
+ const pred = urlProofPredicateExpr(proof.route, proof.exact);
86221
+ const body = proof.exact ? renderStableExactUrlProof(pred, " ") : `await page.waitForURL(${pred}, { timeout: 15000 });`;
86222
+ return `async (page) => {
86223
+ ${body}
86224
+ }`;
86225
+ }
86226
+ return `async (page) => {
86227
+ await ${authProofLocator(proof)}.waitFor({ state: 'visible', timeout: 15000 });
86228
+ }`;
86229
+ }
86230
+ function renderAuthUrlWaitLine(redirectRoute) {
86231
+ const opts = `{ waitUntil: 'domcontentloaded', timeout: 8000 }`;
86232
+ const tail = `.catch(() => { /* URL drift \u2014 the auth-proof assertion below is the gate */ });`;
86233
+ const rr = redirectRoute ? stripToPathname(redirectRoute) : null;
86234
+ if (rr && rr !== "/") {
86235
+ const r = JSON.stringify(rr);
86236
+ return ` await page.waitForURL((u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/'), ${opts})${tail}`;
86237
+ }
86238
+ if (rr === "/") {
86239
+ return ` await page.waitForURL((u) => !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
86240
+ }
86241
+ return ` await page.waitForURL((u) => u.pathname !== '/' && !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
86242
+ }
86243
+ function locatorChainOf(cmd) {
86244
+ const m = cmd.match(/\bpage\.[\s\S]*?(?=\.(?:fill|click|check|press|selectOption|setChecked|waitFor)\s*\()/);
86245
+ return m ? m[0].trim() : null;
86246
+ }
86247
+ function parseRoleName(locator) {
86248
+ const role = locator.match(/getByRole\(\s*['"`]([^'"`]+)['"`]/)?.[1];
86249
+ const name = locator.match(/getByRole\([^)]*name:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByLabel\(\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByPlaceholder\(\s*['"`]([^'"`]+)['"`]/)?.[1];
86250
+ return { role, name };
86251
+ }
86252
+ function credentialKeyOf(cmd) {
86253
+ return cmd.match(/\.fill\(\s*process\.env\.([A-Za-z_$][\w$]*)!?\s*\)/)?.[1] ?? null;
86254
+ }
86255
+ function commandToReplayStep(cmd) {
86256
+ const chain = locatorChainOf(cmd);
86257
+ if (!chain)
86258
+ return null;
86259
+ const credKey = credentialKeyOf(cmd);
86260
+ if (cmd.includes(".fill(")) {
86261
+ if (!credKey)
86262
+ return null;
86263
+ const { role, name } = parseRoleName(chain);
86264
+ return { kind: "fill", credentialKey: credKey, locator: chain, role, name, element: name ?? role ?? "credential field" };
86265
+ }
86266
+ if (/\.waitFor\s*\(/.test(cmd))
86267
+ return { kind: "wait", locator: chain };
86268
+ const pressKey = cmd.match(/\.press\(\s*['"`]([^'"`]+)['"`]\s*\)/)?.[1];
86269
+ if (pressKey)
86270
+ return { kind: "press", locator: chain, key: pressKey };
86271
+ if (/\.(?:click|check|setChecked|selectOption)\s*\(/.test(cmd))
86272
+ return { kind: "click", locator: chain };
86273
+ return null;
86274
+ }
86275
+ function buildReplayPlan(login) {
86276
+ const plan = [];
86277
+ const preNavRoute = login.preNavRoute ?? "/";
86278
+ plan.push({ kind: "nav", route: preNavRoute });
86279
+ for (const c of login.preNavCmds ?? []) {
86280
+ const step = commandToReplayStep(c);
86281
+ if (step)
86282
+ plan.push(step);
86283
+ }
86284
+ if ((login.preNavCmds ?? []).length === 0 && login.route && login.route !== preNavRoute) {
86285
+ plan.push({ kind: "nav", route: login.route });
86286
+ }
86287
+ for (const c of login.cmds) {
86288
+ const step = commandToReplayStep(c);
86289
+ if (step)
86290
+ plan.push(step);
86291
+ }
86292
+ return plan;
86293
+ }
86294
+ function extractAuthRegion(code) {
86295
+ const m = code.match(new RegExp(AUTH_REGION_RE.source));
86296
+ return m ? m[0] : null;
86297
+ }
86298
+ function countAuthRegions(code) {
86299
+ const m = code.match(AUTH_REGION_RE);
86300
+ return m ? m.length : 0;
86301
+ }
86302
+ function stripAuthRegions(code) {
86303
+ return code.replace(AUTH_REGION_RE, "");
86304
+ }
86305
+ var PW_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?/;
86306
+ function ensureExpectImported(code) {
86307
+ const m = code.match(PW_IMPORT_RE);
86308
+ if (!m)
86309
+ return code;
86310
+ const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
86311
+ if (names.includes("expect"))
86312
+ return code;
86313
+ const next = [...names, "expect"];
86314
+ return code.replace(PW_IMPORT_RE, `import { ${next.join(", ")} } from '@playwright/test';`);
86315
+ }
86316
+ function insertAfterPwImport(code, block) {
86317
+ const m = code.match(PW_IMPORT_RE);
86318
+ if (!m || m.index === void 0) {
86319
+ return `${block}
86320
+
86321
+ ${code.replace(/^\r?\n+/, "")}`;
86322
+ }
86323
+ const end = m.index + m[0].length;
86324
+ const head = code.slice(0, end);
86325
+ const tail = code.slice(end).replace(/^\r?\n+/, "");
86326
+ return `${head}
86327
+
86328
+ ${block}
86329
+
86330
+ ${tail}`;
86331
+ }
86332
+ var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
86333
+ var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
86334
+ function ensureLoginCall(code) {
86335
+ if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
86336
+ return code;
86337
+ let result = code;
86338
+ if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
86339
+ result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
86340
+ }
86341
+ const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
86342
+ for (let i = matches.length - 1; i >= 0; i--) {
86343
+ const m = matches[i];
86344
+ if (m.index === void 0)
86345
+ continue;
86346
+ const insertAt = m.index + m[0].length;
86347
+ const following = result.slice(insertAt, insertAt + 60);
86348
+ if (/^\s*await\s+__vqaLogin\s*\(\s*page\s*\)/.test(following))
86349
+ continue;
86350
+ result = `${result.slice(0, insertAt)}
86351
+ await __vqaLogin(page);${result.slice(insertAt)}`;
86352
+ }
86353
+ return result;
86354
+ }
86355
+ function upsertAuthBlock(code, ctx) {
86356
+ if (ctx.mode === "generate") {
86357
+ if (!ctx.recipe || !ctx.isAuthNeeded)
86358
+ return code;
86359
+ let result = stripAuthRegions(code);
86360
+ result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
86361
+ result = ensureExpectImported(result);
86362
+ const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
86363
+ result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
86364
+ result = ensureLoginCall(result);
86365
+ const n = countAuthRegions(result);
86366
+ if (n !== 1) {
86367
+ throw new Error(`upsertAuthBlock(generate): expected exactly 1 auth region after stamp, got ${n}`);
86368
+ }
86369
+ if (!LOGIN_CALL_RE.test(result)) {
86370
+ throw new Error("upsertAuthBlock(generate): stamped __vqaLogin but found no test() opener to inject the call");
86371
+ }
86372
+ return result;
86373
+ }
86374
+ try {
86375
+ if (!ctx.preservedAuthRegion)
86376
+ return code;
86377
+ let result = ensureExpectImported(code);
86378
+ if (countAuthRegions(result) >= 1) {
86379
+ result = result.replace(AUTH_REGION_RE, "");
86380
+ result = insertAfterPwImport(result, ctx.preservedAuthRegion);
86381
+ } else {
86382
+ result = insertAfterPwImport(result, ctx.preservedAuthRegion);
86383
+ }
86384
+ result = ensureLoginCall(result);
86385
+ if (countAuthRegions(result) !== 1) {
86386
+ throw new Error("heal re-stamp produced non-unique auth region");
86387
+ }
86388
+ return result;
86389
+ } catch (err) {
86390
+ const reason = err instanceof Error ? err.message : String(err);
86391
+ ctx.onSignal?.({ type: "auth-stamp-degraded", reason });
86392
+ return code;
86393
+ }
86394
+ }
86395
+ function buildHealAuthStamp(originalCode, failureError, logs2) {
86396
+ const region = extractAuthRegion(originalCode);
86397
+ const loginFailed = !!failureError && /__vqaLogin/.test(failureError);
86398
+ return {
86399
+ mode: "heal",
86400
+ preservedAuthRegion: region && !loginFailed ? region : null,
86401
+ onSignal: logs2 ? (s) => logs2.push(`\u26A0 auth-stamp ${s.type}: ${s.reason}`) : void 0
86402
+ };
86403
+ }
86404
+ }
86405
+ });
86406
+
86407
+ // ../runner-core/dist/services/inbox-stamp.js
86408
+ var require_inbox_stamp = __commonJS({
86409
+ "../runner-core/dist/services/inbox-stamp.js"(exports2) {
86410
+ "use strict";
86411
+ Object.defineProperty(exports2, "__esModule", { value: true });
86412
+ exports2.INBOX_HELPER_TOKEN_RE = exports2.INBOX_HELPER_NAME = exports2.INBOX_REGION_RE = exports2.INBOX_MARKER_VERSION = void 0;
86413
+ exports2.codeUsesInboxHelper = codeUsesInboxHelper;
86414
+ exports2.stripInboxRegions = stripInboxRegions;
86415
+ exports2.renderInboxFn = renderInboxFn;
86416
+ exports2.upsertInboxBlock = upsertInboxBlock;
86417
+ var auth_recipe_js_1 = require_auth_recipe();
86418
+ exports2.INBOX_MARKER_VERSION = "v1";
86419
+ var START_MARKER = `// >>> VALIDATE_QA_INBOX ${exports2.INBOX_MARKER_VERSION} \u2014 generated, do not edit >>>`;
86420
+ var END_MARKER = `// <<< VALIDATE_QA_INBOX ${exports2.INBOX_MARKER_VERSION} <<<`;
86421
+ exports2.INBOX_REGION_RE = /\/\/ >>> VALIDATE_QA_INBOX[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_INBOX[^\n]*\r?\n?/g;
86422
+ exports2.INBOX_HELPER_NAME = "__vqaWaitForEmail";
86423
+ exports2.INBOX_HELPER_TOKEN_RE = /__vqaWaitForEmail\s*\(/;
86424
+ function codeUsesInboxHelper(code) {
86425
+ if (!code)
86426
+ return false;
86427
+ return exports2.INBOX_HELPER_TOKEN_RE.test(code) || /VALIDATE_QA_INBOX/.test(code);
86428
+ }
86429
+ function stripInboxRegions(code) {
86430
+ return code.replace(exports2.INBOX_REGION_RE, "");
86431
+ }
86432
+ function countInboxRegions(code) {
86433
+ const m = code.match(exports2.INBOX_REGION_RE);
86434
+ return m ? m.length : 0;
86435
+ }
86436
+ function renderInboxFn() {
86437
+ const lines = [
86438
+ START_MARKER,
86439
+ `// Reads THIS run's verification email via the runner's inbox broker. The`,
86440
+ `// IMAP credential stays in the runner parent; this helper can only see mail`,
86441
+ `// addressed to the run's own registration address (VQA_TEST_INBOX_EMAIL).`,
86442
+ `// kind: 'code' \u2192 the one-time code, 'link' \u2192 the verification URL,`,
86443
+ `// 'either' / omitted \u2192 { code, link, subject, from } for flexible flows.`,
86444
+ `async function __vqaWaitForEmail(kind, opts) {`,
86445
+ ` const want = kind === 'code' || kind === 'link' ? kind : 'either';`,
86446
+ ` if (!process.env.VQA_TEST_INBOX_EMAIL || !process.env.VALIDATE_SERVER_URL || !process.env.VALIDATE_RUN_ID) {`,
86447
+ ` throw new Error('CONFIG_ISSUE: email verification unavailable - this runner has no verification inbox, so a register-and-verify test cannot run here');`,
86448
+ ` }`,
86449
+ ` const timeoutMs = Math.min(Math.max((opts && opts.timeoutMs) || 150000, 5000), 170000);`,
86450
+ ` const res = await fetch(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + encodeURIComponent(process.env.VALIDATE_RUN_ID) + '/inbox-wait', {`,
86451
+ ` method: 'POST',`,
86452
+ ` headers: { authorization: process.env.VALIDATE_RUNNER_AUTH || '', 'content-type': 'application/json' },`,
86453
+ ` body: JSON.stringify({ kind: want, timeoutMs: timeoutMs }),`,
86454
+ ` });`,
86455
+ ` if (res.status === 404) {`,
86456
+ ` throw new Error('CONFIG_ISSUE: email verification unavailable - the runner inbox broker is not active for this run');`,
86457
+ ` }`,
86458
+ ` let data = null;`,
86459
+ ` try { data = await res.json(); } catch (e) { data = null; }`,
86460
+ ` if (!res.ok || !data) { throw new Error('inbox broker request failed (status ' + res.status + ')'); }`,
86461
+ ` if (!data.found) {`,
86462
+ ` if (data.error) { throw new Error('CONFIG_ISSUE: verification inbox failed on this runner - ' + data.error); }`,
86463
+ ` throw new Error('VERIFICATION_EMAIL_NOT_RECEIVED: no verification email arrived for this test account within ' + Math.round(timeoutMs / 1000) + 's - the app may not have sent it');`,
86464
+ ` }`,
86465
+ ` if (want === 'code' && !data.code) {`,
86466
+ ` throw new Error('VERIFICATION_EMAIL_HAS_NO_CODE: an email arrived (subject: ' + JSON.stringify(data.subject || '') + ') but no one-time code could be extracted');`,
86467
+ ` }`,
86468
+ ` if (want === 'link' && !data.link) {`,
86469
+ ` throw new Error('VERIFICATION_EMAIL_HAS_NO_LINK: an email arrived (subject: ' + JSON.stringify(data.subject || '') + ') but no verification link could be extracted');`,
86470
+ ` }`,
86471
+ ` return want === 'code' ? data.code : want === 'link' ? data.link : data;`,
86472
+ `}`,
86473
+ END_MARKER
86474
+ ];
86475
+ return lines.join("\n");
86476
+ }
86477
+ function upsertInboxBlock(code) {
86478
+ const stripped = stripInboxRegions(code);
86479
+ if (!exports2.INBOX_HELPER_TOKEN_RE.test(stripped))
86480
+ return stripped;
86481
+ const result = (0, auth_recipe_js_1.insertAfterPwImport)(stripped, renderInboxFn());
86482
+ const n = countInboxRegions(result);
86483
+ if (n !== 1) {
86484
+ throw new Error(`upsertInboxBlock: expected exactly 1 inbox region after stamp, got ${n}`);
86485
+ }
86486
+ return result;
86487
+ }
86488
+ }
86489
+ });
86490
+
86491
+ // ../runner-core/dist/services/test-inbox-broker.js
86492
+ var require_test_inbox_broker = __commonJS({
86493
+ "../runner-core/dist/services/test-inbox-broker.js"(exports2) {
86494
+ "use strict";
86495
+ Object.defineProperty(exports2, "__esModule", { value: true });
86496
+ exports2.INBOX_WAIT_ROUTE = exports2.TEST_INBOX_PASSWORD_ENV = exports2.TEST_INBOX_EMAIL_ENV = void 0;
86497
+ exports2.deriveTestRunInboxTag = deriveTestRunInboxTag;
86498
+ exports2.mintTestRunInboxIdentity = mintTestRunInboxIdentity;
86499
+ exports2.decideInboxWaitResponse = decideInboxWaitResponse;
86500
+ exports2.startTestInboxBroker = startTestInboxBroker;
86501
+ var node_crypto_1 = require("crypto");
86502
+ var verification_inbox_js_1 = require_verification_inbox();
86503
+ var inbox_tools_js_1 = require_inbox_tools();
86504
+ exports2.TEST_INBOX_EMAIL_ENV = "VQA_TEST_INBOX_EMAIL";
86505
+ exports2.TEST_INBOX_PASSWORD_ENV = "VQA_TEST_ACCOUNT_PASSWORD";
86506
+ exports2.INBOX_WAIT_ROUTE = "inbox-wait";
86507
+ var MIN_WAIT_MS = 5e3;
86508
+ var MAX_WAIT_MS = 17e4;
86509
+ var DEFAULT_WAIT_MS = 15e4;
86510
+ function deriveTestRunInboxTag(seed, projectId, runId) {
86511
+ const hostKey = (0, verification_inbox_js_1.seedToHostKey)(seed || "");
86512
+ const hash = (0, node_crypto_1.createHash)("sha256").update(`testrun|${hostKey}|${projectId ?? ""}|${runId}`).digest("hex").slice(0, 10);
86513
+ return `vqat${hash}`;
86514
+ }
86515
+ function mintTestRunInboxIdentity(config, input) {
86516
+ const tag = deriveTestRunInboxTag(input.baseUrl, input.projectId, input.runId);
86517
+ const email = (0, verification_inbox_js_1.applyPlusTag)(config.address, tag);
86518
+ return { email, password: (0, verification_inbox_js_1.deriveRegistrationPassword)(email), tag };
86519
+ }
86520
+ function decideInboxWaitResponse(msg, publicLink) {
86521
+ const code = msg && msg.code && msg.codeConfidence !== "fallback" ? msg.code : null;
86522
+ const link = publicLink;
86523
+ if (!msg || !code && !link)
86524
+ return { found: false };
86525
+ return { found: true, code, link, subject: msg.subject, from: msg.from };
86526
+ }
86527
+ async function startTestInboxBroker(opts) {
86528
+ const identity = mintTestRunInboxIdentity(opts.config, opts);
86529
+ const reader = new verification_inbox_js_1.VerificationInboxReader(opts.config);
86530
+ const sinceMs = Date.now();
86531
+ let sinceUid = 0;
86532
+ let sinceUidValidity = 0;
86533
+ let authDeadReason = null;
86534
+ try {
86535
+ const wm = await reader.getUidWatermark();
86536
+ sinceUid = wm.uid;
86537
+ sinceUidValidity = wm.uidValidity;
86538
+ } catch (error2) {
86539
+ if ((0, verification_inbox_js_1.isInboxAuthFailure)(error2)) {
86540
+ authDeadReason = error2 instanceof Error ? error2.message : String(error2);
86541
+ opts.log?.(`\u26A0 Inbox broker: IMAP authentication rejected at watermark probe \u2014 email waits will fail as CONFIG_ISSUE (${authDeadReason})`);
86542
+ } else {
86543
+ opts.log?.("\u26A0 Inbox broker: UID watermark unavailable, falling back to the time floor");
86544
+ }
86545
+ }
86546
+ opts.log?.(`\u{1F4EC} Inbox broker active for ${identity.email} (uid watermark ${sinceUid || "none"})`);
86547
+ let waiting = false;
86548
+ const handleWait = async (body) => {
86549
+ if (authDeadReason) {
86550
+ return { status: 200, body: { found: false, error: "inbox authentication failed (revoked/wrong app password)" } };
86551
+ }
86552
+ if (waiting) {
86553
+ return { status: 200, body: { found: false, error: "another verification wait is already in progress for this run" } };
86554
+ }
86555
+ waiting = true;
86556
+ const rawTimeout = typeof body.timeoutMs === "number" && Number.isFinite(body.timeoutMs) ? body.timeoutMs : DEFAULT_WAIT_MS;
86557
+ const timeoutMs = Math.min(Math.max(rawTimeout, MIN_WAIT_MS), MAX_WAIT_MS);
86558
+ const require2 = body.kind === "code" ? "code" : body.kind === "link" ? "link" : "signal";
86559
+ try {
86560
+ const stats = { filteredOut: 0 };
86561
+ const msg = await reader.waitForMessage({
86562
+ toAddress: identity.email,
86563
+ sinceMs,
86564
+ sinceUid,
86565
+ sinceUidValidity,
86566
+ maxWaitMs: timeoutMs,
86567
+ require: require2,
86568
+ stats
86569
+ });
86570
+ const publicLink = msg && msg.link && (0, inbox_tools_js_1.isPublicHttpUrl)(msg.link) ? msg.link : null;
86571
+ const decision = decideInboxWaitResponse(msg, publicLink);
86572
+ if (!decision.found) {
86573
+ opts.log?.(`\u{1F4ED} Inbox broker: no usable verification email for ${identity.tag} within ${Math.round(timeoutMs / 1e3)}s`);
86574
+ return { status: 200, body: { found: false } };
86575
+ }
86576
+ opts.log?.(`\u{1F4EC} Inbox broker: email for ${identity.tag} (code=${decision.code ? "yes" : "no"}, link=${decision.link ? "yes" : "no"})`);
86577
+ return { status: 200, body: { ...decision } };
86578
+ } catch (error2) {
86579
+ if ((0, verification_inbox_js_1.isInboxAuthFailure)(error2)) {
86580
+ authDeadReason = error2 instanceof Error ? error2.message : String(error2);
86581
+ return { status: 200, body: { found: false, error: "inbox authentication failed (revoked/wrong app password)" } };
86582
+ }
86583
+ const message = error2 instanceof Error ? error2.message : String(error2);
86584
+ opts.log?.(`\u26A0 Inbox broker wait failed: ${message}`);
86585
+ return { status: 200, body: { found: false, error: `inbox read failed: ${message}` } };
86586
+ } finally {
86587
+ waiting = false;
86588
+ }
86589
+ };
86590
+ return {
86591
+ identity,
86592
+ childEnv: () => ({
86593
+ [exports2.TEST_INBOX_EMAIL_ENV]: identity.email,
86594
+ [exports2.TEST_INBOX_PASSWORD_ENV]: identity.password
86595
+ }),
86596
+ handleWait,
86597
+ close: () => reader.close()
86598
+ };
86599
+ }
86600
+ }
86601
+ });
86602
+
85634
86603
  // ../runner-core/dist/services/playwright-native.js
85635
86604
  var require_playwright_native = __commonJS({
85636
86605
  "../runner-core/dist/services/playwright-native.js"(exports2) {
@@ -85688,6 +86657,9 @@ var require_playwright_native = __commonJS({
85688
86657
  var har_stream_js_1 = require_har_stream();
85689
86658
  var api_auth_headers_js_1 = require_api_auth_headers();
85690
86659
  var run_callback_proxy_js_1 = require_run_callback_proxy();
86660
+ var inbox_stamp_js_1 = require_inbox_stamp();
86661
+ var test_inbox_broker_js_1 = require_test_inbox_broker();
86662
+ var verification_inbox_js_1 = require_verification_inbox();
85691
86663
  var api_auth_headers_js_2 = require_api_auth_headers();
85692
86664
  Object.defineProperty(exports2, "parseApiAuthHeaders", { enumerable: true, get: function() {
85693
86665
  return api_auth_headers_js_2.parseApiAuthHeaders;
@@ -85722,17 +86694,20 @@ var require_playwright_native = __commonJS({
85722
86694
  let runDir = null;
85723
86695
  let standingNeedleMatcher = null;
85724
86696
  let callbackProxy;
86697
+ let inboxBroker;
85725
86698
  try {
85726
86699
  runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
85727
86700
  await (0, promises_1.mkdir)(runDir, { recursive: true });
85728
86701
  const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
86702
+ const usesInboxHelper = !isApiTest2 && (0, inbox_stamp_js_1.codeUsesInboxHelper)(playwrightCode);
85729
86703
  const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
85730
86704
  const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
85731
86705
  explicit: extraHttpHeaders,
85732
86706
  standingHeadersMode: effectiveStandingMode,
85733
86707
  apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
85734
86708
  });
85735
- standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : []);
86709
+ const needleValues = resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : [];
86710
+ standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(needleValues);
85736
86711
  const cssDisableSetup = `
85737
86712
  const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
85738
86713
  let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
@@ -85971,7 +86946,7 @@ test.afterEach(async ({ page }, testInfo) => {
85971
86946
  module.exports = {
85972
86947
  testDir: ${JSON.stringify(absTestDir)},
85973
86948
  outputDir: ${JSON.stringify(absOutputDir)},
85974
- timeout: 60000,
86949
+ timeout: ${usesInboxHelper ? 24e4 : 6e4},
85975
86950
  retries: 0,
85976
86951
  use: {
85977
86952
  baseURL: ${JSON.stringify(baseUrl)},
@@ -85997,6 +86972,27 @@ module.exports = {
85997
86972
  const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
85998
86973
  const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
85999
86974
  const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
86975
+ if (usesInboxHelper && validateContext?.runId) {
86976
+ const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
86977
+ if (!inboxConfig) {
86978
+ logs2.push("\u{1F4EA} Test uses __vqaWaitForEmail but this runner has no verification inbox configured \u2014 the helper will fail as CONFIG_ISSUE");
86979
+ } else {
86980
+ try {
86981
+ inboxBroker = await (0, test_inbox_broker_js_1.startTestInboxBroker)({
86982
+ config: inboxConfig,
86983
+ baseUrl,
86984
+ projectId: options.projectId ?? null,
86985
+ runId: validateContext.runId,
86986
+ log: (line) => logs2.push(line)
86987
+ });
86988
+ standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)([...needleValues, inboxBroker.identity.password]);
86989
+ } catch (error3) {
86990
+ logs2.push(`\u26A0 Inbox broker failed to start: ${error3 instanceof Error ? error3.message : String(error3)}`);
86991
+ }
86992
+ }
86993
+ } else if (usesInboxHelper) {
86994
+ logs2.push("\u{1F4EA} Test uses __vqaWaitForEmail but no run callback context is available \u2014 the helper will fail as CONFIG_ISSUE");
86995
+ }
86000
86996
  if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
86001
86997
  try {
86002
86998
  callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
@@ -86005,7 +87001,8 @@ module.exports = {
86005
87001
  upstreamAuthHeader: validateContext.authHeader,
86006
87002
  taskArn: process.env.VALIDATE_TASK_ARN,
86007
87003
  claimEpoch: callbackClaimEpoch,
86008
- authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
87004
+ authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH,
87005
+ ...inboxBroker ? { localRoutes: { [test_inbox_broker_js_1.INBOX_WAIT_ROUTE]: inboxBroker.handleWait } } : {}
86009
87006
  });
86010
87007
  } catch (error3) {
86011
87008
  logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
@@ -86032,14 +87029,19 @@ module.exports = {
86032
87029
  // Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
86033
87030
  // both parent and generated __vqaLogin code; "false" must still cross this process
86034
87031
  // boundary to disable the early publish POST. It remains a per-run toggle.
86035
- ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
87032
+ ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {},
87033
+ // Register-and-verify tests: the run's minted registration identity. An
87034
+ // address + a DERIVED throwaway-app password — never the IMAP secret,
87035
+ // which stays in this parent (test-inbox-broker.ts). Useless without the
87036
+ // loopback proxy the helper reads mail through, hence the guard.
87037
+ ...inboxBroker && callbackProxy ? inboxBroker.childEnv() : {}
86036
87038
  };
86037
87039
  delete childTestVariables.API_AUTH_HEADERS;
86038
87040
  logs2.push("\u{1F680} Spawning playwright test...");
86039
87041
  const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
86040
87042
  let playResult;
86041
87043
  try {
86042
- playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
87044
+ playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv, usesInboxHelper ? 3e5 : void 0);
86043
87045
  } finally {
86044
87046
  await stopLiveBrowserPolling?.();
86045
87047
  }
@@ -86130,6 +87132,8 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
86130
87132
  } finally {
86131
87133
  await callbackProxy?.close().catch(() => {
86132
87134
  });
87135
+ await inboxBroker?.close().catch(() => {
87136
+ });
86133
87137
  if (runDir) {
86134
87138
  await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
86135
87139
  });
@@ -86181,7 +87185,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
86181
87185
  stopped = true;
86182
87186
  };
86183
87187
  }
86184
- function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
87188
+ function runPlaywright(configPath, testVariables, trustedRuntimeEnv, killAfterMs) {
86185
87189
  return new Promise((resolvePromise) => {
86186
87190
  const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
86187
87191
  const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
@@ -86199,11 +87203,13 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
86199
87203
  proc.stderr?.on("data", (chunk) => {
86200
87204
  stderr += chunk.toString();
86201
87205
  });
86202
- const cancelKill = (0, spawn_with_tree_kill_js_1.scheduleTreeKill)(proc, 12e4, {
87206
+ const killMs = killAfterMs ?? 12e4;
87207
+ const cancelKill = (0, spawn_with_tree_kill_js_1.scheduleTreeKill)(proc, killMs, {
86203
87208
  graceMs: 1e4,
86204
87209
  onTimeout: () => {
86205
87210
  timedOut = true;
86206
- stderr += "\n[playwright killed after 120s safety timeout]";
87211
+ stderr += `
87212
+ [playwright killed after ${Math.round(killMs / 1e3)}s safety timeout]`;
86207
87213
  }
86208
87214
  });
86209
87215
  const settle = (result) => {
@@ -89902,402 +90908,6 @@ var require_anchor_harvester = __commonJS({
89902
90908
  }
89903
90909
  });
89904
90910
 
89905
- // ../runner-core/dist/services/discovery-lint-flags.js
89906
- var require_discovery_lint_flags = __commonJS({
89907
- "../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
89908
- "use strict";
89909
- Object.defineProperty(exports2, "__esModule", { value: true });
89910
- exports2.structuralLocatorLintMode = structuralLocatorLintMode;
89911
- exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
89912
- exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
89913
- function strictOn(value) {
89914
- return value === "true" || value === "1";
89915
- }
89916
- function structuralLocatorLintMode() {
89917
- const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
89918
- if (v === "warn")
89919
- return "warn";
89920
- if (v === "true" || v === "1" || v === "enforce")
89921
- return "enforce";
89922
- return "off";
89923
- }
89924
- function isPwMethodPreflightEnabled() {
89925
- return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
89926
- }
89927
- function isVqaLoginDedupeEnabled() {
89928
- return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
89929
- }
89930
- }
89931
- });
89932
-
89933
- // ../runner-core/dist/services/auth-recipe.js
89934
- var require_auth_recipe = __commonJS({
89935
- "../runner-core/dist/services/auth-recipe.js"(exports2) {
89936
- "use strict";
89937
- Object.defineProperty(exports2, "__esModule", { value: true });
89938
- exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = void 0;
89939
- exports2.computeRecipeHash = computeRecipeHash;
89940
- exports2.buildAuthRecipe = buildAuthRecipe;
89941
- exports2.authProofLocator = authProofLocator;
89942
- exports2.urlProofPredicateExpr = urlProofPredicateExpr;
89943
- exports2.renderAuthProofAssertion = renderAuthProofAssertion;
89944
- exports2.renderAuthFn = renderAuthFn;
89945
- exports2.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
89946
- exports2.absoluteRoute = absoluteRoute;
89947
- exports2.renderReplayClickArrow = renderReplayClickArrow;
89948
- exports2.renderReplaySettleArrow = renderReplaySettleArrow;
89949
- exports2.renderAuthUrlWaitLine = renderAuthUrlWaitLine;
89950
- exports2.commandToReplayStep = commandToReplayStep;
89951
- exports2.buildReplayPlan = buildReplayPlan;
89952
- exports2.extractAuthRegion = extractAuthRegion;
89953
- exports2.stripAuthRegions = stripAuthRegions;
89954
- exports2.ensureExpectImported = ensureExpectImported;
89955
- exports2.ensureLoginCall = ensureLoginCall;
89956
- exports2.upsertAuthBlock = upsertAuthBlock;
89957
- exports2.buildHealAuthStamp = buildHealAuthStamp;
89958
- var crypto_1 = require("crypto");
89959
- var shared_1 = (init_dist(), __toCommonJS(dist_exports));
89960
- var discovery_lint_flags_js_1 = require_discovery_lint_flags();
89961
- exports2.AUTH_MARKER_VERSION = "v3";
89962
- var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
89963
- var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
89964
- var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
89965
- var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
89966
- function computeRecipeHash(input) {
89967
- const canonical = JSON.stringify({
89968
- preNavRoute: input.preNavRoute,
89969
- preNavCmds: input.preNavCmds,
89970
- route: input.route,
89971
- cmds: input.cmds,
89972
- redirectRoute: input.redirectRoute,
89973
- authProof: input.authProof
89974
- });
89975
- return (0, crypto_1.createHash)("sha256").update(canonical).digest("hex").slice(0, 12);
89976
- }
89977
- function buildAuthRecipe(login, authProof, verifiedAt) {
89978
- const preNavRoute = login.preNavRoute ?? "/";
89979
- const preNavCmds = login.preNavCmds ?? [];
89980
- const recipeHash = computeRecipeHash({
89981
- preNavRoute,
89982
- preNavCmds,
89983
- route: login.route,
89984
- cmds: login.cmds,
89985
- redirectRoute: login.redirectRoute,
89986
- authProof
89987
- });
89988
- return {
89989
- version: 1,
89990
- recipeHash,
89991
- preNavRoute,
89992
- preNavCmds,
89993
- route: login.route,
89994
- cmds: login.cmds,
89995
- redirectRoute: login.redirectRoute,
89996
- authProof,
89997
- replayPlan: login.replayPlan ?? [],
89998
- verifiedAt
89999
- };
90000
- }
90001
- function authProofLocator(proof) {
90002
- if (proof.kind === "url") {
90003
- throw new Error("authProofLocator: url proof has no element locator; callers must branch on proof.kind");
90004
- }
90005
- if (proof.kind === "testid") {
90006
- return `page.getByTestId(${JSON.stringify(proof.testId)})`;
90007
- }
90008
- if (proof.name) {
90009
- return `page.getByRole(${JSON.stringify(proof.role)}, { name: ${JSON.stringify(proof.name)} })`;
90010
- }
90011
- return `page.getByRole(${JSON.stringify(proof.role)}).first()`;
90012
- }
90013
- function stripToPathname(route2) {
90014
- return route2.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/";
90015
- }
90016
- function urlProofPredicateExpr(route2) {
90017
- const p = stripToPathname(route2);
90018
- if (p === "/")
90019
- return `() => false`;
90020
- const r = JSON.stringify(p);
90021
- return `(u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/')`;
90022
- }
90023
- function renderAuthProofAssertion(proof) {
90024
- if (proof.kind === "url") {
90025
- return `await page.waitForURL(${urlProofPredicateExpr(proof.route)}, { timeout: 15000 });`;
90026
- }
90027
- return `await expect(${authProofLocator(proof)}).toBeVisible({ timeout: 15000 });`;
90028
- }
90029
- function normalizeCmd(cmd) {
90030
- let c = cmd.trim();
90031
- if (c.endsWith(";"))
90032
- c = c.slice(0, -1);
90033
- return ` ${c};`;
90034
- }
90035
- function renderAuthFn(recipe, opts) {
90036
- const lines = [];
90037
- lines.push(START_MARKER);
90038
- lines.push(`async function __vqaLogin(page) {`);
90039
- if (opts?.warmGuard) {
90040
- const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
90041
- const loginRoute = recipe.route ?? recipe.preNavRoute;
90042
- lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
90043
- lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
90044
- if (recipe.authProof.kind === "url") {
90045
- const pred = urlProofPredicateExpr(recipe.authProof.route);
90046
- lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
90047
- lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
90048
- } else if (loginRoute) {
90049
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
90050
- } else {
90051
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
90052
- }
90053
- lines.push(` }`);
90054
- }
90055
- lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
90056
- for (const cmd of recipe.preNavCmds)
90057
- lines.push(normalizeCmd(cmd));
90058
- if (recipe.preNavCmds.length === 0 && recipe.route && recipe.route !== recipe.preNavRoute) {
90059
- lines.push(` await page.goto(${JSON.stringify(recipe.route)}, { waitUntil: 'domcontentloaded' });`);
90060
- }
90061
- for (const cmd of recipe.cmds)
90062
- lines.push(normalizeCmd(cmd));
90063
- lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
90064
- lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
90065
- if (opts?.warmGuard) {
90066
- lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
90067
- lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_CLAIM_EPOCH && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
90068
- lines.push(` try {`);
90069
- lines.push(` const __vqaState = await page.context().storageState();`);
90070
- lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN, claimEpoch: Number(process.env.VALIDATE_CLAIM_EPOCH) }, timeout: 10000 });`);
90071
- lines.push(` } catch {}`);
90072
- lines.push(` }`);
90073
- }
90074
- lines.push(`}`);
90075
- lines.push(END_MARKER);
90076
- return lines.join("\n");
90077
- }
90078
- exports2.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
90079
- "AUTH_FILE",
90080
- "SHARED_AUTH_INJECT",
90081
- "VALIDATE_RUN_ID",
90082
- "VALIDATE_CLAIM_EPOCH",
90083
- "VALIDATE_AUTH_CONFIG_HASH",
90084
- "VALIDATE_TASK_ARN",
90085
- "VALIDATE_RUNNER_AUTH",
90086
- "VALIDATE_SERVER_URL"
90087
- ]);
90088
- function isReservedRunnerEnvVar(key) {
90089
- return exports2.RESERVED_RUNNER_ENV_VARS.has(key);
90090
- }
90091
- function absoluteRoute(route2, baseUrl) {
90092
- try {
90093
- return new URL(route2, baseUrl).href;
90094
- } catch {
90095
- return route2;
90096
- }
90097
- }
90098
- function renderReplayClickArrow(step) {
90099
- const loc = step.locator.trim();
90100
- if (step.kind === "wait") {
90101
- return `async (page) => {
90102
- await ${loc}.waitFor({ state: 'visible', timeout: 15000 });
90103
- }`;
90104
- }
90105
- if (step.kind === "press") {
90106
- return `async (page) => {
90107
- await ${loc}.press(${JSON.stringify(step.key ?? "Enter")});
90108
- }`;
90109
- }
90110
- return `async (page) => {
90111
- await ${loc}.click();
90112
- }`;
90113
- }
90114
- function renderReplaySettleArrow(proof) {
90115
- if (proof.kind === "url") {
90116
- return `async (page) => {
90117
- await page.waitForURL(${urlProofPredicateExpr(proof.route)}, { timeout: 15000 });
90118
- }`;
90119
- }
90120
- return `async (page) => {
90121
- await ${authProofLocator(proof)}.waitFor({ state: 'visible', timeout: 15000 });
90122
- }`;
90123
- }
90124
- function renderAuthUrlWaitLine(redirectRoute) {
90125
- const opts = `{ waitUntil: 'domcontentloaded', timeout: 8000 }`;
90126
- const tail = `.catch(() => { /* URL drift \u2014 the auth-proof assertion below is the gate */ });`;
90127
- const rr = redirectRoute ? stripToPathname(redirectRoute) : null;
90128
- if (rr && rr !== "/") {
90129
- const r = JSON.stringify(rr);
90130
- return ` await page.waitForURL((u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/'), ${opts})${tail}`;
90131
- }
90132
- if (rr === "/") {
90133
- return ` await page.waitForURL((u) => !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
90134
- }
90135
- return ` await page.waitForURL((u) => u.pathname !== '/' && !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
90136
- }
90137
- function locatorChainOf(cmd) {
90138
- const m = cmd.match(/\bpage\.[\s\S]*?(?=\.(?:fill|click|check|press|selectOption|setChecked|waitFor)\s*\()/);
90139
- return m ? m[0].trim() : null;
90140
- }
90141
- function parseRoleName(locator) {
90142
- const role = locator.match(/getByRole\(\s*['"`]([^'"`]+)['"`]/)?.[1];
90143
- const name = locator.match(/getByRole\([^)]*name:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByLabel\(\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByPlaceholder\(\s*['"`]([^'"`]+)['"`]/)?.[1];
90144
- return { role, name };
90145
- }
90146
- function credentialKeyOf(cmd) {
90147
- return cmd.match(/\.fill\(\s*process\.env\.([A-Za-z_$][\w$]*)!?\s*\)/)?.[1] ?? null;
90148
- }
90149
- function commandToReplayStep(cmd) {
90150
- const chain = locatorChainOf(cmd);
90151
- if (!chain)
90152
- return null;
90153
- const credKey = credentialKeyOf(cmd);
90154
- if (cmd.includes(".fill(")) {
90155
- if (!credKey)
90156
- return null;
90157
- const { role, name } = parseRoleName(chain);
90158
- return { kind: "fill", credentialKey: credKey, locator: chain, role, name, element: name ?? role ?? "credential field" };
90159
- }
90160
- if (/\.waitFor\s*\(/.test(cmd))
90161
- return { kind: "wait", locator: chain };
90162
- const pressKey = cmd.match(/\.press\(\s*['"`]([^'"`]+)['"`]\s*\)/)?.[1];
90163
- if (pressKey)
90164
- return { kind: "press", locator: chain, key: pressKey };
90165
- if (/\.(?:click|check|setChecked|selectOption)\s*\(/.test(cmd))
90166
- return { kind: "click", locator: chain };
90167
- return null;
90168
- }
90169
- function buildReplayPlan(login) {
90170
- const plan = [];
90171
- const preNavRoute = login.preNavRoute ?? "/";
90172
- plan.push({ kind: "nav", route: preNavRoute });
90173
- for (const c of login.preNavCmds ?? []) {
90174
- const step = commandToReplayStep(c);
90175
- if (step)
90176
- plan.push(step);
90177
- }
90178
- if ((login.preNavCmds ?? []).length === 0 && login.route && login.route !== preNavRoute) {
90179
- plan.push({ kind: "nav", route: login.route });
90180
- }
90181
- for (const c of login.cmds) {
90182
- const step = commandToReplayStep(c);
90183
- if (step)
90184
- plan.push(step);
90185
- }
90186
- return plan;
90187
- }
90188
- function extractAuthRegion(code) {
90189
- const m = code.match(new RegExp(AUTH_REGION_RE.source));
90190
- return m ? m[0] : null;
90191
- }
90192
- function countAuthRegions(code) {
90193
- const m = code.match(AUTH_REGION_RE);
90194
- return m ? m.length : 0;
90195
- }
90196
- function stripAuthRegions(code) {
90197
- return code.replace(AUTH_REGION_RE, "");
90198
- }
90199
- var PW_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?/;
90200
- function ensureExpectImported(code) {
90201
- const m = code.match(PW_IMPORT_RE);
90202
- if (!m)
90203
- return code;
90204
- const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
90205
- if (names.includes("expect"))
90206
- return code;
90207
- const next = [...names, "expect"];
90208
- return code.replace(PW_IMPORT_RE, `import { ${next.join(", ")} } from '@playwright/test';`);
90209
- }
90210
- function insertAfterPwImport(code, block) {
90211
- const m = code.match(PW_IMPORT_RE);
90212
- if (!m || m.index === void 0) {
90213
- return `${block}
90214
-
90215
- ${code.replace(/^\r?\n+/, "")}`;
90216
- }
90217
- const end = m.index + m[0].length;
90218
- const head = code.slice(0, end);
90219
- const tail = code.slice(end).replace(/^\r?\n+/, "");
90220
- return `${head}
90221
-
90222
- ${block}
90223
-
90224
- ${tail}`;
90225
- }
90226
- var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
90227
- var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
90228
- function ensureLoginCall(code) {
90229
- if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
90230
- return code;
90231
- let result = code;
90232
- if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
90233
- result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
90234
- }
90235
- const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
90236
- for (let i = matches.length - 1; i >= 0; i--) {
90237
- const m = matches[i];
90238
- if (m.index === void 0)
90239
- continue;
90240
- const insertAt = m.index + m[0].length;
90241
- const following = result.slice(insertAt, insertAt + 60);
90242
- if (/^\s*await\s+__vqaLogin\s*\(\s*page\s*\)/.test(following))
90243
- continue;
90244
- result = `${result.slice(0, insertAt)}
90245
- await __vqaLogin(page);${result.slice(insertAt)}`;
90246
- }
90247
- return result;
90248
- }
90249
- function upsertAuthBlock(code, ctx) {
90250
- if (ctx.mode === "generate") {
90251
- if (!ctx.recipe || !ctx.isAuthNeeded)
90252
- return code;
90253
- let result = stripAuthRegions(code);
90254
- result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
90255
- result = ensureExpectImported(result);
90256
- const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
90257
- result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
90258
- result = ensureLoginCall(result);
90259
- const n = countAuthRegions(result);
90260
- if (n !== 1) {
90261
- throw new Error(`upsertAuthBlock(generate): expected exactly 1 auth region after stamp, got ${n}`);
90262
- }
90263
- if (!LOGIN_CALL_RE.test(result)) {
90264
- throw new Error("upsertAuthBlock(generate): stamped __vqaLogin but found no test() opener to inject the call");
90265
- }
90266
- return result;
90267
- }
90268
- try {
90269
- if (!ctx.preservedAuthRegion)
90270
- return code;
90271
- let result = ensureExpectImported(code);
90272
- if (countAuthRegions(result) >= 1) {
90273
- result = result.replace(AUTH_REGION_RE, "");
90274
- result = insertAfterPwImport(result, ctx.preservedAuthRegion);
90275
- } else {
90276
- result = insertAfterPwImport(result, ctx.preservedAuthRegion);
90277
- }
90278
- result = ensureLoginCall(result);
90279
- if (countAuthRegions(result) !== 1) {
90280
- throw new Error("heal re-stamp produced non-unique auth region");
90281
- }
90282
- return result;
90283
- } catch (err) {
90284
- const reason = err instanceof Error ? err.message : String(err);
90285
- ctx.onSignal?.({ type: "auth-stamp-degraded", reason });
90286
- return code;
90287
- }
90288
- }
90289
- function buildHealAuthStamp(originalCode, failureError, logs2) {
90290
- const region = extractAuthRegion(originalCode);
90291
- const loginFailed = !!failureError && /__vqaLogin/.test(failureError);
90292
- return {
90293
- mode: "heal",
90294
- preservedAuthRegion: region && !loginFailed ? region : null,
90295
- onSignal: logs2 ? (s) => logs2.push(`\u26A0 auth-stamp ${s.type}: ${s.reason}`) : void 0
90296
- };
90297
- }
90298
- }
90299
- });
90300
-
90301
90911
  // ../runner-core/dist/services/auth-replay.js
90302
90912
  var require_auth_replay = __commonJS({
90303
90913
  "../runner-core/dist/services/auth-replay.js"(exports2) {
@@ -94174,9 +94784,41 @@ var require_discover_generator = __commonJS({
94174
94784
  var reveal_diff_js_1 = require_reveal_diff();
94175
94785
  var anchor_harvester_js_1 = require_anchor_harvester();
94176
94786
  var auth_recipe_js_1 = require_auth_recipe();
94787
+ var inbox_stamp_js_1 = require_inbox_stamp();
94177
94788
  var auth_replay_js_1 = require_auth_replay();
94178
94789
  var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
94179
94790
  var discovery_lint_flags_js_1 = require_discovery_lint_flags();
94791
+ var inbox_tools_js_1 = require_inbox_tools();
94792
+ var verification_inbox_js_1 = require_verification_inbox();
94793
+ var test_inbox_broker_js_1 = require_test_inbox_broker();
94794
+ var REGISTER_SCENARIO_RE = /\b(register|registration|sign[\s-]?up|create[\s-]?(a[n]?[\s-]?)?(new[\s-]?)?account|verify[\s-]?(your[\s-]?)?email|email[\s-]?verification|confirm[\s-]?(your[\s-]?)?(email|account)|activate[\s-]?(your[\s-]?)?account|new[\s-]?account)\b/i;
94795
+ function scenarioLooksLikeRegistration(scenario) {
94796
+ const hay = `${scenario.name ?? ""} ${scenario.description ?? ""} ${scenario.entryRoute ?? ""} ${(scenario.targetPages ?? []).join(" ")}`;
94797
+ return REGISTER_SCENARIO_RE.test(hay);
94798
+ }
94799
+ function buildEmailVerifyGenBlock(genEmail, genPassword) {
94800
+ return `
94801
+
94802
+ ## EMAIL VERIFICATION TEST \u2014 register a fresh account and verify it (LIVE, then EMIT)
94803
+ This is a signup / email-verification scenario and this runner CAN receive verification email. Prove the full flow live, then emit a test that re-does it on every run.
94804
+
94805
+ LIVE (to prove the flow while generating):
94806
+ - Register on the app's signup form using this throwaway account:
94807
+ - email: \`${genEmail}\`
94808
+ - password: \`${genPassword}\`
94809
+ Type them with browser_type and submit the form ONCE.
94810
+ - If the app then asks for an emailed code or a "verify your email" link, call \`wait_for_verification\` (poll it again if the first call returns found:false \u2014 mail can take 30\u201390s). If it returns a \`code\`, type it into the verification field. If it returns a \`link\`, use \`open_verification_link\`.
94811
+ - Reach the verified / signed-in state and confirm it with a browser_verify_* call, so your final assertion is proven.
94812
+
94813
+ EMIT (the code you put in finish_generate) \u2014 do NOT hardcode the throwaway values above:
94814
+ - Use \`process.env.${test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV}!\` for the email and \`process.env.${test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV}!\` for the password. The runner mints a FRESH real account for each execution and injects these \u2014 a hardcoded email would collide with "already registered" on re-runs.
94815
+ - Read the verification email at runtime with the injected helper (it is provided for you \u2014 do NOT define it, do NOT import it):
94816
+ - code flow: \`const code = await __vqaWaitForEmail('code');\` then type \`code\` into the verification field.
94817
+ - link flow: \`const link = await __vqaWaitForEmail('link');\` then \`await page.goto(link);\`.
94818
+ - if unsure: \`const email = await __vqaWaitForEmail();\` returns \`{ code, link, subject, from }\`.
94819
+ - Tag the test \`@email-verify\`. Emit EXACTLY ONE test() in this file (one registration per run).
94820
+ - End with a DURABLE assertion of the post-verification signed-in state (e.g. a Logout control, an account/dashboard landmark) in your OWN test body \u2014 the helper call is not itself a proof.`;
94821
+ }
94180
94822
  exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
94181
94823
  "browser_install",
94182
94824
  "browser_close",
@@ -95363,6 +96005,11 @@ ${testOpen}`;
95363
96005
  if (options?.authStamp) {
95364
96006
  result = (0, auth_recipe_js_1.upsertAuthBlock)(result, options.authStamp);
95365
96007
  }
96008
+ try {
96009
+ result = (0, inbox_stamp_js_1.upsertInboxBlock)(result);
96010
+ } catch {
96011
+ result = (0, inbox_stamp_js_1.stripInboxRegions)(result);
96012
+ }
95366
96013
  return result;
95367
96014
  }
95368
96015
  function ensureDiscoveredTag(tags) {
@@ -95449,6 +96096,16 @@ ${testOpen}`;
95449
96096
  logs2?.push(` \u2717 Rejected "${t.name}": JS parse error \u2014 ${msg}`);
95450
96097
  return false;
95451
96098
  }
96099
+ {
96100
+ const modelBody = (0, inbox_stamp_js_1.stripInboxRegions)(code);
96101
+ if (inbox_stamp_js_1.INBOX_HELPER_TOKEN_RE.test(modelBody)) {
96102
+ const openerCount = (stripStringsAndComments(modelBody).match(/\btest(?:\.(?:only|skip|fixme))?\s*\(/g) ?? []).length;
96103
+ if (openerCount > 1) {
96104
+ logs2?.push(` \u2717 Rejected "${t.name}": ${openerCount} test() blocks in a register-and-verify spec \u2014 the per-run registration email supports exactly one registration; emit ONE test() per spec`);
96105
+ return false;
96106
+ }
96107
+ }
96108
+ }
95452
96109
  const codeForArgOrderScan = stripStringsAndComments(code);
95453
96110
  if (/test\s*\(\s*,\s*(?:async\s*)?(?:\([^)]*\)|function\s*\([^)]*\))\s*(?:=>)?\s*\{[\s\S]*?\}\s*,\s*\{\s*(?:tags?|annotations)\s*:/.test(codeForArgOrderScan)) {
95454
96111
  logs2?.push(` \u2717 Rejected "${t.name}": test() arg order is wrong \u2014 Playwright signature is test(name, options, fn), AI emitted test(name, fn, options)`);
@@ -96269,6 +96926,23 @@ ${testOpen}`;
96269
96926
  return { offendingLines: offending };
96270
96927
  }
96271
96928
  var ACTION_METHODS = ["click", "dblclick", "tap", "fill", "type", "pressSequentially", "press", "selectOption", "hover", "check", "uncheck", "setInputFiles", "dragTo", "focus", "goto", "waitForURL"];
96929
+ var DYNAMIC_VERIFICATION_ARG = "<verification-dynamic>";
96930
+ function collectVerificationHelperVars(code) {
96931
+ const vars = /* @__PURE__ */ new Set();
96932
+ const assignRe = /(?:(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*await\s+__vqaWaitForEmail\s*\(/g;
96933
+ let m;
96934
+ while ((m = assignRe.exec(code)) !== null)
96935
+ vars.add(m[1]);
96936
+ const destructRe = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*await\s+__vqaWaitForEmail\s*\(/g;
96937
+ while ((m = destructRe.exec(code)) !== null) {
96938
+ for (const part of m[1].split(",")) {
96939
+ const name = part.split(":").pop()?.trim().replace(/\s.*$/, "");
96940
+ if (name && /^[A-Za-z_$][\w$]*$/.test(name))
96941
+ vars.add(name);
96942
+ }
96943
+ }
96944
+ return vars;
96945
+ }
96272
96946
  function extractStringArg(text) {
96273
96947
  const m = text.match(/^\s*(['"`])((?:\\.|(?!\1).)*)\1/);
96274
96948
  if (!m)
@@ -96538,6 +97212,8 @@ ${testOpen}`;
96538
97212
  if (!locatorsMatch(ledger.locator, submitted.locator))
96539
97213
  return false;
96540
97214
  if (ledger.method === "fill" || ledger.method === "type") {
97215
+ if (submitted.arg === DYNAMIC_VERIFICATION_ARG)
97216
+ return true;
96541
97217
  if (ledger.arg === "<env>" || ledger.arg === "<test-value>") {
96542
97218
  return submitted.arg === "<env>" || submitted.arg === "<test-value>";
96543
97219
  }
@@ -96632,6 +97308,25 @@ ${testOpen}`;
96632
97308
  }
96633
97309
  }
96634
97310
  const submittedSigs = extractActionSignatures(playwrightCode, credentials);
97311
+ const helperVars = collectVerificationHelperVars(playwrightCode);
97312
+ if (helperVars.size > 0) {
97313
+ const usesHelperVar = (text) => {
97314
+ for (const v of helperVars) {
97315
+ if (new RegExp(`\\b${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text))
97316
+ return true;
97317
+ }
97318
+ return false;
97319
+ };
97320
+ for (const sub of submittedSigs) {
97321
+ if ((sub.method === "fill" || sub.method === "type") && sub.arg && usesHelperVar(sub.arg)) {
97322
+ sub.arg = DYNAMIC_VERIFICATION_ARG;
97323
+ } else if (sub.method === "goto") {
97324
+ const gotoArg = sub.raw.match(/\.goto\s*\(\s*([^),]+)/);
97325
+ if (gotoArg && usesHelperVar(gotoArg[1]))
97326
+ sub.arg = DYNAMIC_VERIFICATION_ARG;
97327
+ }
97328
+ }
97329
+ }
96635
97330
  const offending = [];
96636
97331
  let firstGotoConsumed = false;
96637
97332
  let cursor = 0;
@@ -96639,6 +97334,8 @@ ${testOpen}`;
96639
97334
  for (const sub of submittedSigs) {
96640
97335
  if (sub.method === "waitForURL")
96641
97336
  continue;
97337
+ if (sub.method === "goto" && sub.arg === DYNAMIC_VERIFICATION_ARG)
97338
+ continue;
96642
97339
  if (sub.method === "goto" && !firstGotoConsumed) {
96643
97340
  firstGotoConsumed = true;
96644
97341
  if (expectedEntry !== null && sub.arg !== expectedEntry) {
@@ -96964,7 +97661,7 @@ expect(secondTitle).toBeTruthy();
96964
97661
  ${tailBlock}`;
96965
97662
  }
96966
97663
  function evaluateAmbiguityGate(finalCode, alreadyAttempted, hasRevisionBudget) {
96967
- const findings = (0, agentic_discovery_prompts_js_1.scanAmbiguousLocators)((0, auth_recipe_js_1.stripAuthRegions)(finalCode));
97664
+ const findings = (0, agentic_discovery_prompts_js_1.scanAmbiguousLocators)((0, auth_recipe_js_1.stripAuthRegions)((0, inbox_stamp_js_1.stripInboxRegions)(finalCode)));
96968
97665
  return { fire: findings.length > 0 && !alreadyAttempted && hasRevisionBudget, findings };
96969
97666
  }
96970
97667
  async function generateTestFromScenario(mcpClient, scenario, siteMap, baseUrl, credentials, aiClient, agentModel, logs2, onAICall, opts) {
@@ -96988,6 +97685,7 @@ ${tailBlock}`;
96988
97685
  }
96989
97686
  }
96990
97687
  }
97688
+ let genInboxReader = null;
96991
97689
  try {
96992
97690
  const startTime = Date.now();
96993
97691
  const groupId = `scenario-gen-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
@@ -97038,12 +97736,28 @@ ${tailBlock}`;
97038
97736
  });
97039
97737
  const baseSkeletonHadRegression = tags.includes("@regression");
97040
97738
  const hasVerifiedRecipe = !!opts?.recipe && tags.includes("@needs-auth");
97041
- const systemPrompt = buildScenarioSystemPrompt(scenario, baseUrl, credentials, tags, siteMap.pages, { appBrief: opts?.appBrief, availableVariableKeys: opts?.availableVariableKeys, hasVerifiedRecipe, knowledgeContext: opts?.knowledgeContext });
97739
+ const projectHasRegisterIntent = credentials?.DISCOVERY_SHOULD_REGISTER === "true";
97740
+ const inboxGenConfig = (0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() && projectHasRegisterIntent && !hasVerifiedRecipe && scenarioLooksLikeRegistration(scenario) ? (0, verification_inbox_js_1.loadVerificationInboxConfig)() : null;
97741
+ let genInboxState = null;
97742
+ let genInboxCredentials = credentials;
97743
+ let genInboxSecret = null;
97744
+ if (inboxGenConfig) {
97745
+ const genTag = `vqag${(0, crypto_1.createHash)("sha256").update(`testgen|${(0, verification_inbox_js_1.seedToHostKey)(baseUrl)}|${groupId}`).digest("hex").slice(0, 10)}`;
97746
+ const genEmail = (0, verification_inbox_js_1.applyPlusTag)(inboxGenConfig.address, genTag);
97747
+ const genPassword = (0, verification_inbox_js_1.deriveRegistrationPassword)(genEmail);
97748
+ genInboxReader = new verification_inbox_js_1.VerificationInboxReader(inboxGenConfig);
97749
+ genInboxState = (0, inbox_tools_js_1.createInboxRunState)(inboxGenConfig, { tag: genTag, passwordKey: test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV });
97750
+ genInboxCredentials = { ...credentials, [test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV]: genEmail, [test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV]: genPassword };
97751
+ genInboxSecret = genPassword;
97752
+ openaiTools.push(...inbox_tools_js_1.INBOX_TOOLS);
97753
+ logs2.push(` \u{1F4EC} Email-verification generation ON for "${scenario.name}" \u2014 registering throwaway ${genEmail}`);
97754
+ }
97755
+ const systemPrompt = buildScenarioSystemPrompt(scenario, baseUrl, credentials, tags, siteMap.pages, { appBrief: opts?.appBrief, availableVariableKeys: opts?.availableVariableKeys, hasVerifiedRecipe, knowledgeContext: opts?.knowledgeContext }) + (genInboxState ? buildEmailVerifyGenBlock(genInboxCredentials[test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV], genInboxCredentials[test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV]) : "");
97042
97756
  const messages = [
97043
97757
  { role: "system", content: systemPrompt },
97044
97758
  {
97045
97759
  role: "user",
97046
- content: hasVerifiedRecipe ? `Start generating the test for scenario: "${scenario.name}". You are ALREADY logged in \u2014 \`await __vqaLogin(page);\` is injected as the first line of your test body, so do NOT author any login steps. Begin from the authenticated app and reach ${baseUrl}${findFirstNonAuthRoute(scenario.targetPages) || scenario.targetPages[0] || "/"} via visible navigation or a direct goto. Execute each step from the scenario plan, verifying with browser_verify_* tools at transition points.` : tags.includes("@needs-auth") ? `Start generating the test for scenario: "${scenario.name}". Begin at ${baseUrl}${scenario.entryRoute}. Complete the AUTH SETUP from the app's own visible flow (do NOT deep-link directly to auth unless the start route exposes no usable entry), then continue to ${baseUrl}${findFirstNonAuthRoute(scenario.targetPages) || scenario.targetPages[0] || "/"}. Execute each step from the scenario plan, verifying with browser_verify_* tools at transition points.` : `Start generating the test for scenario: "${scenario.name}". Begin by navigating to ${baseUrl}${scenario.entryRoute} using browser_navigate. From there, use the app's own navigation to reach the scenario flow. Do NOT deep-link directly to ${baseUrl}${scenario.targetPages[0] || "/"} unless the start route immediately redirects there or no visible navigation path exists. Then execute each step from the scenario plan, verifying with browser_verify_* tools at transition points.`
97760
+ content: genInboxState ? `Start generating the register-and-verify test for scenario: "${scenario.name}". Begin at ${baseUrl}${scenario.entryRoute}. Follow the "EMAIL VERIFICATION TEST" section: register the throwaway account LIVE, complete email verification via wait_for_verification, reach the signed-in state and verify it \u2014 then emit a test that uses process.env.${test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV}!/process.env.${test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV}! and __vqaWaitForEmail(...) as instructed.` : hasVerifiedRecipe ? `Start generating the test for scenario: "${scenario.name}". You are ALREADY logged in \u2014 \`await __vqaLogin(page);\` is injected as the first line of your test body, so do NOT author any login steps. Begin from the authenticated app and reach ${baseUrl}${findFirstNonAuthRoute(scenario.targetPages) || scenario.targetPages[0] || "/"} via visible navigation or a direct goto. Execute each step from the scenario plan, verifying with browser_verify_* tools at transition points.` : tags.includes("@needs-auth") ? `Start generating the test for scenario: "${scenario.name}". Begin at ${baseUrl}${scenario.entryRoute}. Complete the AUTH SETUP from the app's own visible flow (do NOT deep-link directly to auth unless the start route exposes no usable entry), then continue to ${baseUrl}${findFirstNonAuthRoute(scenario.targetPages) || scenario.targetPages[0] || "/"}. Execute each step from the scenario plan, verifying with browser_verify_* tools at transition points.` : `Start generating the test for scenario: "${scenario.name}". Begin by navigating to ${baseUrl}${scenario.entryRoute} using browser_navigate. From there, use the app's own navigation to reach the scenario flow. Do NOT deep-link directly to ${baseUrl}${scenario.targetPages[0] || "/"} unless the start route immediately redirects there or no visible navigation path exists. Then execute each step from the scenario plan, verifying with browser_verify_* tools at transition points.`
97047
97761
  }
97048
97762
  ];
97049
97763
  let iteration = 0;
@@ -97171,6 +97885,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97171
97885
  const iterStart = Date.now();
97172
97886
  const requestMessageCount = messages.length;
97173
97887
  const requestMessages = messages.slice();
97888
+ const scrubAudit = (payload) => {
97889
+ if (!genInboxSecret)
97890
+ return payload;
97891
+ try {
97892
+ return JSON.parse(JSON.stringify(payload).split(genInboxSecret).join("[REDACTED:VQA_TEST_ACCOUNT_PASSWORD]"));
97893
+ } catch {
97894
+ return payload;
97895
+ }
97896
+ };
97174
97897
  let response;
97175
97898
  try {
97176
97899
  response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
@@ -97212,7 +97935,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97212
97935
  messages.push(assistantMessage);
97213
97936
  if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
97214
97937
  try {
97215
- onAICall?.({
97938
+ onAICall?.(scrubAudit({
97216
97939
  iteration,
97217
97940
  groupId,
97218
97941
  callType: "scenarioGenerate",
@@ -97226,7 +97949,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97226
97949
  requestMessageCount,
97227
97950
  requestMessages,
97228
97951
  ledger: actionLedger.slice()
97229
- });
97952
+ }));
97230
97953
  } catch {
97231
97954
  }
97232
97955
  logs2.push(` No tool calls \u2014 agent stopped`);
@@ -97253,7 +97976,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97253
97976
  const suspectedBugDeclared = toolArgs.suspectedBug === true;
97254
97977
  const bugDescription = String(toolArgs.bugDescription || "").slice(0, 500).replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/\s+/g, " ").trim();
97255
97978
  const testObj = { name, description, playwrightCode: code, tags };
97256
- const validated = validateTests([testObj], logs2, { sensitiveEnvMap: credentials, authRecipe: opts?.recipe ?? null, basePath: gotoBasePathFromBaseUrl(baseUrl) });
97979
+ const validated = validateTests([testObj], logs2, { sensitiveEnvMap: genInboxCredentials, authRecipe: opts?.recipe ?? null, basePath: gotoBasePathFromBaseUrl(baseUrl) });
97257
97980
  if (validated.length > 0) {
97258
97981
  if (actionLedger.length > lastRenderedLedgerEntryCount && actionLedger.length > 0) {
97259
97982
  const renderedLedger = renderSessionRecording(actionLedger, credentials);
@@ -97305,7 +98028,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97305
98028
  });
97306
98029
  continue;
97307
98030
  }
97308
- const actionCheck = validateFinalActionFidelity(code, actionLedger, credentials, { entryRoute: scenario.entryRoute });
98031
+ const actionCheck = validateFinalActionFidelity(code, actionLedger, genInboxCredentials, { entryRoute: scenario.entryRoute });
97309
98032
  if (actionCheck.offendingActions.length > 0 && !actionRevisionAttempted) {
97310
98033
  actionRevisionAttempted = true;
97311
98034
  const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
@@ -97618,6 +98341,15 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
97618
98341
  });
97619
98342
  continue;
97620
98343
  }
98344
+ if (genInboxReader && genInboxState && inbox_tools_js_1.INBOX_TOOL_NAMES.has(toolName)) {
98345
+ const inboxResult = await (0, inbox_tools_js_1.dispatchInboxTool)(toolName, toolArgs, {
98346
+ reader: genInboxReader,
98347
+ state: genInboxState,
98348
+ mcpClient
98349
+ });
98350
+ toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: inboxResult.content });
98351
+ continue;
98352
+ }
97621
98353
  let toolResultContent = "";
97622
98354
  try {
97623
98355
  const mcpResult = await mcpClient.callTool({ name: toolName, arguments: toolArgs });
@@ -97780,7 +98512,7 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
97780
98512
  tool: assistantMessage.tool_calls?.find((tc) => tc.id === tr.tool_call_id)?.function.name ?? "unknown",
97781
98513
  result: typeof tr.content === "string" ? tr.content.slice(0, 5e4) : String(tr.content).slice(0, 5e4)
97782
98514
  }));
97783
- onAICall?.({
98515
+ onAICall?.(scrubAudit({
97784
98516
  iteration,
97785
98517
  groupId,
97786
98518
  callType: "scenarioGenerate",
@@ -97796,7 +98528,7 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
97796
98528
  requestMessageCount,
97797
98529
  requestMessages,
97798
98530
  ledger: actionLedger.slice()
97799
- });
98531
+ }));
97800
98532
  } catch {
97801
98533
  }
97802
98534
  messages.push(...toolResults);
@@ -97804,8 +98536,12 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
97804
98536
  if (!result) {
97805
98537
  logs2.push(` Scenario generation did not produce a test after ${iteration} iterations`);
97806
98538
  }
98539
+ await genInboxReader?.close().catch(() => {
98540
+ });
97807
98541
  return result;
97808
98542
  } catch (err) {
98543
+ await genInboxReader?.close().catch(() => {
98544
+ });
97809
98545
  const msg = err instanceof Error ? err.message : String(err);
97810
98546
  logs2.push(` Scenario generation failed: ${msg}`);
97811
98547
  if ((0, ai_retry_js_1.isAIProviderError)(err)) {
@@ -319090,7 +319826,10 @@ var require_survey_link_frontier = __commonJS({
319090
319826
  "../runner-core/dist/services/survey-link-frontier.js"(exports2) {
319091
319827
  "use strict";
319092
319828
  Object.defineProperty(exports2, "__esModule", { value: true });
319093
- exports2.FINISH_GATE_MIN_BUDGET_PCT = exports2.MAX_FINISH_REDIRECTS = void 0;
319829
+ exports2.FINISH_ESCAPE_BOUNCE_STREAK = exports2.FINISH_GATE_MIN_BUDGET_PCT = exports2.MAX_FINISH_REDIRECTS = void 0;
319830
+ exports2.initialFinishBounceState = initialFinishBounceState;
319831
+ exports2.evaluateFinishBounce = evaluateFinishBounce;
319832
+ exports2.recordFinishBounce = recordFinishBounce;
319094
319833
  exports2.isSurveyLinkFrontierEnabled = isSurveyLinkFrontierEnabled;
319095
319834
  exports2.extractSnapshotLinks = extractSnapshotLinks;
319096
319835
  exports2.deriveLinkScope = deriveLinkScope;
@@ -319107,6 +319846,17 @@ var require_survey_link_frontier = __commonJS({
319107
319846
  var exploration_parser_js_1 = require_exploration_parser();
319108
319847
  exports2.MAX_FINISH_REDIRECTS = 2;
319109
319848
  exports2.FINISH_GATE_MIN_BUDGET_PCT = 0.2;
319849
+ exports2.FINISH_ESCAPE_BOUNCE_STREAK = 3;
319850
+ function initialFinishBounceState() {
319851
+ return { streak: 0, pagesBaseline: 0 };
319852
+ }
319853
+ function evaluateFinishBounce(prev, pagesVisited, threshold = exports2.FINISH_ESCAPE_BOUNCE_STREAK) {
319854
+ const base2 = pagesVisited > prev.pagesBaseline ? { streak: 0, pagesBaseline: pagesVisited } : prev;
319855
+ return { state: base2, escape: base2.streak >= threshold };
319856
+ }
319857
+ function recordFinishBounce(prev) {
319858
+ return { streak: prev.streak + 1, pagesBaseline: prev.pagesBaseline };
319859
+ }
319110
319860
  function isSurveyLinkFrontierEnabled() {
319111
319861
  const v = process.env.DISCOVERY_SURVEY_LINK_FRONTIER;
319112
319862
  return v === "true" || v === "1";
@@ -323295,6 +324045,7 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
323295
324045
  const linkScope = (0, survey_link_frontier_js_1.deriveLinkScope)(baseUrl);
323296
324046
  const surveyNavScope = options?.mode === "survey" ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
323297
324047
  let finishRedirects = 0;
324048
+ let finishBounce = (0, survey_link_frontier_js_1.initialFinishBounceState)();
323298
324049
  let lastLandedPageUrl = null;
323299
324050
  const state2 = (0, exploration_state_js_1.createExplorationState)();
323300
324051
  function ingestLinksFromText(rawText) {
@@ -323532,12 +324283,14 @@ ${inboxPromptBlock}`;
323532
324283
  const item = state2.frontier.get(r);
323533
324284
  return item?.source === "link-seen" && item.attempted === true;
323534
324285
  };
323535
- const ranked = (0, exploration_state_js_1.getRankedFrontier)(state2, { preferShallow: true }).filter((r) => r !== "/" && !isAttemptedLinkEntry(r));
324286
+ const isSatisfied = (r) => (0, survey_link_frontier_js_1.visitedSatisfies)(state2, r);
324287
+ const ranked = (0, exploration_state_js_1.getRankedFrontier)(state2, { preferShallow: true }).filter((r) => r !== "/" && !isAttemptedLinkEntry(r) && !isSatisfied(r));
323536
324288
  const rankedSet = new Set(ranked);
323537
324289
  const navigatedUncaptured = [];
323538
324290
  for (const r of state2.frontier.keys()) {
323539
- if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r))
324291
+ if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r) && !isSatisfied(r)) {
323540
324292
  navigatedUncaptured.push(r);
324293
+ }
323541
324294
  }
323542
324295
  return ranked.concat(navigatedUncaptured);
323543
324296
  };
@@ -324090,6 +324843,7 @@ ${currentSummary}`;
324090
324843
  }
324091
324844
  const toolResults = [];
324092
324845
  let finishBouncedThisBatch = false;
324846
+ let finishStreakCountedThisBatch = false;
324093
324847
  const fullVarMapForGuard = variableContext.allTestVariables ?? variableContext.publicTestVariables;
324094
324848
  const { secrets: secretsOnlyForGuard } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMapForGuard);
324095
324849
  const allowedCredentialValues = inboxState ? [inboxState.email] : [];
@@ -324268,25 +325022,41 @@ ${currentSummary}`;
324268
325022
  }
324269
325023
  logs2.push(` Captured page: ${normalized.route}, ${normalized.provenCommands.length} proven commands${normalized.alreadyCaptured ? " (RE-VISIT)" : ""}`);
324270
325024
  const transitionHint = !normalized.alreadyCaptured && isSurveyMode ? { tip: `Also call record_transition with fromRoute, toRoute, triggerType, and triggerLabel (the exact link text you clicked to get here).` } : {};
324271
- const lastAction = normalized.observations.length > 0 ? normalized.observations[normalized.observations.length - 1].action : void 0;
324272
- const snapshotBlock = (0, snapshot_observation_js_1.renderCompactSnapshotBlock)(
324273
- normalized,
324274
- { lastAction },
324275
- // G10: no explicit elementLimit — let the flag-aware deep default govern
324276
- // (flags-off 6 = byte-identical; richRender 8, so demoted form controls fit).
324277
- { formLimit: 3, observationLimit: 3, mode: options?.mode ?? "full", recoveredClickables }
324278
- );
324279
- toolResults.push({
324280
- role: "tool",
324281
- tool_call_id: toolCall.id,
324282
- content: JSON.stringify({
324283
- acknowledged: true,
324284
- ...normalized.alreadyCaptured ? { note: "Already captured - move on to unvisited pages." } : {},
324285
- ...transitionHint,
324286
- snapshot: snapshotBlock,
324287
- progress: `${state2.pagesVisited} pages, ${state2.transitionsFound} transitions, ${state2.recordedJourneys.length} tests | iteration ${iteration}/${maxIterations} (${maxIterations - iteration} remaining)`
324288
- })
324289
- });
325025
+ const progressLine = `${state2.pagesVisited} pages, ${state2.transitionsFound} transitions, ${state2.recordedJourneys.length} tests | iteration ${iteration}/${maxIterations} (${maxIterations - iteration} remaining)`;
325026
+ if (isSurveyMode && normalized.alreadyCaptured && normalized.observations.length === 0 && (adaptiveActive || linkFrontierEnabled)) {
325027
+ const nextRoutes = (adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route)).slice(0, 6);
325028
+ const steer = nextRoutes.length > 0 ? `Already mapped \u2014 no new intel on this page. Still un-captured: ${nextRoutes.join(", ")}. Navigate to one and capture_page, or call finish_exploration if none remain.` : "Already mapped and no un-captured routes remain. Call finish_exploration.";
325029
+ toolResults.push({
325030
+ role: "tool",
325031
+ tool_call_id: toolCall.id,
325032
+ content: JSON.stringify({
325033
+ acknowledged: true,
325034
+ alreadyCaptured: true,
325035
+ note: steer,
325036
+ progress: progressLine
325037
+ })
325038
+ });
325039
+ } else {
325040
+ const lastAction = normalized.observations.length > 0 ? normalized.observations[normalized.observations.length - 1].action : void 0;
325041
+ const snapshotBlock = (0, snapshot_observation_js_1.renderCompactSnapshotBlock)(
325042
+ normalized,
325043
+ { lastAction },
325044
+ // G10: no explicit elementLimit — let the flag-aware deep default govern
325045
+ // (flags-off 6 = byte-identical; richRender 8, so demoted form controls fit).
325046
+ { formLimit: 3, observationLimit: 3, mode: options?.mode ?? "full", recoveredClickables }
325047
+ );
325048
+ toolResults.push({
325049
+ role: "tool",
325050
+ tool_call_id: toolCall.id,
325051
+ content: JSON.stringify({
325052
+ acknowledged: true,
325053
+ ...normalized.alreadyCaptured ? { note: "Already captured - move on to unvisited pages." } : {},
325054
+ ...transitionHint,
325055
+ snapshot: snapshotBlock,
325056
+ progress: progressLine
325057
+ })
325058
+ });
325059
+ }
324290
325060
  } catch (err) {
324291
325061
  const errMsg2 = err instanceof Error ? err.message : String(err);
324292
325062
  logs2.push(` Error persisting page: ${errMsg2}`);
@@ -324703,7 +325473,18 @@ ${currentSummary}`;
324703
325473
  continue;
324704
325474
  }
324705
325475
  if (toolName === "finish_exploration") {
324706
- if (linkFrontierEnabled) {
325476
+ const distinctPathsVisited = new Set([...state2.visitedRoutes].map((r) => (0, exploration_parser_js_1.normalizeRouteForFocusScope)(r))).size;
325477
+ const fb = (0, survey_link_frontier_js_1.evaluateFinishBounce)(finishBounce, distinctPathsVisited);
325478
+ finishBounce = fb.state;
325479
+ const forceAcceptFinish = fb.escape && !finishStreakCountedThisBatch;
325480
+ if (forceAcceptFinish) {
325481
+ const stuck = adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route);
325482
+ const reasonText = String(toolArgs.summary || "").replace(/\s+/g, " ").trim().slice(0, 400);
325483
+ logs2.push(` [finish-escape] accepting finish after ${finishBounce.streak} consecutive no-progress bounce(s)` + (stuck.length > 0 ? `; ${stuck.length} route(s) unresolved: ${stuck.slice(0, 6).join(", ")}${stuck.length > 6 ? " \u2026" : ""}` : ""));
325484
+ if (reasonText)
325485
+ logs2.push(` [finish-escape] agent's stated reason: ${reasonText}`);
325486
+ }
325487
+ if (!forceAcceptFinish && linkFrontierEnabled) {
324707
325488
  const blocking = (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2);
324708
325489
  const budgetRemainingPct = maxIterations > 0 ? (maxIterations - iteration) / maxIterations : 0;
324709
325490
  if (blocking.length > 0 && budgetRemainingPct >= survey_link_frontier_js_1.FINISH_GATE_MIN_BUDGET_PCT && (finishRedirects < survey_link_frontier_js_1.MAX_FINISH_REDIRECTS || finishBouncedThisBatch)) {
@@ -324722,13 +325503,17 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
324722
325503
  reason: `Not done yet - ${blocking.length} route(s) were visible as links but never visited or captured: ${targets}. A survey must map every reachable top-level page. Navigate to each, capture_page + record_transition, then call finish_exploration. If one is genuinely unreachable (requires credentials you don't have, or leaves the app), note that in your summary and move on.`
324723
325504
  })
324724
325505
  });
325506
+ if (!finishStreakCountedThisBatch) {
325507
+ finishBounce = (0, survey_link_frontier_js_1.recordFinishBounce)(finishBounce);
325508
+ finishStreakCountedThisBatch = true;
325509
+ }
324725
325510
  continue;
324726
325511
  }
324727
325512
  if (blocking.length > 0) {
324728
325513
  logs2.push(` finish accepted with ${blocking.length} uncovered discovered route(s): ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(blocking, 10)}`);
324729
325514
  }
324730
325515
  }
324731
- if (adaptiveActive && !adaptiveComplete) {
325516
+ if (!forceAcceptFinish && adaptiveActive && !adaptiveComplete) {
324732
325517
  refreshAdaptiveDiscovery();
324733
325518
  const remainingList = adaptiveDebtRoutes();
324734
325519
  if (remainingList.length > 0 && iteration < adaptiveHardCap) {
@@ -324736,10 +325521,10 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
324736
325521
  if (iteration >= adaptiveDecisionArmedAt) {
324737
325522
  const { directive, keepGoing } = runBudgetController();
324738
325523
  if (keepGoing) {
324739
- reason = directive ?? `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
325524
+ reason = directive ?? `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration. If one is genuinely unreachable, say so in your summary and finish.`;
324740
325525
  }
324741
325526
  } else {
324742
- reason = `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
325527
+ reason = `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration. If one is genuinely unreachable, say so in your summary and finish.`;
324743
325528
  }
324744
325529
  if (reason) {
324745
325530
  toolResults.push({
@@ -324748,6 +325533,10 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
324748
325533
  content: JSON.stringify({ acknowledged: false, keepExploring: true, reason })
324749
325534
  });
324750
325535
  logs2.push(` [adaptive-budget] finish bounced: ${remainingList.length} route(s) still un-captured`);
325536
+ if (!finishStreakCountedThisBatch) {
325537
+ finishBounce = (0, survey_link_frontier_js_1.recordFinishBounce)(finishBounce);
325538
+ finishStreakCountedThisBatch = true;
325539
+ }
324751
325540
  continue;
324752
325541
  }
324753
325542
  }
@@ -324809,7 +325598,7 @@ Exploration complete: ${summary}`);
324809
325598
  continue;
324810
325599
  }
324811
325600
  if (toolName === "fill_credential" || toolName === "fill_credential_form") {
324812
- if (emailVerificationUnavailable) {
325601
+ if (emailVerificationUnavailable || inboxState?.authDead) {
324813
325602
  const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
324814
325603
  if (codeGuard.blocked) {
324815
325604
  logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
@@ -324903,7 +325692,7 @@ Exploration complete: ${summary}`);
324903
325692
  continue;
324904
325693
  }
324905
325694
  }
324906
- if (emailVerificationUnavailable) {
325695
+ if (emailVerificationUnavailable || inboxState?.authDead) {
324907
325696
  const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
324908
325697
  if (codeGuard.blocked) {
324909
325698
  logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
@@ -326318,7 +327107,10 @@ var require_auth_capture_run = __commonJS({
326318
327107
  exports2.getAuthCaptureTimeoutMs = getAuthCaptureTimeoutMs;
326319
327108
  exports2.getAuthCaptureRunRetryMaxAttempts = getAuthCaptureRunRetryMaxAttempts;
326320
327109
  exports2.classifyCaptureMiss = classifyCaptureMiss;
327110
+ exports2.harvestCollapsedAuthProof = harvestCollapsedAuthProof;
327111
+ exports2.selectAuthLandingRoute = selectAuthLandingRoute;
326321
327112
  exports2.harvestAuthProofLive = harvestAuthProofLive;
327113
+ exports2.detectRedirectBackCollapse = detectRedirectBackCollapse;
326322
327114
  exports2.findCredentialLiteralLeaks = findCredentialLiteralLeaks;
326323
327115
  exports2.runLoginCaptureRunWithOutcome = runLoginCaptureRunWithOutcome;
326324
327116
  exports2.runLoginCaptureRun = runLoginCaptureRun;
@@ -326439,35 +327231,26 @@ ${keyLines || "- (none provided)"}
326439
327231
  return false;
326440
327232
  return true;
326441
327233
  }
326442
- function harvestAuthProofLive(siteMap, redirectRoute, loginRoute, credentials, logs2) {
326443
- const LOGOUT_RE = /\b(log\s?out|sign\s?out)\b/i;
326444
- const ACCOUNT_RE = /\b(account|profile|settings|avatar|notifications?|dashboard|inbox|my\s+account|user\s+menu)\b/i;
326445
- const GREETING_RE = /\b(welcome|hello|hey|good\s+(morning|afternoon|evening|day))\b/i;
326446
- const AUTH_ROUTE_RE = /\/(log[\s_-]?in|sign[\s_-]?in|sign[\s_-]?up|register|auth|forgot|reset)\b/i;
326447
- const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
326448
- const authedPages = siteMap.pages.filter((p) => (p.requiresAuth || redirectRoute !== null && p.route === redirectRoute) && !isLoginPage(p.route));
326449
- const landing = (redirectRoute !== null ? authedPages.find((p) => p.route === redirectRoute) : void 0) ?? authedPages[0] ?? null;
326450
- if (!landing) {
326451
- logs2(" \u26A0 auth-proof: no authenticated landing page captured \u2014 cannot derive a durable proof");
326452
- return null;
326453
- }
326454
- const loginRoleNames = /* @__PURE__ */ new Set();
326455
- const loginTestIds = /* @__PURE__ */ new Set();
326456
- for (const p of siteMap.pages) {
326457
- if (!isLoginPage(p.route) || p.route === landing.route)
326458
- continue;
326459
- for (const el of p.elements ?? []) {
326460
- const nm = (el.label ?? "").trim().toLowerCase();
326461
- if (el.role && nm)
326462
- loginRoleNames.add(`${el.role}|${nm}`);
326463
- if (el.testId)
326464
- loginTestIds.add(el.testId);
326465
- }
326466
- }
327234
+ var LOGOUT_RE = /\b(log\s?out|sign\s?out)\b/i;
327235
+ var ACCOUNT_RE = /\b(account|profile|settings|avatar|notifications?|dashboard|inbox|my\s+account|user\s+menu)\b/i;
327236
+ var GREETING_RE = /\b(welcome|hello|hey|good\s+(morning|afternoon|evening|day))\b/i;
327237
+ function describeAuthProof(proof) {
327238
+ switch (proof.kind) {
327239
+ case "role":
327240
+ return `role:${proof.role}${proof.name ? `(${proof.name})` : ""}`;
327241
+ case "testid":
327242
+ return `testid:${proof.testId}`;
327243
+ case "endpoint":
327244
+ return `endpoint:${proof.url}`;
327245
+ case "url":
327246
+ return `url:${proof.route}`;
327247
+ }
327248
+ }
327249
+ function rankDurableAuthProof(landingElements, loginRoleNames, loginTestIds, credentials, logs2) {
326467
327250
  const haveLoginDiff = loginRoleNames.size > 0 || loginTestIds.size > 0;
326468
327251
  const roleNameFreq = /* @__PURE__ */ new Map();
326469
327252
  const testIdFreq = /* @__PURE__ */ new Map();
326470
- for (const el of landing.elements ?? []) {
327253
+ for (const el of landingElements) {
326471
327254
  const nm = (el.label ?? "").trim().toLowerCase();
326472
327255
  if (el.role && nm)
326473
327256
  roleNameFreq.set(`${el.role}|${nm}`, (roleNameFreq.get(`${el.role}|${nm}`) ?? 0) + 1);
@@ -326507,7 +327290,7 @@ ${keyLines || "- (none provided)"}
326507
327290
  return true;
326508
327291
  };
326509
327292
  const ranked = [];
326510
- for (const el of landing.elements ?? []) {
327293
+ for (const el of landingElements) {
326511
327294
  const label = (el.label ?? "").trim();
326512
327295
  const role = el.role ?? "";
326513
327296
  const testId = el.testId ?? "";
@@ -326519,7 +327302,7 @@ ${keyLines || "- (none provided)"}
326519
327302
  const r = role === "link" ? "link" : "button";
326520
327303
  ranked.push({ tier: 1, proof: { kind: "role", role: r, name: label }, desc: `${r} "${label}" (logout control)` });
326521
327304
  }
326522
- if (label && ACCOUNT_RE.test(label) && !personalized && uniqueRoleName && (role === "button" || role === "link") && absentOnLogin(roleNameKey, "")) {
327305
+ if (label && ACCOUNT_RE.test(label) && !personalized && uniqueRoleName && haveLoginDiff && (role === "button" || role === "link") && absentOnLogin(roleNameKey, "")) {
326523
327306
  ranked.push({ tier: 2, proof: { kind: "role", role, name: label }, desc: `${role} "${label}" (account control)` });
326524
327307
  }
326525
327308
  if (testId && isSemanticTestId(testId) && uniqueTestId && haveLoginDiff && absentOnLogin("", testId)) {
@@ -326533,25 +327316,110 @@ ${keyLines || "- (none provided)"}
326533
327316
  }
326534
327317
  }
326535
327318
  if (ranked.length === 0) {
326536
- const loginPathname = loginRoute ? loginRoute.replace(/[?#].*$/, "").replace(/\/+$/, "") : null;
326537
- const toProofPathname = (r) => {
326538
- if (!r)
326539
- return null;
326540
- const p = r.replace(/[?#].*$/, "").replace(/\/+$/, "");
326541
- return p && p !== "/" && !isLoginPage(p) && p !== loginPathname ? p : null;
326542
- };
326543
- const urlRoute = toProofPathname(redirectRoute) ?? toProofPathname(landing.route);
326544
- if (urlRoute) {
326545
- logs2(` \u2713 url auth-proof: authenticated landing route ${urlRoute} (no durable element proof on the default-state page)`);
326546
- return { kind: "url", route: urlRoute };
326547
- }
326548
- logs2(" \u26A0 auth-proof: no durable element proof and no concrete authed landing route \u2014 falling back to model-authored login (no blind navigation default)");
327319
+ logs2(" \u26A0 auth-proof: no durable login-absent element proof on the authenticated page (no url fallback \u2014 coverage deferred to a positive proof)");
326549
327320
  return null;
326550
327321
  }
326551
327322
  ranked.sort((a, b) => a.tier - b.tier);
326552
327323
  logs2(` \u2713 live auth-proof: ${ranked[0].desc}`);
326553
327324
  return ranked[0].proof;
326554
327325
  }
327326
+ function harvestCollapsedAuthProof(landingElements, loginAbsentElements, credentials, logs2) {
327327
+ const loginRoleNames = /* @__PURE__ */ new Set();
327328
+ const loginTestIds = /* @__PURE__ */ new Set();
327329
+ for (const el of loginAbsentElements) {
327330
+ const nm = (el.label ?? "").trim().toLowerCase();
327331
+ if (el.role && nm)
327332
+ loginRoleNames.add(`${el.role}|${nm}`);
327333
+ if (el.testId)
327334
+ loginTestIds.add(el.testId);
327335
+ }
327336
+ return rankDurableAuthProof(landingElements, loginRoleNames, loginTestIds, credentials, logs2);
327337
+ }
327338
+ function pickAuthLandingPage(siteMap, redirectRoute, loginRoute) {
327339
+ const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
327340
+ const authedPages = siteMap.pages.filter((p) => (p.requiresAuth || redirectRoute !== null && p.route === redirectRoute) && !isLoginPage(p.route));
327341
+ return (redirectRoute !== null ? authedPages.find((p) => p.route === redirectRoute) : void 0) ?? authedPages[0] ?? null;
327342
+ }
327343
+ function selectAuthLandingRoute(siteMap, redirectRoute, loginRoute) {
327344
+ return pickAuthLandingPage(siteMap, redirectRoute, loginRoute)?.route ?? null;
327345
+ }
327346
+ function harvestAuthProofLive(siteMap, redirectRoute, loginRoute, credentials, logs2) {
327347
+ const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
327348
+ const landing = pickAuthLandingPage(siteMap, redirectRoute, loginRoute);
327349
+ if (!landing) {
327350
+ logs2(" \u26A0 auth-proof: no authenticated landing page captured \u2014 cannot derive a durable proof");
327351
+ return null;
327352
+ }
327353
+ const loginRoleNames = /* @__PURE__ */ new Set();
327354
+ const loginTestIds = /* @__PURE__ */ new Set();
327355
+ for (const p of siteMap.pages) {
327356
+ if (!isLoginPage(p.route) || p.route === landing.route)
327357
+ continue;
327358
+ for (const el of p.elements ?? []) {
327359
+ const nm = (el.label ?? "").trim().toLowerCase();
327360
+ if (el.role && nm)
327361
+ loginRoleNames.add(`${el.role}|${nm}`);
327362
+ if (el.testId)
327363
+ loginTestIds.add(el.testId);
327364
+ }
327365
+ }
327366
+ return rankDurableAuthProof(landing.elements ?? [], loginRoleNames, loginTestIds, credentials, logs2);
327367
+ }
327368
+ var AUTH_ROUTE_RE = /\/(log[\s_-]?in|sign[\s_-]?in|sign[\s_-]?up|register|auth|forgot|reset)\b/i;
327369
+ var AUTO_NAV_TRIGGERS = /* @__PURE__ */ new Set(["AUTO", "REDIRECT"]);
327370
+ var USER_ACTION_TRIGGERS = /* @__PURE__ */ new Set(["CLICK", "SUBMIT", "NAVIGATION"]);
327371
+ var stripToBarePath = (route2) => {
327372
+ if (!route2)
327373
+ return null;
327374
+ let path = route2;
327375
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(route2)) {
327376
+ try {
327377
+ path = new URL(route2).pathname;
327378
+ } catch {
327379
+ }
327380
+ }
327381
+ const p = path.replace(/[?#].*$/, "").replace(/\/+$/, "");
327382
+ return p || "/";
327383
+ };
327384
+ function detectRedirectBackCollapse(baseUrl, transitions) {
327385
+ let basePath;
327386
+ try {
327387
+ basePath = stripToBarePath(new URL(baseUrl).pathname);
327388
+ } catch {
327389
+ return null;
327390
+ }
327391
+ if (!basePath || basePath === "/" || AUTH_ROUTE_RE.test(basePath))
327392
+ return null;
327393
+ const userActionEdges = new Set(transitions.filter((t) => USER_ACTION_TRIGGERS.has(t.triggerType)).map((t) => `${stripToBarePath(t.fromRoute) ?? ""} ${stripToBarePath(t.toRoute) ?? ""}`));
327394
+ const bounceIdx = transitions.findIndex((t) => AUTO_NAV_TRIGGERS.has(t.triggerType) && stripToBarePath(t.fromRoute) === basePath && AUTH_ROUTE_RE.test(stripToBarePath(t.toRoute) ?? "") && !userActionEdges.has(`${basePath} ${stripToBarePath(t.toRoute) ?? ""}`));
327395
+ if (bounceIdx < 0)
327396
+ return null;
327397
+ const authRoute = stripToBarePath(transitions[bounceIdx].toRoute);
327398
+ if (!authRoute)
327399
+ return null;
327400
+ const returned = transitions.slice(bounceIdx + 1).some((t) => AUTH_ROUTE_RE.test(stripToBarePath(t.fromRoute) ?? "") && stripToBarePath(t.toRoute) === basePath);
327401
+ if (!returned)
327402
+ return null;
327403
+ return { basePath, authRoute };
327404
+ }
327405
+ function pickRouteElements(routeElements, wantRoute) {
327406
+ const want = stripToBarePath(wantRoute);
327407
+ for (const [route2, els] of routeElements) {
327408
+ if (stripToBarePath(route2) === want && (els?.length ?? 0) > 0)
327409
+ return els;
327410
+ }
327411
+ return [];
327412
+ }
327413
+ function pickLoginFormElements(routeElements, authRoute) {
327414
+ const exact = pickRouteElements(routeElements, authRoute);
327415
+ if (exact.length > 0)
327416
+ return exact;
327417
+ for (const [route2, els] of routeElements) {
327418
+ if (AUTH_ROUTE_RE.test(stripToBarePath(route2) ?? "") && (els?.length ?? 0) > 0)
327419
+ return els;
327420
+ }
327421
+ return [];
327422
+ }
326555
327423
  function findCredentialLiteralLeaks(rendered, credentials) {
326556
327424
  const leaked = [];
326557
327425
  for (const [key, value] of Object.entries(credentials)) {
@@ -326564,12 +327432,22 @@ ${keyLines || "- (none provided)"}
326564
327432
  return leaked;
326565
327433
  }
326566
327434
  async function verifyAuthRecipe(recipe, baseUrl, credentials, logs2) {
327435
+ const baselineRoute = recipe.redirectRoute ?? baseUrl;
326567
327436
  const spec = [
326568
327437
  `import { test, expect } from '@playwright/test';`,
326569
327438
  ``,
326570
327439
  (0, auth_recipe_js_1.renderAuthFn)(recipe),
326571
327440
  ``,
326572
327441
  `test('__vqa_auth_verify', async ({ page }) => {`,
327442
+ ` // DIFFERENTIAL BASELINE (logged out): the durable proof must NOT already hold here, else it`,
327443
+ ` // does not discriminate an authenticated session from an anonymous one \u2014 reject rather than`,
327444
+ ` // publish a proof that passes while logged out (the cardinal invariant). The goto is NOT`,
327445
+ ` // .catch-swallowed: a genuine navigation failure must fail the verify (fail-closed) rather than`,
327446
+ ` // leave the baseline running on about:blank where the proof is vacuously "absent". An auth-gated`,
327447
+ ` // route resolves fine (it follows the redirect to /login).`,
327448
+ ` await page.goto(${JSON.stringify(baselineRoute)}, { waitUntil: 'domcontentloaded' });`,
327449
+ ` ${(0, auth_recipe_js_1.renderProofBaselineCheck)(recipe.authProof, " ")}`,
327450
+ ` // AUTHED: real login + the durable proof assertion (inside __vqaLogin).`,
326573
327451
  ` await __vqaLogin(page);`,
326574
327452
  `});`,
326575
327453
  ``
@@ -326671,21 +327549,50 @@ ${keyLines || "- (none provided)"}
326671
327549
  cmds: login.cmds.map((c) => (0, scrub_credentials_js_1.scrubCredentials)(c, credentials)),
326672
327550
  preNavCmds: login.preNavCmds?.map((c) => (0, scrub_credentials_js_1.scrubCredentials)(c, credentials))
326673
327551
  };
326674
- const authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
327552
+ let authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
327553
+ let effectiveLogin = scrubbedLogin;
327554
+ if (authProof && (authProof.kind === "role" || authProof.kind === "testid")) {
327555
+ const landingRoute = selectAuthLandingRoute(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route);
327556
+ if (landingRoute)
327557
+ effectiveLogin = { ...effectiveLogin, redirectRoute: landingRoute };
327558
+ }
326675
327559
  if (!authProof) {
326676
- logs2(" \u26A0 auth unverified (no durable signed-in proof on the authenticated page) \u2014 falling back to model-authored login");
326677
- return { recipe: null, outcome: exploreMiss("verify_failed") };
327560
+ const collapse = detectRedirectBackCollapse(baseUrl, exploreResult.collectedTransitions);
327561
+ if (collapse) {
327562
+ const landingEls = pickRouteElements(handle.routeElements, collapse.basePath);
327563
+ const loginEls = pickLoginFormElements(handle.routeElements, collapse.authRoute);
327564
+ const collapsedProof = harvestCollapsedAuthProof(landingEls, loginEls, credentials, logs2);
327565
+ if (collapsedProof) {
327566
+ logs2(` \u2713 collapsed-entry element auth-proof (redirect-back-to-entry): ${JSON.stringify(collapsedProof)} on ${collapse.basePath} (${landingEls.length} authed / ${loginEls.length} login-form elements differenced)`);
327567
+ authProof = collapsedProof;
327568
+ effectiveLogin = { ...effectiveLogin, redirectRoute: collapse.basePath };
327569
+ } else {
327570
+ logs2(" \u26A0 redirect-back-to-entry collapse detected but no durable authed-only element differenced (no url fallback) \u2014 deferring to model-authored login");
327571
+ }
327572
+ }
326678
327573
  }
326679
- const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(scrubbedLogin, authProof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
326680
- const rendered = (0, auth_recipe_js_1.renderAuthFn)(candidate);
326681
- const leaks = findCredentialLiteralLeaks(rendered, credentials);
326682
- if (leaks.length > 0) {
326683
- logs2(` \u26A0 auth recipe rejected: rendered __vqaLogin contains literal credential value(s) for ${leaks.join(", ")} \u2014 falling back to model-authored login`);
326684
- return { recipe: null, outcome: "credential_leak" };
327574
+ let identityEndpointProof = null;
327575
+ try {
327576
+ const cookies = await handle.page.context().cookies();
327577
+ if (cookies.some((c) => /(next-auth|authjs)\.session-token/.test(c.name))) {
327578
+ identityEndpointProof = { kind: "endpoint", url: "/api/auth/session" };
327579
+ logs2(" \u2139 NextAuth/Auth.js session cookie detected \u2014 trying an identity-endpoint proof (/api/auth/session) before the element proof");
327580
+ }
327581
+ } catch {
326685
327582
  }
326686
- const planLeaks = findCredentialLiteralLeaks(JSON.stringify(candidate.replayPlan ?? []), credentials);
326687
- if (planLeaks.length > 0) {
326688
- logs2(` \u26A0 auth recipe rejected: structured replayPlan contains literal credential value(s) for ${planLeaks.join(", ")} \u2014 falling back to model-authored login`);
327583
+ const proofCandidates = [];
327584
+ if (identityEndpointProof)
327585
+ proofCandidates.push(identityEndpointProof);
327586
+ if (authProof)
327587
+ proofCandidates.push(authProof);
327588
+ if (proofCandidates.length === 0) {
327589
+ logs2(" \u26A0 auth unverified (no positive proof \u2014 no identity endpoint, no login-absent element) \u2014 falling back to model-authored login");
327590
+ return { recipe: null, outcome: exploreMiss("verify_failed") };
327591
+ }
327592
+ const loginText = [...effectiveLogin.cmds, ...effectiveLogin.preNavCmds ?? []].join("\n") + "\n" + JSON.stringify(effectiveLogin.replayPlan ?? []);
327593
+ const loginLeaks = findCredentialLiteralLeaks(loginText, credentials);
327594
+ if (loginLeaks.length > 0) {
327595
+ logs2(` \u26A0 auth recipe rejected: login sequence contains literal credential value(s) for ${loginLeaks.join(", ")} \u2014 falling back to model-authored login`);
326689
327596
  return { recipe: null, outcome: "credential_leak" };
326690
327597
  }
326691
327598
  try {
@@ -326693,14 +327600,25 @@ ${keyLines || "- (none provided)"}
326693
327600
  } catch {
326694
327601
  }
326695
327602
  handle = null;
326696
- const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
326697
- if (!ok) {
326698
- logs2(" \u26A0 auth unverified (live-verify replay failed \u2014 fleet-config drift) \u2014 falling back to model-authored login");
326699
- return { recipe: null, outcome: exploreMiss("verify_failed") };
327603
+ let lastReject = "no candidate verified";
327604
+ for (const proof of proofCandidates) {
327605
+ const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(effectiveLogin, proof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
327606
+ const renderLeaks = findCredentialLiteralLeaks((0, auth_recipe_js_1.renderAuthFn)(candidate), credentials);
327607
+ if (renderLeaks.length > 0) {
327608
+ lastReject = `${describeAuthProof(proof)} render coincidentally matched credential value(s) ${renderLeaks.join(", ")}`;
327609
+ logs2(` \u26A0 auth-proof candidate skipped (${lastReject}) \u2014 trying the next candidate`);
327610
+ continue;
327611
+ }
327612
+ const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
327613
+ if (ok) {
327614
+ logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${describeAuthProof(proof)}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
327615
+ return { recipe: candidate, outcome: "captured" };
327616
+ }
327617
+ lastReject = `${describeAuthProof(proof)} failed the differential verify`;
327618
+ logs2(` \u26A0 auth-proof candidate rejected (${lastReject}) \u2014 trying the next candidate`);
326700
327619
  }
326701
- const proofDesc = authProof.kind === "role" ? `role:${authProof.role}${authProof.name ? `(${authProof.name})` : ""}` : authProof.kind === "testid" ? `testid:${authProof.testId}` : `url:${authProof.route}`;
326702
- logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${proofDesc}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
326703
- return { recipe: candidate, outcome: "captured" };
327620
+ logs2(` \u26A0 auth unverified (all proof candidates failed: ${lastReject}) \u2014 falling back to model-authored login`);
327621
+ return { recipe: null, outcome: exploreMiss("verify_failed") };
326704
327622
  } catch (err) {
326705
327623
  logs2(` \u26A0 auth-capture run failed: ${err instanceof Error ? err.message : String(err)} \u2014 falling back to model-authored login`);
326706
327624
  return { recipe: null, outcome: (0, ai_retry_js_1.isTransientRateError)(err) ? "rate_limited" : "error" };
@@ -333827,8 +334745,13 @@ Respond with ONLY valid JSON:
333827
334745
  if (!(0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && ctx.mode !== "capture")
333828
334746
  return null;
333829
334747
  if (ctx.authRecipe) {
333830
- pushLog(`auth-prologue: reusing persisted login recipe ${ctx.authRecipe.recipeHash} \u2014 skipping capture`);
333831
- return Promise.resolve(ctx.authRecipe);
334748
+ if (ctx.authRecipe.authProof?.kind === "url") {
334749
+ pushLog(`auth-prologue: persisted recipe ${ctx.authRecipe.recipeHash} uses a demoted url proof \u2014 re-capturing with the differential pipeline (legacy invalidation)`);
334750
+ ctx.authRecipe = void 0;
334751
+ } else {
334752
+ pushLog(`auth-prologue: reusing persisted login recipe ${ctx.authRecipe.recipeHash} \u2014 skipping capture`);
334753
+ return Promise.resolve(ctx.authRecipe);
334754
+ }
333832
334755
  }
333833
334756
  const exploreModel = ctx.exploreModel ?? "";
333834
334757
  const authCaptureModel = ctx.authCaptureModel ?? "";
@@ -349375,7 +350298,7 @@ var require_package4 = __commonJS({
349375
350298
  "package.json"(exports2, module2) {
349376
350299
  module2.exports = {
349377
350300
  name: "@validate.qa/runner",
349378
- version: "1.0.55",
350301
+ version: "1.0.57",
349379
350302
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
349380
350303
  bin: {
349381
350304
  "validate-runner": "dist/cli.js"
@@ -350989,6 +351912,17 @@ async function submitCssKeyboardInCurrentContext(sessionPath, css) {
350989
351912
  if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
350990
351913
  throw new Error(`WEBVIEW element not found for css=${css}`);
350991
351914
  }
351915
+ try {
351916
+ const scriptResult = await appiumRequest(`${sessionPath}/execute/sync`, {
351917
+ method: "POST",
351918
+ body: JSON.stringify({
351919
+ script: "var el = arguments[0];var form = el && (el.form || (el.closest ? el.closest('form') : null));if (form && typeof form.requestSubmit === 'function') { form.requestSubmit(); return 'request-submit'; }return 'no-form';",
351920
+ args: [{ ELEMENT: elementId, "element-6066-11e4-a52e-4f735466cecf": elementId }]
351921
+ })
351922
+ });
351923
+ if (readValue(scriptResult) === "request-submit") return;
351924
+ } catch {
351925
+ }
350992
351926
  await appiumRequest(`${sessionPath}/element/${elementId}/click`, {
350993
351927
  method: "POST",
350994
351928
  body: "{}"
@@ -350998,6 +351932,14 @@ async function submitCssKeyboardInCurrentContext(sessionPath, css) {
350998
351932
  body: JSON.stringify({ text: WEBDRIVER_ENTER_KEY, value: [WEBDRIVER_ENTER_KEY] })
350999
351933
  });
351000
351934
  }
351935
+ async function hideKeyboardIfShown(sessionPath) {
351936
+ try {
351937
+ const shown = await appiumRequest(`${sessionPath}/appium/device/is_keyboard_shown`, { method: "GET" });
351938
+ if (readValue(shown) !== true) return;
351939
+ await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, { method: "POST", body: "{}" });
351940
+ } catch {
351941
+ }
351942
+ }
351001
351943
  async function withWebViewContext(sessionPath, webviewName, body, options = {}) {
351002
351944
  const previous = await getAppContext(sessionPath) ?? "NATIVE_APP";
351003
351945
  let bodyCompleted = false;
@@ -355799,6 +356741,7 @@ var MobileBridgeDriver = class {
355799
356741
  () => typeCssInCurrentContext(this.sessionPath, css, value),
355800
356742
  this.webViewEntryOptions(webviewContext)
355801
356743
  );
356744
+ await hideKeyboardIfShown(this.sessionPath);
355802
356745
  });
355803
356746
  }
355804
356747
  async webSubmitKeyboard(webviewContext, css) {
@@ -355809,6 +356752,7 @@ var MobileBridgeDriver = class {
355809
356752
  () => submitCssKeyboardInCurrentContext(this.sessionPath, css),
355810
356753
  this.webViewEntryOptions(webviewContext)
355811
356754
  );
356755
+ await hideKeyboardIfShown(this.sessionPath);
355812
356756
  });
355813
356757
  }
355814
356758
  };
@@ -356248,7 +357192,7 @@ var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bea
356248
357192
  var IS_CLOUD = parseBoolFlag(process.env.RUNNER_CLOUD);
356249
357193
  var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
356250
357194
  var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
356251
- var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.GIT_COMMIT_SHA ?? process.env.SOURCE_VERSION;
357195
+ var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.GIT_COMMIT_SHA ?? process.env.SOURCE_VERSION ?? (RUNNER_VERSION !== "0.0.0" ? `npm-v${RUNNER_VERSION}` : void 0);
356252
357196
  var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
356253
357197
  var DEFAULT_TYPE_LIMITS = {
356254
357198
  discovery: IS_CLOUD ? 2 : 15,
@@ -356899,7 +357843,9 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
356899
357843
  }, assignedDeviceId, mobileLeaseError) {
356900
357844
  const runMode = run2.mode ?? "run";
356901
357845
  const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
356902
- log.info(`Running test: ${run2.testCaseId ?? "auth-setup"} (run ${run2.runId}) [mode: ${runMode === "heal" ? "heal" : runMode === "auth-setup" ? "auth-setup" : RUNNER_MODE}]`);
357846
+ const runLabel = run2.testCaseId ?? run2.testName ?? (runMode === "run" ? "auth-setup" : runMode);
357847
+ const modeLabel = runMode === "run" ? RUNNER_MODE : runMode;
357848
+ log.info(`Running test: ${runLabel} (run ${run2.runId}) [mode: ${modeLabel}]`);
356903
357849
  const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
356904
357850
  sessionId: `${run2.runId}`,
356905
357851
  mode: toLiveBrowserMode(runMode),
@@ -358733,9 +359679,15 @@ ${finalVerifyResult.logs ?? ""}`;
358733
359679
  }
358734
359680
  const reproduceError = reproduce.error;
358735
359681
  const modelOverride = run2.healerModel ?? void 0;
359682
+ const healDeadlineAt = Date.now() + Math.max(6e4, envInt("MOBILE_HEAL_DEADLINE_MS", 20 * 6e4));
358736
359683
  const healerOptions = {
358737
359684
  ...modelOverride ? { model: modelOverride } : {},
359685
+ deadlineAt: healDeadlineAt,
358738
359686
  runNativeTest,
359687
+ // Surface heal liveness: a Phase-1/Phase-2 heal can legitimately run
359688
+ // for many minutes of model calls with zero device traffic, which
359689
+ // previously printed nothing between the start banner and the verdict.
359690
+ onProgress: (message) => log.info(`\u{1FA7A} ${message}`),
358739
359691
  selectorRegistryContext: run2.selectorRegistryContext ?? null,
358740
359692
  runLiveHeal: async (a) => {
358741
359693
  let liveSession = null;
@@ -358778,6 +359730,14 @@ ${finalVerifyResult.logs ?? ""}`;
358778
359730
  }
358779
359731
  return bridgeDriver;
358780
359732
  };
359733
+ if (Date.now() >= healDeadlineAt) {
359734
+ return {
359735
+ passed: false,
359736
+ verdict: "config_issue",
359737
+ healReason: "mobile heal deadline exceeded before opening the live Phase-2 session"
359738
+ };
359739
+ }
359740
+ log.info("\u{1FA7A} Phase 2 (live device) \u2014 opening the live observation session...");
358781
359741
  try {
358782
359742
  await openLiveSession();
358783
359743
  const liveDriver = {
@@ -359623,12 +360583,29 @@ async function startRunner(opts) {
359623
360583
  }
359624
360584
  }, PIDS_WATCHDOG_INTERVAL_MS);
359625
360585
  pidsWatchdog.unref();
360586
+ const deferralLogged = /* @__PURE__ */ new Map();
360587
+ const logDeferredOnce = (runId, line) => {
360588
+ if (deferralLogged.get(runId) === line) return;
360589
+ deferralLogged.set(runId, line);
360590
+ log.dim(line);
360591
+ };
360592
+ let lastPendingSummary = "";
359626
360593
  while (!shuttingDown) {
359627
360594
  try {
359628
360595
  if (activeRuns < MAX_CONCURRENT) {
359629
360596
  const pending = await fetchPendingRuns(serverUrl, headers);
360597
+ {
360598
+ const pendingIds = new Set(pending.map((r) => r.runId));
360599
+ for (const id of [...deferralLogged.keys()]) {
360600
+ if (!pendingIds.has(id)) deferralLogged.delete(id);
360601
+ }
360602
+ }
359630
360603
  if (pending.length > 0) {
359631
- log.info(`${pending.length} pending run(s) found (${activeRuns}/${MAX_CONCURRENT} slots used)`);
360604
+ const pendingSummary = `${pending.length} pending run(s) found (${activeRuns}/${MAX_CONCURRENT} slots used)`;
360605
+ if (pendingSummary !== lastPendingSummary) {
360606
+ lastPendingSummary = pendingSummary;
360607
+ log.info(pendingSummary);
360608
+ }
359632
360609
  const staggerMs = envInt("RUN_STAGGER_MS", DEFAULT_RUN_STAGGER_MS);
359633
360610
  let claimedThisPoll = 0;
359634
360611
  for (const run2 of pending) {
@@ -359641,7 +360618,7 @@ async function startRunner(opts) {
359641
360618
  const runProject = projectKey(run2);
359642
360619
  const projectLimit = projectCapacityFor(runProject, pending);
359643
360620
  if (getActiveForProject(runProject) >= projectLimit) {
359644
- log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
360621
+ logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
359645
360622
  continue;
359646
360623
  }
359647
360624
  const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
@@ -359651,27 +360628,27 @@ async function startRunner(opts) {
359651
360628
  browserPool: browserPool.stats()
359652
360629
  });
359653
360630
  if (!admission.allowed) {
359654
- log.dim(` Deferred ${run2.runId} (${admission.reason})`);
360631
+ logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (${admission.reason})`);
359655
360632
  continue;
359656
360633
  }
359657
360634
  if (enableMobile && run2.framework === "APPIUM" && getBridgeState().recordingActive) {
359658
- log.dim(` Deferred ${run2.runId} (mobile recording session active)`);
360635
+ logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active)`);
359659
360636
  continue;
359660
360637
  }
359661
360638
  const discoveryMedium = runMode === "discovery" ? run2.discoveryContext?.medium : void 0;
359662
360639
  if (discoveryMedium === "IOS_NATIVE" || discoveryMedium === "ANDROID_NATIVE") {
359663
360640
  if (!enableMobile) {
359664
- log.dim(` Left PENDING ${run2.runId} (native mobile discovery needs a runner started with --mobile)`);
360641
+ logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (native mobile discovery needs a runner started with --mobile)`);
359665
360642
  continue;
359666
360643
  }
359667
360644
  const caps = getRunnerCapabilities();
359668
360645
  const hasDeviceCap = discoveryMedium === "IOS_NATIVE" ? caps.ios : caps.android;
359669
360646
  if (!hasDeviceCap) {
359670
- log.dim(` Left PENDING ${run2.runId} (no booted ${discoveryMedium === "IOS_NATIVE" ? "iOS" : "Android"} device for native mobile discovery)`);
360647
+ logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (no booted ${discoveryMedium === "IOS_NATIVE" ? "iOS" : "Android"} device for native mobile discovery)`);
359671
360648
  continue;
359672
360649
  }
359673
360650
  if (getBridgeState().hasActiveSession) {
359674
- log.dim(` Deferred ${run2.runId} (mobile recording session active on shared device)`);
360651
+ logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active on shared device)`);
359675
360652
  continue;
359676
360653
  }
359677
360654
  }
@@ -359716,7 +360693,7 @@ async function startRunner(opts) {
359716
360693
  activeMobileDiscoveries
359717
360694
  });
359718
360695
  if (!decision.claim) {
359719
- log.dim(` ${decision.disposition === "pending" ? "Left PENDING" : "Deferred"} ${run2.runId} (${decision.reason})`);
360696
+ logDeferredOnce(run2.runId, ` ${decision.disposition === "pending" ? "Left PENDING" : "Deferred"} ${run2.runId} (${decision.reason})`);
359720
360697
  continue;
359721
360698
  }
359722
360699
  leasedDeviceId = freeDeviceId;