@validate.qa/runner 1.0.56 → 1.0.58

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 +1255 -535
  2. package/dist/cli.mjs +1255 -535
  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
@@ -3864,8 +3869,9 @@ function inlineLocatorBindings(stmts) {
3864
3869
  function stripLoginScaffolding(code) {
3865
3870
  const out = [];
3866
3871
  let inAuth = false;
3872
+ let inInbox = false;
3867
3873
  for (const ln of code.split("\n")) {
3868
- if (!inAuth && AUTH_START.test(ln)) {
3874
+ if (!inAuth && !inInbox && AUTH_START.test(ln)) {
3869
3875
  inAuth = true;
3870
3876
  continue;
3871
3877
  }
@@ -3874,6 +3880,15 @@ function stripLoginScaffolding(code) {
3874
3880
  inAuth = false;
3875
3881
  continue;
3876
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
+ }
3877
3892
  if (LOGIN_CALL_LINE.test(ln))
3878
3893
  continue;
3879
3894
  out.push(ln);
@@ -4262,7 +4277,7 @@ function validateAssertionStrength(code, opts = {}) {
4262
4277
  signals
4263
4278
  };
4264
4279
  }
4265
- 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;
4266
4281
  var init_assertion_strength = __esm({
4267
4282
  "../shared/dist/assertion-strength.js"() {
4268
4283
  "use strict";
@@ -4312,6 +4327,8 @@ var init_assertion_strength = __esm({
4312
4327
  AUTH_START = /^\s*\/\/\s*>>>\s*VALIDATE_QA_AUTH/;
4313
4328
  AUTH_END = /^\s*\/\/\s*<<<\s*VALIDATE_QA_AUTH/;
4314
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/;
4315
4332
  NEGATED_ASSERT = /\.not\.(toBeVisible|toContainText|toHaveText|toBeChecked)\b|\.toBeHidden\s*\(/;
4316
4333
  TYPED_INPUT = /\.(fill|type|pressSequentially|setInputFiles)\s*\(/;
4317
4334
  TITLE_LINE = /^\s*(?:test|describe)(?:\.\w+)?\s*\(/;
@@ -4354,10 +4371,7 @@ function isEnvironmentalError(run2) {
4354
4371
  return false;
4355
4372
  if (run2.status !== "ERROR")
4356
4373
  return false;
4357
- const err = run2.error;
4358
- if (typeof err !== "string")
4359
- return false;
4360
- return ENVIRONMENTAL_ERROR_PREFIXES.some((p) => err.startsWith(p));
4374
+ return classifyErrorMessage(run2.error) !== null;
4361
4375
  }
4362
4376
  function isCountedFailure(run2) {
4363
4377
  if (!run2)
@@ -4376,9 +4390,10 @@ function environmentalReason(run2) {
4376
4390
  function classifyErrorMessage(error2) {
4377
4391
  if (typeof error2 !== "string")
4378
4392
  return null;
4379
- if (error2.startsWith("RATE_LIMIT_BLOCKED:"))
4393
+ const normalized = error2.replace(/^\s+/, "").replace(/^Error:\s*/i, "");
4394
+ if (normalized.startsWith("RATE_LIMIT_BLOCKED:"))
4380
4395
  return "rate_limit";
4381
- if (error2.startsWith("CONFIG_ISSUE:"))
4396
+ if (normalized.startsWith("CONFIG_ISSUE:"))
4382
4397
  return "config_issue";
4383
4398
  return null;
4384
4399
  }
@@ -5261,6 +5276,12 @@ var init_auth_state_freshness = __esm({
5261
5276
  function detectSkippedApiCoverage(steps) {
5262
5277
  const allSkipped = steps.length > 0 && steps.every((s) => s.status === "skipped");
5263
5278
  if (allSkipped) {
5279
+ if (steps.every((s) => s.skipKind === "fixme")) {
5280
+ return {
5281
+ detected: false,
5282
+ logLine: "All @api tests skipped, but EVERY skip is a deliberate fixme (intentional N/A \u2014 e.g. a UI-only redirect login) \u2014 NOT relabelling CONFIG_ISSUE"
5283
+ };
5284
+ }
5264
5285
  return {
5265
5286
  detected: true,
5266
5287
  error: (
@@ -72600,7 +72621,7 @@ var require_verification_inbox = __commonJS({
72600
72621
  };
72601
72622
  })();
72602
72623
  Object.defineProperty(exports2, "__esModule", { value: true });
72603
- exports2.VerificationInboxReader = void 0;
72624
+ exports2.VerificationInboxReader = exports2.InboxAuthError = void 0;
72604
72625
  exports2.isVerificationInboxConfigured = isVerificationInboxConfigured;
72605
72626
  exports2.describeVerificationInboxConfigGap = describeVerificationInboxConfigGap;
72606
72627
  exports2.loadVerificationInboxConfig = loadVerificationInboxConfig;
@@ -72612,8 +72633,10 @@ var require_verification_inbox = __commonJS({
72612
72633
  exports2.applyPlusTag = applyPlusTag;
72613
72634
  exports2.deriveRegistrationPassword = deriveRegistrationPassword;
72614
72635
  exports2.extractVerificationCode = extractVerificationCode;
72636
+ exports2.extractVerificationCodeDetailed = extractVerificationCodeDetailed;
72615
72637
  exports2.extractVerificationLink = extractVerificationLink;
72616
72638
  exports2.collectMessageUrls = collectMessageUrls;
72639
+ exports2.isInboxAuthFailure = isInboxAuthFailure;
72617
72640
  var node_crypto_1 = require("crypto");
72618
72641
  var DEFAULT_MAILBOX = "INBOX";
72619
72642
  var DEFAULT_GMAIL_SPAM_MAILBOX = "[Gmail]/Spam";
@@ -72786,6 +72809,9 @@ var require_verification_inbox = __commonJS({
72786
72809
  var CODE_TOKEN = new RegExp(`\\b(${CODE_ALT})\\b`, "gi");
72787
72810
  var YEAR_CONTEXT = /(©|\(c\)|copyright|all rights reserved|®)/i;
72788
72811
  function extractVerificationCode(text, html, subject) {
72812
+ return extractVerificationCodeDetailed(text, html, subject).code;
72813
+ }
72814
+ function extractVerificationCodeDetailed(text, html, subject) {
72789
72815
  const subj = collapseSpacedDigits(subject || "");
72790
72816
  const bodies = [text || "", stripHtml(html || "")].map(collapseSpacedDigits);
72791
72817
  const withSubject = [subj, ...bodies];
@@ -72794,7 +72820,7 @@ var require_verification_inbox = __commonJS({
72794
72820
  if (lone) {
72795
72821
  const c = cleanCode(lone[1]);
72796
72822
  if (c)
72797
- return c;
72823
+ return { code: c, confidence: "subject" };
72798
72824
  }
72799
72825
  }
72800
72826
  for (const body of withSubject) {
@@ -72806,26 +72832,28 @@ var require_verification_inbox = __commonJS({
72806
72832
  continue;
72807
72833
  const m = matchBestCode(line);
72808
72834
  if (m)
72809
- return m;
72835
+ return { code: m, confidence: "keyword" };
72810
72836
  }
72811
72837
  const windowed = body.match(new RegExp(`${CODE_KEYWORDS.source}[\\s\\S]{0,40}?\\b(${CODE_ALT})\\b`, "i"));
72812
72838
  if (windowed && windowed[1]) {
72813
72839
  const cleaned = cleanCode(windowed[1]);
72814
72840
  if (cleaned && !looksLikeYearOrDate(cleaned))
72815
- return cleaned;
72841
+ return { code: cleaned, confidence: "keyword" };
72816
72842
  }
72817
72843
  }
72844
+ const distinct = /* @__PURE__ */ new Set();
72818
72845
  for (const body of bodies) {
72819
72846
  if (!body)
72820
72847
  continue;
72821
- const candidates = body.match(/\b[0-9]{4,8}\b/g);
72822
- if (!candidates)
72823
- continue;
72824
- const real = candidates.find((n) => !looksLikeYearOrDate(n));
72825
- if (real)
72826
- return real;
72848
+ for (const n of body.match(/\b[0-9]{4,8}\b/g) ?? []) {
72849
+ if (!looksLikeYearOrDate(n))
72850
+ distinct.add(n);
72851
+ }
72827
72852
  }
72828
- return null;
72853
+ if (distinct.size === 1) {
72854
+ return { code: distinct.values().next().value ?? null, confidence: "fallback" };
72855
+ }
72856
+ return { code: null, confidence: null };
72829
72857
  }
72830
72858
  function matchBestCode(line) {
72831
72859
  const hasYearContext = YEAR_CONTEXT.test(line);
@@ -72947,6 +72975,28 @@ var require_verification_inbox = __commonJS({
72947
72975
  }
72948
72976
  var MAX_WAIT_CEILING_MS = 18e4;
72949
72977
  var MIN_POLL_MS = 2e3;
72978
+ var InboxAuthError = class extends Error {
72979
+ constructor(message) {
72980
+ super(message);
72981
+ this.name = "InboxAuthError";
72982
+ }
72983
+ };
72984
+ exports2.InboxAuthError = InboxAuthError;
72985
+ function isInboxAuthFailure(error2) {
72986
+ if (!error2)
72987
+ return false;
72988
+ if (error2 instanceof InboxAuthError)
72989
+ return true;
72990
+ const e = error2;
72991
+ if (e.authenticationFailed === true)
72992
+ return true;
72993
+ const hay = `${e.responseText ?? ""} ${e.message ?? ""}`;
72994
+ return /\bAUTHENTICATIONFAILED\b|\bInvalid credentials\b/i.test(hay);
72995
+ }
72996
+ function isPlusTagged(address) {
72997
+ const at = address.indexOf("@");
72998
+ return at > 0 && address.slice(0, at).includes("+");
72999
+ }
72950
73000
  var VerificationInboxReader = class {
72951
73001
  config;
72952
73002
  client = null;
@@ -72976,7 +73026,14 @@ var require_verification_inbox = __commonJS({
72976
73026
  });
72977
73027
  client.on("error", () => {
72978
73028
  });
72979
- await client.connect();
73029
+ try {
73030
+ await client.connect();
73031
+ } catch (error2) {
73032
+ if (isInboxAuthFailure(error2)) {
73033
+ throw new InboxAuthError(`IMAP authentication rejected for ${this.config.user} \u2014 the inbox app password is revoked or wrong`);
73034
+ }
73035
+ throw error2;
73036
+ }
72980
73037
  this.client = client;
72981
73038
  return client;
72982
73039
  })();
@@ -73021,7 +73078,10 @@ var require_verification_inbox = __commonJS({
73021
73078
  } finally {
73022
73079
  lock.release();
73023
73080
  }
73024
- } catch {
73081
+ } catch (error2) {
73082
+ if (isInboxAuthFailure(error2)) {
73083
+ throw error2 instanceof InboxAuthError ? error2 : new InboxAuthError(`IMAP authentication rejected for ${this.config.user} \u2014 the inbox app password is revoked or wrong`);
73084
+ }
73025
73085
  return { uid: 0, uidValidity: 0 };
73026
73086
  }
73027
73087
  }
@@ -73072,23 +73132,52 @@ var require_verification_inbox = __commonJS({
73072
73132
  }
73073
73133
  }
73074
73134
  /**
73075
- * Poll until a matching message arrives or the deadline passes. Returns the
73076
- * newest matching message or null on timeout.
73135
+ * Poll until a message with a STRONG verification signal (subject/keyword
73136
+ * code or a positive-scoring link) arrives, or the deadline passes.
73137
+ *
73138
+ * Signal-less mail addressed to the same recipient (a welcome email, a
73139
+ * receipt) is remembered but polled PAST — returning it immediately used to
73140
+ * hand welcome-mail order numbers to the caller as "the code" whenever the
73141
+ * welcome email out-raced the verification email. Only at the deadline is
73142
+ * the newest signal-less message surfaced (better than pretending the
73143
+ * mailbox is empty: the caller can still read it and decide).
73077
73144
  */
73078
73145
  async waitForMessage(opts) {
73079
73146
  const maxWaitMs = Math.min(opts.maxWaitMs ?? 9e4, MAX_WAIT_CEILING_MS);
73080
73147
  const pollIntervalMs = Math.max(opts.pollIntervalMs ?? 5e3, MIN_POLL_MS);
73081
73148
  const deadline = startTime() + maxWaitMs;
73149
+ const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
73150
+ const satisfies = (m) => {
73151
+ if (opts.require === "code")
73152
+ return m.code !== null && m.codeConfidence !== "fallback";
73153
+ if (opts.require === "link")
73154
+ return m.link !== null;
73155
+ return m.hasVerificationSignal;
73156
+ };
73157
+ let weakest = null;
73158
+ const check2 = async () => {
73159
+ if (opts.stats)
73160
+ opts.stats.filteredOut = 0;
73161
+ const messages = await this.fetchMatching({ ...opts, limit }, true);
73162
+ if (!opts.require)
73163
+ return messages[0] ?? null;
73164
+ const strong = messages.find((m) => satisfies(m) && !m.senderMismatch) ?? messages.find(satisfies);
73165
+ if (strong)
73166
+ return strong;
73167
+ if (messages[0])
73168
+ weakest = messages[0];
73169
+ return null;
73170
+ };
73082
73171
  do {
73083
- const latest = await this.getLatest(opts);
73084
- if (latest)
73085
- return latest;
73172
+ const found = await check2();
73173
+ if (found)
73174
+ return found;
73086
73175
  const remaining = deadline - startTime();
73087
73176
  if (remaining <= 0)
73088
73177
  break;
73089
73178
  await sleep(Math.min(pollIntervalMs, remaining));
73090
73179
  } while (startTime() < deadline);
73091
- return this.getLatest(opts);
73180
+ return await check2() ?? weakest;
73092
73181
  }
73093
73182
  async fetchMatching(opts, withBody) {
73094
73183
  const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
@@ -73102,7 +73191,7 @@ var require_verification_inbox = __commonJS({
73102
73191
  throw e;
73103
73192
  }
73104
73193
  }
73105
- results.sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
73194
+ results.sort((a, b) => Number(a.senderMismatch ?? false) - Number(b.senderMismatch ?? false) || Date.parse(b.date) - Date.parse(a.date));
73106
73195
  return results.slice(0, limit);
73107
73196
  }
73108
73197
  async fetchMatchingInMailbox(opts, withBody, mailbox, limit) {
@@ -73110,6 +73199,7 @@ var require_verification_inbox = __commonJS({
73110
73199
  const since = new Date(Math.max(0, opts.sinceMs));
73111
73200
  const lock = await client.getMailboxLock(mailbox);
73112
73201
  const results = [];
73202
+ const taggedRecipient = isPlusTagged(opts.toAddress);
73113
73203
  try {
73114
73204
  try {
73115
73205
  await client.noop();
@@ -73153,17 +73243,23 @@ var require_verification_inbox = __commonJS({
73153
73243
  const receivedMs = (parsed.date ?? msg.envelope?.date ?? /* @__PURE__ */ new Date(0)).getTime();
73154
73244
  if (!useUid && receivedMs < opts.sinceMs - 6e4)
73155
73245
  continue;
73156
- if (opts.fromMatch && !matchField(parsed.from?.text, opts.fromMatch))
73246
+ const filterActive = Boolean(opts.fromMatch || opts.subjectMatch);
73247
+ const filterMatched = (!opts.fromMatch || matchField(parsed.from?.text, opts.fromMatch)) && (!opts.subjectMatch || matchField(parsed.subject, opts.subjectMatch));
73248
+ if (filterActive && !filterMatched && !taggedRecipient) {
73249
+ if (opts.stats)
73250
+ opts.stats.filteredOut += 1;
73157
73251
  continue;
73158
- if (opts.subjectMatch && !matchField(parsed.subject, opts.subjectMatch))
73159
- continue;
73160
- results.push({ uid: msg.uid, parsed, receivedMs });
73252
+ }
73253
+ results.push({ uid: msg.uid, parsed, receivedMs, senderMismatch: filterActive && !filterMatched });
73161
73254
  }
73162
73255
  } finally {
73163
73256
  lock.release();
73164
73257
  }
73165
- results.sort((a, b) => b.receivedMs - a.receivedMs);
73166
- return results.slice(0, limit).map((r) => toMessage(messageId(mailbox, String(r.uid), this.config.mailbox), r.parsed, withBody, mailbox));
73258
+ results.sort((a, b) => Number(a.senderMismatch) - Number(b.senderMismatch) || b.receivedMs - a.receivedMs);
73259
+ return results.slice(0, limit).map((r) => ({
73260
+ ...toMessage(messageId(mailbox, String(r.uid), this.config.mailbox), r.parsed, withBody, mailbox),
73261
+ ...r.senderMismatch ? { senderMismatch: true } : {}
73262
+ }));
73167
73263
  }
73168
73264
  async close() {
73169
73265
  const client = this.client;
@@ -73194,12 +73290,18 @@ var require_verification_inbox = __commonJS({
73194
73290
  const text = parsed.text || "";
73195
73291
  const html = typeof parsed.html === "string" ? parsed.html : "";
73196
73292
  const summary = toSummary({ uid, parsed, mailbox });
73293
+ const { code, confidence } = extractVerificationCodeDetailed(text, html, parsed.subject || "");
73294
+ const link = extractVerificationLink(text, html);
73197
73295
  return {
73198
73296
  ...summary,
73199
73297
  text: withBody ? text : "",
73200
73298
  html: withBody ? html : "",
73201
- code: extractVerificationCode(text, html, parsed.subject || ""),
73202
- link: extractVerificationLink(text, html)
73299
+ code,
73300
+ link,
73301
+ codeConfidence: confidence,
73302
+ // A fallback-confidence code is NOT a signal: it is exactly the shape a
73303
+ // welcome/receipt email produces (one stray number, no keyword context).
73304
+ hasVerificationSignal: code !== null && confidence !== "fallback" || link !== null
73203
73305
  };
73204
73306
  }
73205
73307
  function toSummary(input) {
@@ -73397,7 +73499,11 @@ var require_inbox_tools = __commonJS({
73397
73499
  function err(message) {
73398
73500
  return { content: JSON.stringify({ error: message }), didMutate: false };
73399
73501
  }
73502
+ 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.";
73400
73503
  async function dispatchInboxTool(toolName, toolArgs, ctx) {
73504
+ if (ctx.state.authDead) {
73505
+ return err(`${toolName} skipped: ${INBOX_AUTH_DEAD_GUIDANCE}`);
73506
+ }
73401
73507
  try {
73402
73508
  switch (toolName) {
73403
73509
  case "provision_identity":
@@ -73419,6 +73525,10 @@ var require_inbox_tools = __commonJS({
73419
73525
  }
73420
73526
  } catch (e) {
73421
73527
  const message = e instanceof Error ? e.message : String(e);
73528
+ if ((0, verification_inbox_js_1.isInboxAuthFailure)(e)) {
73529
+ ctx.state.authDead = true;
73530
+ return err(`${toolName} failed: ${INBOX_AUTH_DEAD_GUIDANCE}`);
73531
+ }
73422
73532
  return err(`${toolName} failed: ${message}`);
73423
73533
  }
73424
73534
  }
@@ -73476,6 +73586,7 @@ var require_inbox_tools = __commonJS({
73476
73586
  return notReady;
73477
73587
  const maxWaitSeconds = clampNumber(args.maxWaitSeconds, 90, 5, 180);
73478
73588
  const { toAddress, altAddress } = inboxRecipients(ctx.state);
73589
+ const stats = { filteredOut: 0 };
73479
73590
  const msg = await ctx.reader.waitForMessage({
73480
73591
  toAddress,
73481
73592
  altAddress,
@@ -73484,13 +73595,16 @@ var require_inbox_tools = __commonJS({
73484
73595
  sinceUidValidity: ctx.state.sinceUidValidity,
73485
73596
  fromMatch: asStr(args.senderMatch),
73486
73597
  subjectMatch: asStr(args.subjectMatch),
73487
- maxWaitMs: maxWaitSeconds * 1e3
73598
+ maxWaitMs: maxWaitSeconds * 1e3,
73599
+ stats
73488
73600
  });
73489
73601
  if (!msg) {
73602
+ 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.` : "";
73490
73603
  return ok({
73491
73604
  found: false,
73492
73605
  waitedSeconds: maxWaitSeconds,
73493
- 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.'
73606
+ ...stats.filteredOut > 0 ? { filteredOut: stats.filteredOut } : {},
73607
+ 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.`
73494
73608
  }, ctx.maxResultChars);
73495
73609
  }
73496
73610
  rememberLink(ctx.state, msg);
@@ -73502,6 +73616,8 @@ var require_inbox_tools = __commonJS({
73502
73616
  subject: msg.subject,
73503
73617
  from: msg.from,
73504
73618
  code: msg.code,
73619
+ ...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." } : {},
73620
+ ...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." } : {},
73505
73621
  link,
73506
73622
  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."
73507
73623
  }, ctx.maxResultChars);
@@ -73635,20 +73751,42 @@ var require_inbox_tools = __commonJS({
73635
73751
  return false;
73636
73752
  if (host === "::1" || host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd"))
73637
73753
  return false;
73638
- const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
73639
- if (m) {
73640
- const [a, b] = [Number(m[1]), Number(m[2])];
73641
- if (a === 127 || a === 0 || a === 10)
73642
- return false;
73643
- if (a === 169 && b === 254)
73644
- return false;
73645
- if (a === 192 && b === 168)
73646
- return false;
73647
- if (a === 172 && b >= 16 && b <= 31)
73754
+ if (host.includes(":")) {
73755
+ if (host === "::" || host === "::ffff:0:0")
73648
73756
  return false;
73649
- if (a >= 224)
73757
+ const dotted = host.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
73758
+ if (dotted && !ipv4IsPublic(dotted[1]))
73650
73759
  return false;
73760
+ const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
73761
+ if (mapped) {
73762
+ const hi = parseInt(mapped[1], 16);
73763
+ const lo = parseInt(mapped[2], 16);
73764
+ const ipv4 = `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
73765
+ if (!ipv4IsPublic(ipv4))
73766
+ return false;
73767
+ }
73651
73768
  }
73769
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) && !ipv4IsPublic(host))
73770
+ return false;
73771
+ return true;
73772
+ }
73773
+ function ipv4IsPublic(ipv4) {
73774
+ const m = ipv4.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
73775
+ if (!m)
73776
+ return true;
73777
+ const [a, b] = [Number(m[1]), Number(m[2])];
73778
+ if (a > 255 || b > 255 || Number(m[3]) > 255 || Number(m[4]) > 255)
73779
+ return false;
73780
+ if (a === 127 || a === 0 || a === 10)
73781
+ return false;
73782
+ if (a === 169 && b === 254)
73783
+ return false;
73784
+ if (a === 192 && b === 168)
73785
+ return false;
73786
+ if (a === 172 && b >= 16 && b <= 31)
73787
+ return false;
73788
+ if (a >= 224)
73789
+ return false;
73652
73790
  return true;
73653
73791
  }
73654
73792
  function clampNumber(v, dflt, min, max) {
@@ -75382,12 +75520,11 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
75382
75520
  keyboardRecoveryScreenId = currentScreenId ?? null;
75383
75521
  keyboardRecoverySameScreenTurns = 0;
75384
75522
  }
75385
- const keyboardLikelyOpen = (0, mobile_keyboard_js_1.hasFocusedEditableElement)(getLatest()?.screen.elements ?? []);
75386
- if (keyboardRecoverySameScreenTurns >= 4 && keyboardLikelyOpen && iteration - lastKeyboardRecoveryIteration > 4) {
75523
+ if (keyboardRecoverySameScreenTurns >= 4 && iteration - lastKeyboardRecoveryIteration > 4) {
75387
75524
  lastKeyboardRecoveryIteration = iteration;
75388
75525
  try {
75389
75526
  await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
75390
- log2(" Auto-dismissed the on-screen keyboard (several turns stalled on this screen with a focused input).");
75527
+ log2(" Auto-dismissed the on-screen keyboard (several turns stalled on this screen).");
75391
75528
  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.");
75392
75529
  } catch (error2) {
75393
75530
  (0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(error2);
@@ -84163,6 +84300,7 @@ ${args.directive}`);
84163
84300
  aiResponseCount += 1;
84164
84301
  const parsed = parseHealVerdict(content);
84165
84302
  if (!parsed) {
84303
+ options.onProgress?.(`Phase 1 attempt ${attempt}: response was not a valid verdict \u2014 granting a recovery turn`);
84166
84304
  markRecoverableTurn();
84167
84305
  pendingDirective = "Your last response was not valid JSON matching the verdict schema. Return exactly one JSON verdict object.";
84168
84306
  continue;
@@ -85717,12 +85855,14 @@ var require_run_callback_proxy = __commonJS({
85717
85855
  }
85718
85856
  const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
85719
85857
  const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
85858
+ const localPaths = new Map(Object.entries(input.localRoutes ?? {}).map(([suffix, handler]) => [`${runPath}/${suffix}`, handler]));
85720
85859
  const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
85721
85860
  const authHeader = `Bearer ${token}`;
85722
85861
  const server3 = (0, node_http_1.createServer)(async (req, res) => {
85723
85862
  try {
85724
85863
  const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
85725
- if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) || requestUrl.search) {
85864
+ const localHandler = localPaths.get(requestUrl.pathname);
85865
+ if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) && !localHandler || requestUrl.search) {
85726
85866
  sendJson(res, 404, { error: "Callback route not available" });
85727
85867
  return;
85728
85868
  }
@@ -85741,6 +85881,11 @@ var require_run_callback_proxy = __commonJS({
85741
85881
  sendJson(res, 400, { error: "Callback body must be a JSON object" });
85742
85882
  return;
85743
85883
  }
85884
+ if (localHandler) {
85885
+ const out = await localHandler(parsedBody);
85886
+ sendJson(res, out.status, out.body);
85887
+ return;
85888
+ }
85744
85889
  const upstreamBody = JSON.stringify({
85745
85890
  ...parsedBody,
85746
85891
  ...input.taskArn ? { taskArn: input.taskArn } : {},
@@ -85799,6 +85944,668 @@ var require_run_callback_proxy = __commonJS({
85799
85944
  }
85800
85945
  });
85801
85946
 
85947
+ // ../runner-core/dist/services/discovery-lint-flags.js
85948
+ var require_discovery_lint_flags = __commonJS({
85949
+ "../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
85950
+ "use strict";
85951
+ Object.defineProperty(exports2, "__esModule", { value: true });
85952
+ exports2.structuralLocatorLintMode = structuralLocatorLintMode;
85953
+ exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
85954
+ exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
85955
+ exports2.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
85956
+ function strictOn(value) {
85957
+ return value === "true" || value === "1";
85958
+ }
85959
+ function structuralLocatorLintMode() {
85960
+ const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
85961
+ if (v === "warn")
85962
+ return "warn";
85963
+ if (v === "true" || v === "1" || v === "enforce")
85964
+ return "enforce";
85965
+ return "off";
85966
+ }
85967
+ function isPwMethodPreflightEnabled() {
85968
+ return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
85969
+ }
85970
+ function isVqaLoginDedupeEnabled() {
85971
+ return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
85972
+ }
85973
+ function isTestgenEmailVerifyEnabled() {
85974
+ return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
85975
+ }
85976
+ }
85977
+ });
85978
+
85979
+ // ../runner-core/dist/services/auth-recipe.js
85980
+ var require_auth_recipe = __commonJS({
85981
+ "../runner-core/dist/services/auth-recipe.js"(exports2) {
85982
+ "use strict";
85983
+ Object.defineProperty(exports2, "__esModule", { value: true });
85984
+ exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = void 0;
85985
+ exports2.computeRecipeHash = computeRecipeHash;
85986
+ exports2.buildAuthRecipe = buildAuthRecipe;
85987
+ exports2.authProofLocator = authProofLocator;
85988
+ exports2.urlProofPredicateExpr = urlProofPredicateExpr;
85989
+ exports2.renderAuthProofAssertion = renderAuthProofAssertion;
85990
+ exports2.renderProofBaselineCheck = renderProofBaselineCheck;
85991
+ exports2.renderAuthFn = renderAuthFn;
85992
+ exports2.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
85993
+ exports2.absoluteRoute = absoluteRoute;
85994
+ exports2.renderReplayClickArrow = renderReplayClickArrow;
85995
+ exports2.renderReplaySettleArrow = renderReplaySettleArrow;
85996
+ exports2.renderAuthUrlWaitLine = renderAuthUrlWaitLine;
85997
+ exports2.commandToReplayStep = commandToReplayStep;
85998
+ exports2.buildReplayPlan = buildReplayPlan;
85999
+ exports2.extractAuthRegion = extractAuthRegion;
86000
+ exports2.stripAuthRegions = stripAuthRegions;
86001
+ exports2.ensureExpectImported = ensureExpectImported;
86002
+ exports2.insertAfterPwImport = insertAfterPwImport;
86003
+ exports2.ensureLoginCall = ensureLoginCall;
86004
+ exports2.upsertAuthBlock = upsertAuthBlock;
86005
+ exports2.buildHealAuthStamp = buildHealAuthStamp;
86006
+ var crypto_1 = require("crypto");
86007
+ var shared_1 = (init_dist(), __toCommonJS(dist_exports));
86008
+ var discovery_lint_flags_js_1 = require_discovery_lint_flags();
86009
+ exports2.AUTH_MARKER_VERSION = "v3";
86010
+ var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
86011
+ var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
86012
+ var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
86013
+ var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
86014
+ function computeRecipeHash(input) {
86015
+ const canonical = JSON.stringify({
86016
+ preNavRoute: input.preNavRoute,
86017
+ preNavCmds: input.preNavCmds,
86018
+ route: input.route,
86019
+ cmds: input.cmds,
86020
+ redirectRoute: input.redirectRoute,
86021
+ authProof: input.authProof
86022
+ });
86023
+ return (0, crypto_1.createHash)("sha256").update(canonical).digest("hex").slice(0, 12);
86024
+ }
86025
+ function buildAuthRecipe(login, authProof, verifiedAt) {
86026
+ const preNavRoute = login.preNavRoute ?? "/";
86027
+ const preNavCmds = login.preNavCmds ?? [];
86028
+ const recipeHash = computeRecipeHash({
86029
+ preNavRoute,
86030
+ preNavCmds,
86031
+ route: login.route,
86032
+ cmds: login.cmds,
86033
+ redirectRoute: login.redirectRoute,
86034
+ authProof
86035
+ });
86036
+ return {
86037
+ version: 1,
86038
+ recipeHash,
86039
+ preNavRoute,
86040
+ preNavCmds,
86041
+ route: login.route,
86042
+ cmds: login.cmds,
86043
+ redirectRoute: login.redirectRoute,
86044
+ authProof,
86045
+ replayPlan: login.replayPlan ?? [],
86046
+ verifiedAt
86047
+ };
86048
+ }
86049
+ function authProofLocator(proof) {
86050
+ if (proof.kind === "url" || proof.kind === "endpoint") {
86051
+ throw new Error(`authProofLocator: ${proof.kind} proof has no element locator; callers must branch on proof.kind`);
86052
+ }
86053
+ if (proof.kind === "testid") {
86054
+ return `page.getByTestId(${JSON.stringify(proof.testId)})`;
86055
+ }
86056
+ if (proof.name) {
86057
+ return `page.getByRole(${JSON.stringify(proof.role)}, { name: ${JSON.stringify(proof.name)} })`;
86058
+ }
86059
+ return `page.getByRole(${JSON.stringify(proof.role)}).first()`;
86060
+ }
86061
+ function stripToPathname(route2) {
86062
+ return route2.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/";
86063
+ }
86064
+ function urlProofPredicateExpr(route2, exact = false) {
86065
+ const p = stripToPathname(route2);
86066
+ if (p === "/")
86067
+ return `() => false`;
86068
+ const r = JSON.stringify(p);
86069
+ if (exact)
86070
+ return `(u) => u.pathname === ${r} || u.pathname === ${r} + '/'`;
86071
+ return `(u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/')`;
86072
+ }
86073
+ function renderStableExactUrlProof(pred, tabs) {
86074
+ return [
86075
+ `await page.waitForURL(${pred}, { timeout: 15000 });`,
86076
+ `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)');`,
86077
+ `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');`
86078
+ ].join(`
86079
+ ${tabs}`);
86080
+ }
86081
+ var ENDPOINT_ORIGIN_EXPR = `(page.url().match(/^https?:\\/\\/[^/]+/) || [''])[0]`;
86082
+ function renderEndpointProbe(url, tabs, mode, originMode) {
86083
+ const u = JSON.stringify(url);
86084
+ const target = originMode === "pageurl" ? `${ENDPOINT_ORIGIN_EXPR} + ${u}` : u;
86085
+ const lines = [
86086
+ `const __vqaResp = await page.request.get(${target});`,
86087
+ `const __vqaJson = __vqaResp.ok() ? await __vqaResp.json().catch(() => null) : null;`,
86088
+ `const __vqaUser = __vqaJson && (__vqaJson.user ?? __vqaJson.data?.user ?? __vqaJson);`,
86089
+ `const __vqaAuthed = !!(__vqaUser && (__vqaUser.id || __vqaUser.email || __vqaUser.sub));`,
86090
+ 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');`
86091
+ ];
86092
+ return lines.join(`
86093
+ ${tabs}`);
86094
+ }
86095
+ function renderAuthProofAssertion(proof) {
86096
+ if (proof.kind === "endpoint") {
86097
+ return renderEndpointProbe(proof.url, " ", "authed", "baseurl");
86098
+ }
86099
+ if (proof.kind === "url") {
86100
+ const pred = urlProofPredicateExpr(proof.route, proof.exact);
86101
+ if (proof.exact)
86102
+ return renderStableExactUrlProof(pred, " ");
86103
+ return `await page.waitForURL(${pred}, { timeout: 15000 });`;
86104
+ }
86105
+ return `await expect(${authProofLocator(proof)}).toBeVisible({ timeout: 15000 });`;
86106
+ }
86107
+ var BASELINE_VISIBLE_TIMEOUT_MS = 15e3;
86108
+ function renderProofBaselineCheck(proof, tabs) {
86109
+ if (proof.kind === "endpoint") {
86110
+ return renderEndpointProbe(proof.url, tabs, "anon", "baseurl");
86111
+ }
86112
+ if (proof.kind === "url") {
86113
+ const pred = urlProofPredicateExpr(proof.route, proof.exact);
86114
+ 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');`;
86115
+ }
86116
+ const loc = authProofLocator(proof);
86117
+ return [
86118
+ `if (await ${loc}.waitFor({ state: 'visible', timeout: ${BASELINE_VISIBLE_TIMEOUT_MS} }).then(() => true, () => false)) {`,
86119
+ ` throw new Error('__vqa auth-baseline: authed-only proof visible while logged out \u2014 not a discriminating proof');`,
86120
+ `}`
86121
+ ].join(`
86122
+ ${tabs}`);
86123
+ }
86124
+ function normalizeCmd(cmd) {
86125
+ let c = cmd.trim();
86126
+ if (c.endsWith(";"))
86127
+ c = c.slice(0, -1);
86128
+ return ` ${c};`;
86129
+ }
86130
+ function renderAuthFn(recipe, opts) {
86131
+ const lines = [];
86132
+ lines.push(START_MARKER);
86133
+ lines.push(`async function __vqaLogin(page) {`);
86134
+ if (opts?.warmGuard) {
86135
+ const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
86136
+ const loginRoute = recipe.route ?? recipe.preNavRoute;
86137
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
86138
+ lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
86139
+ if (recipe.authProof.kind === "endpoint") {
86140
+ lines.push(` try {`);
86141
+ lines.push(` ${renderEndpointProbe(recipe.authProof.url, " ", "authed", "baseurl")}`);
86142
+ lines.push(` return;`);
86143
+ lines.push(` } catch {}`);
86144
+ } else if (recipe.authProof.kind === "url") {
86145
+ const pred = urlProofPredicateExpr(recipe.authProof.route, recipe.authProof.exact);
86146
+ lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
86147
+ lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
86148
+ } else if (loginRoute) {
86149
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
86150
+ } else {
86151
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
86152
+ }
86153
+ lines.push(` }`);
86154
+ }
86155
+ lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
86156
+ for (const cmd of recipe.preNavCmds)
86157
+ lines.push(normalizeCmd(cmd));
86158
+ if (recipe.preNavCmds.length === 0 && recipe.route && recipe.route !== recipe.preNavRoute) {
86159
+ lines.push(` await page.goto(${JSON.stringify(recipe.route)}, { waitUntil: 'domcontentloaded' });`);
86160
+ }
86161
+ for (const cmd of recipe.cmds)
86162
+ lines.push(normalizeCmd(cmd));
86163
+ lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
86164
+ lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
86165
+ if (opts?.warmGuard) {
86166
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
86167
+ 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) {`);
86168
+ lines.push(` try {`);
86169
+ lines.push(` const __vqaState = await page.context().storageState();`);
86170
+ 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 });`);
86171
+ lines.push(` } catch {}`);
86172
+ lines.push(` }`);
86173
+ }
86174
+ lines.push(`}`);
86175
+ lines.push(END_MARKER);
86176
+ return lines.join("\n");
86177
+ }
86178
+ exports2.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
86179
+ "AUTH_FILE",
86180
+ "SHARED_AUTH_INJECT",
86181
+ "VALIDATE_RUN_ID",
86182
+ "VALIDATE_CLAIM_EPOCH",
86183
+ "VALIDATE_AUTH_CONFIG_HASH",
86184
+ "VALIDATE_TASK_ARN",
86185
+ "VALIDATE_RUNNER_AUTH",
86186
+ "VALIDATE_SERVER_URL",
86187
+ // Register-and-verify tests (VALIDATE_QA_INBOX region, inbox-stamp.ts): the
86188
+ // per-run minted registration address + its DERIVED throwaway-app password,
86189
+ // injected by the parent's test-inbox-broker — never project variables.
86190
+ "VQA_TEST_INBOX_EMAIL",
86191
+ "VQA_TEST_ACCOUNT_PASSWORD"
86192
+ ]);
86193
+ function isReservedRunnerEnvVar(key) {
86194
+ return exports2.RESERVED_RUNNER_ENV_VARS.has(key);
86195
+ }
86196
+ function absoluteRoute(route2, baseUrl) {
86197
+ try {
86198
+ return new URL(route2, baseUrl).href;
86199
+ } catch {
86200
+ return route2;
86201
+ }
86202
+ }
86203
+ function renderReplayClickArrow(step) {
86204
+ const loc = step.locator.trim();
86205
+ if (step.kind === "wait") {
86206
+ return `async (page) => {
86207
+ await ${loc}.waitFor({ state: 'visible', timeout: 15000 });
86208
+ }`;
86209
+ }
86210
+ if (step.kind === "press") {
86211
+ return `async (page) => {
86212
+ await ${loc}.press(${JSON.stringify(step.key ?? "Enter")});
86213
+ }`;
86214
+ }
86215
+ return `async (page) => {
86216
+ await ${loc}.click();
86217
+ }`;
86218
+ }
86219
+ function renderReplaySettleArrow(proof) {
86220
+ if (proof.kind === "endpoint") {
86221
+ return `async (page) => {
86222
+ ${renderEndpointProbe(proof.url, " ", "authed", "pageurl")}
86223
+ }`;
86224
+ }
86225
+ if (proof.kind === "url") {
86226
+ const pred = urlProofPredicateExpr(proof.route, proof.exact);
86227
+ const body = proof.exact ? renderStableExactUrlProof(pred, " ") : `await page.waitForURL(${pred}, { timeout: 15000 });`;
86228
+ return `async (page) => {
86229
+ ${body}
86230
+ }`;
86231
+ }
86232
+ return `async (page) => {
86233
+ await ${authProofLocator(proof)}.waitFor({ state: 'visible', timeout: 15000 });
86234
+ }`;
86235
+ }
86236
+ function renderAuthUrlWaitLine(redirectRoute) {
86237
+ const opts = `{ waitUntil: 'domcontentloaded', timeout: 8000 }`;
86238
+ const tail = `.catch(() => { /* URL drift \u2014 the auth-proof assertion below is the gate */ });`;
86239
+ const rr = redirectRoute ? stripToPathname(redirectRoute) : null;
86240
+ if (rr && rr !== "/") {
86241
+ const r = JSON.stringify(rr);
86242
+ return ` await page.waitForURL((u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/'), ${opts})${tail}`;
86243
+ }
86244
+ if (rr === "/") {
86245
+ return ` await page.waitForURL((u) => !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
86246
+ }
86247
+ return ` await page.waitForURL((u) => u.pathname !== '/' && !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
86248
+ }
86249
+ function locatorChainOf(cmd) {
86250
+ const m = cmd.match(/\bpage\.[\s\S]*?(?=\.(?:fill|click|check|press|selectOption|setChecked|waitFor)\s*\()/);
86251
+ return m ? m[0].trim() : null;
86252
+ }
86253
+ function parseRoleName(locator) {
86254
+ const role = locator.match(/getByRole\(\s*['"`]([^'"`]+)['"`]/)?.[1];
86255
+ const name = locator.match(/getByRole\([^)]*name:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByLabel\(\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByPlaceholder\(\s*['"`]([^'"`]+)['"`]/)?.[1];
86256
+ return { role, name };
86257
+ }
86258
+ function credentialKeyOf(cmd) {
86259
+ return cmd.match(/\.fill\(\s*process\.env\.([A-Za-z_$][\w$]*)!?\s*\)/)?.[1] ?? null;
86260
+ }
86261
+ function commandToReplayStep(cmd) {
86262
+ const chain = locatorChainOf(cmd);
86263
+ if (!chain)
86264
+ return null;
86265
+ const credKey = credentialKeyOf(cmd);
86266
+ if (cmd.includes(".fill(")) {
86267
+ if (!credKey)
86268
+ return null;
86269
+ const { role, name } = parseRoleName(chain);
86270
+ return { kind: "fill", credentialKey: credKey, locator: chain, role, name, element: name ?? role ?? "credential field" };
86271
+ }
86272
+ if (/\.waitFor\s*\(/.test(cmd))
86273
+ return { kind: "wait", locator: chain };
86274
+ const pressKey = cmd.match(/\.press\(\s*['"`]([^'"`]+)['"`]\s*\)/)?.[1];
86275
+ if (pressKey)
86276
+ return { kind: "press", locator: chain, key: pressKey };
86277
+ if (/\.(?:click|check|setChecked|selectOption)\s*\(/.test(cmd))
86278
+ return { kind: "click", locator: chain };
86279
+ return null;
86280
+ }
86281
+ function buildReplayPlan(login) {
86282
+ const plan = [];
86283
+ const preNavRoute = login.preNavRoute ?? "/";
86284
+ plan.push({ kind: "nav", route: preNavRoute });
86285
+ for (const c of login.preNavCmds ?? []) {
86286
+ const step = commandToReplayStep(c);
86287
+ if (step)
86288
+ plan.push(step);
86289
+ }
86290
+ if ((login.preNavCmds ?? []).length === 0 && login.route && login.route !== preNavRoute) {
86291
+ plan.push({ kind: "nav", route: login.route });
86292
+ }
86293
+ for (const c of login.cmds) {
86294
+ const step = commandToReplayStep(c);
86295
+ if (step)
86296
+ plan.push(step);
86297
+ }
86298
+ return plan;
86299
+ }
86300
+ function extractAuthRegion(code) {
86301
+ const m = code.match(new RegExp(AUTH_REGION_RE.source));
86302
+ return m ? m[0] : null;
86303
+ }
86304
+ function countAuthRegions(code) {
86305
+ const m = code.match(AUTH_REGION_RE);
86306
+ return m ? m.length : 0;
86307
+ }
86308
+ function stripAuthRegions(code) {
86309
+ return code.replace(AUTH_REGION_RE, "");
86310
+ }
86311
+ var PW_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?/;
86312
+ function ensureExpectImported(code) {
86313
+ const m = code.match(PW_IMPORT_RE);
86314
+ if (!m)
86315
+ return code;
86316
+ const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
86317
+ if (names.includes("expect"))
86318
+ return code;
86319
+ const next = [...names, "expect"];
86320
+ return code.replace(PW_IMPORT_RE, `import { ${next.join(", ")} } from '@playwright/test';`);
86321
+ }
86322
+ function insertAfterPwImport(code, block) {
86323
+ const m = code.match(PW_IMPORT_RE);
86324
+ if (!m || m.index === void 0) {
86325
+ return `${block}
86326
+
86327
+ ${code.replace(/^\r?\n+/, "")}`;
86328
+ }
86329
+ const end = m.index + m[0].length;
86330
+ const head = code.slice(0, end);
86331
+ const tail = code.slice(end).replace(/^\r?\n+/, "");
86332
+ return `${head}
86333
+
86334
+ ${block}
86335
+
86336
+ ${tail}`;
86337
+ }
86338
+ var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
86339
+ var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
86340
+ function ensureLoginCall(code) {
86341
+ if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
86342
+ return code;
86343
+ let result = code;
86344
+ if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
86345
+ result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
86346
+ }
86347
+ const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
86348
+ for (let i = matches.length - 1; i >= 0; i--) {
86349
+ const m = matches[i];
86350
+ if (m.index === void 0)
86351
+ continue;
86352
+ const insertAt = m.index + m[0].length;
86353
+ const following = result.slice(insertAt, insertAt + 60);
86354
+ if (/^\s*await\s+__vqaLogin\s*\(\s*page\s*\)/.test(following))
86355
+ continue;
86356
+ result = `${result.slice(0, insertAt)}
86357
+ await __vqaLogin(page);${result.slice(insertAt)}`;
86358
+ }
86359
+ return result;
86360
+ }
86361
+ function upsertAuthBlock(code, ctx) {
86362
+ if (ctx.mode === "generate") {
86363
+ if (!ctx.recipe || !ctx.isAuthNeeded)
86364
+ return code;
86365
+ let result = stripAuthRegions(code);
86366
+ result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
86367
+ result = ensureExpectImported(result);
86368
+ const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
86369
+ result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
86370
+ result = ensureLoginCall(result);
86371
+ const n = countAuthRegions(result);
86372
+ if (n !== 1) {
86373
+ throw new Error(`upsertAuthBlock(generate): expected exactly 1 auth region after stamp, got ${n}`);
86374
+ }
86375
+ if (!LOGIN_CALL_RE.test(result)) {
86376
+ throw new Error("upsertAuthBlock(generate): stamped __vqaLogin but found no test() opener to inject the call");
86377
+ }
86378
+ return result;
86379
+ }
86380
+ try {
86381
+ if (!ctx.preservedAuthRegion)
86382
+ return code;
86383
+ let result = ensureExpectImported(code);
86384
+ if (countAuthRegions(result) >= 1) {
86385
+ result = result.replace(AUTH_REGION_RE, "");
86386
+ result = insertAfterPwImport(result, ctx.preservedAuthRegion);
86387
+ } else {
86388
+ result = insertAfterPwImport(result, ctx.preservedAuthRegion);
86389
+ }
86390
+ result = ensureLoginCall(result);
86391
+ if (countAuthRegions(result) !== 1) {
86392
+ throw new Error("heal re-stamp produced non-unique auth region");
86393
+ }
86394
+ return result;
86395
+ } catch (err) {
86396
+ const reason = err instanceof Error ? err.message : String(err);
86397
+ ctx.onSignal?.({ type: "auth-stamp-degraded", reason });
86398
+ return code;
86399
+ }
86400
+ }
86401
+ function buildHealAuthStamp(originalCode, failureError, logs2) {
86402
+ const region = extractAuthRegion(originalCode);
86403
+ const loginFailed = !!failureError && /__vqaLogin/.test(failureError);
86404
+ return {
86405
+ mode: "heal",
86406
+ preservedAuthRegion: region && !loginFailed ? region : null,
86407
+ onSignal: logs2 ? (s) => logs2.push(`\u26A0 auth-stamp ${s.type}: ${s.reason}`) : void 0
86408
+ };
86409
+ }
86410
+ }
86411
+ });
86412
+
86413
+ // ../runner-core/dist/services/inbox-stamp.js
86414
+ var require_inbox_stamp = __commonJS({
86415
+ "../runner-core/dist/services/inbox-stamp.js"(exports2) {
86416
+ "use strict";
86417
+ Object.defineProperty(exports2, "__esModule", { value: true });
86418
+ exports2.INBOX_HELPER_TOKEN_RE = exports2.INBOX_HELPER_NAME = exports2.INBOX_REGION_RE = exports2.INBOX_MARKER_VERSION = void 0;
86419
+ exports2.codeUsesInboxHelper = codeUsesInboxHelper;
86420
+ exports2.stripInboxRegions = stripInboxRegions;
86421
+ exports2.renderInboxFn = renderInboxFn;
86422
+ exports2.upsertInboxBlock = upsertInboxBlock;
86423
+ var auth_recipe_js_1 = require_auth_recipe();
86424
+ exports2.INBOX_MARKER_VERSION = "v1";
86425
+ var START_MARKER = `// >>> VALIDATE_QA_INBOX ${exports2.INBOX_MARKER_VERSION} \u2014 generated, do not edit >>>`;
86426
+ var END_MARKER = `// <<< VALIDATE_QA_INBOX ${exports2.INBOX_MARKER_VERSION} <<<`;
86427
+ exports2.INBOX_REGION_RE = /\/\/ >>> VALIDATE_QA_INBOX[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_INBOX[^\n]*\r?\n?/g;
86428
+ exports2.INBOX_HELPER_NAME = "__vqaWaitForEmail";
86429
+ exports2.INBOX_HELPER_TOKEN_RE = /__vqaWaitForEmail\s*\(/;
86430
+ function codeUsesInboxHelper(code) {
86431
+ if (!code)
86432
+ return false;
86433
+ return exports2.INBOX_HELPER_TOKEN_RE.test(code) || /VALIDATE_QA_INBOX/.test(code);
86434
+ }
86435
+ function stripInboxRegions(code) {
86436
+ return code.replace(exports2.INBOX_REGION_RE, "");
86437
+ }
86438
+ function countInboxRegions(code) {
86439
+ const m = code.match(exports2.INBOX_REGION_RE);
86440
+ return m ? m.length : 0;
86441
+ }
86442
+ function renderInboxFn() {
86443
+ const lines = [
86444
+ START_MARKER,
86445
+ `// Reads THIS run's verification email via the runner's inbox broker. The`,
86446
+ `// IMAP credential stays in the runner parent; this helper can only see mail`,
86447
+ `// addressed to the run's own registration address (VQA_TEST_INBOX_EMAIL).`,
86448
+ `// kind: 'code' \u2192 the one-time code, 'link' \u2192 the verification URL,`,
86449
+ `// 'either' / omitted \u2192 { code, link, subject, from } for flexible flows.`,
86450
+ `async function __vqaWaitForEmail(kind, opts) {`,
86451
+ ` const want = kind === 'code' || kind === 'link' ? kind : 'either';`,
86452
+ ` if (!process.env.VQA_TEST_INBOX_EMAIL || !process.env.VALIDATE_SERVER_URL || !process.env.VALIDATE_RUN_ID) {`,
86453
+ ` throw new Error('CONFIG_ISSUE: email verification unavailable - this runner has no verification inbox, so a register-and-verify test cannot run here');`,
86454
+ ` }`,
86455
+ ` const timeoutMs = Math.min(Math.max((opts && opts.timeoutMs) || 150000, 5000), 170000);`,
86456
+ ` const res = await fetch(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + encodeURIComponent(process.env.VALIDATE_RUN_ID) + '/inbox-wait', {`,
86457
+ ` method: 'POST',`,
86458
+ ` headers: { authorization: process.env.VALIDATE_RUNNER_AUTH || '', 'content-type': 'application/json' },`,
86459
+ ` body: JSON.stringify({ kind: want, timeoutMs: timeoutMs }),`,
86460
+ ` });`,
86461
+ ` if (res.status === 404) {`,
86462
+ ` throw new Error('CONFIG_ISSUE: email verification unavailable - the runner inbox broker is not active for this run');`,
86463
+ ` }`,
86464
+ ` let data = null;`,
86465
+ ` try { data = await res.json(); } catch (e) { data = null; }`,
86466
+ ` if (!res.ok || !data) { throw new Error('inbox broker request failed (status ' + res.status + ')'); }`,
86467
+ ` if (!data.found) {`,
86468
+ ` if (data.error) { throw new Error('CONFIG_ISSUE: verification inbox failed on this runner - ' + data.error); }`,
86469
+ ` 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');`,
86470
+ ` }`,
86471
+ ` if (want === 'code' && !data.code) {`,
86472
+ ` throw new Error('VERIFICATION_EMAIL_HAS_NO_CODE: an email arrived (subject: ' + JSON.stringify(data.subject || '') + ') but no one-time code could be extracted');`,
86473
+ ` }`,
86474
+ ` if (want === 'link' && !data.link) {`,
86475
+ ` throw new Error('VERIFICATION_EMAIL_HAS_NO_LINK: an email arrived (subject: ' + JSON.stringify(data.subject || '') + ') but no verification link could be extracted');`,
86476
+ ` }`,
86477
+ ` return want === 'code' ? data.code : want === 'link' ? data.link : data;`,
86478
+ `}`,
86479
+ END_MARKER
86480
+ ];
86481
+ return lines.join("\n");
86482
+ }
86483
+ function upsertInboxBlock(code) {
86484
+ const stripped = stripInboxRegions(code);
86485
+ if (!exports2.INBOX_HELPER_TOKEN_RE.test(stripped))
86486
+ return stripped;
86487
+ const result = (0, auth_recipe_js_1.insertAfterPwImport)(stripped, renderInboxFn());
86488
+ const n = countInboxRegions(result);
86489
+ if (n !== 1) {
86490
+ throw new Error(`upsertInboxBlock: expected exactly 1 inbox region after stamp, got ${n}`);
86491
+ }
86492
+ return result;
86493
+ }
86494
+ }
86495
+ });
86496
+
86497
+ // ../runner-core/dist/services/test-inbox-broker.js
86498
+ var require_test_inbox_broker = __commonJS({
86499
+ "../runner-core/dist/services/test-inbox-broker.js"(exports2) {
86500
+ "use strict";
86501
+ Object.defineProperty(exports2, "__esModule", { value: true });
86502
+ exports2.INBOX_WAIT_ROUTE = exports2.TEST_INBOX_PASSWORD_ENV = exports2.TEST_INBOX_EMAIL_ENV = void 0;
86503
+ exports2.deriveTestRunInboxTag = deriveTestRunInboxTag;
86504
+ exports2.mintTestRunInboxIdentity = mintTestRunInboxIdentity;
86505
+ exports2.decideInboxWaitResponse = decideInboxWaitResponse;
86506
+ exports2.startTestInboxBroker = startTestInboxBroker;
86507
+ var node_crypto_1 = require("crypto");
86508
+ var verification_inbox_js_1 = require_verification_inbox();
86509
+ var inbox_tools_js_1 = require_inbox_tools();
86510
+ exports2.TEST_INBOX_EMAIL_ENV = "VQA_TEST_INBOX_EMAIL";
86511
+ exports2.TEST_INBOX_PASSWORD_ENV = "VQA_TEST_ACCOUNT_PASSWORD";
86512
+ exports2.INBOX_WAIT_ROUTE = "inbox-wait";
86513
+ var MIN_WAIT_MS = 5e3;
86514
+ var MAX_WAIT_MS = 17e4;
86515
+ var DEFAULT_WAIT_MS = 15e4;
86516
+ function deriveTestRunInboxTag(seed, projectId, runId) {
86517
+ const hostKey = (0, verification_inbox_js_1.seedToHostKey)(seed || "");
86518
+ const hash = (0, node_crypto_1.createHash)("sha256").update(`testrun|${hostKey}|${projectId ?? ""}|${runId}`).digest("hex").slice(0, 10);
86519
+ return `vqat${hash}`;
86520
+ }
86521
+ function mintTestRunInboxIdentity(config, input) {
86522
+ const tag = deriveTestRunInboxTag(input.baseUrl, input.projectId, input.runId);
86523
+ const email = (0, verification_inbox_js_1.applyPlusTag)(config.address, tag);
86524
+ return { email, password: (0, verification_inbox_js_1.deriveRegistrationPassword)(email), tag };
86525
+ }
86526
+ function decideInboxWaitResponse(msg, publicLink) {
86527
+ const code = msg && msg.code && msg.codeConfidence !== "fallback" ? msg.code : null;
86528
+ const link = publicLink;
86529
+ if (!msg || !code && !link)
86530
+ return { found: false };
86531
+ return { found: true, code, link, subject: msg.subject, from: msg.from };
86532
+ }
86533
+ async function startTestInboxBroker(opts) {
86534
+ const identity = mintTestRunInboxIdentity(opts.config, opts);
86535
+ const reader = new verification_inbox_js_1.VerificationInboxReader(opts.config);
86536
+ const sinceMs = Date.now();
86537
+ let sinceUid = 0;
86538
+ let sinceUidValidity = 0;
86539
+ let authDeadReason = null;
86540
+ try {
86541
+ const wm = await reader.getUidWatermark();
86542
+ sinceUid = wm.uid;
86543
+ sinceUidValidity = wm.uidValidity;
86544
+ } catch (error2) {
86545
+ if ((0, verification_inbox_js_1.isInboxAuthFailure)(error2)) {
86546
+ authDeadReason = error2 instanceof Error ? error2.message : String(error2);
86547
+ opts.log?.(`\u26A0 Inbox broker: IMAP authentication rejected at watermark probe \u2014 email waits will fail as CONFIG_ISSUE (${authDeadReason})`);
86548
+ } else {
86549
+ opts.log?.("\u26A0 Inbox broker: UID watermark unavailable, falling back to the time floor");
86550
+ }
86551
+ }
86552
+ opts.log?.(`\u{1F4EC} Inbox broker active for ${identity.email} (uid watermark ${sinceUid || "none"})`);
86553
+ let waiting = false;
86554
+ const handleWait = async (body) => {
86555
+ if (authDeadReason) {
86556
+ return { status: 200, body: { found: false, error: "inbox authentication failed (revoked/wrong app password)" } };
86557
+ }
86558
+ if (waiting) {
86559
+ return { status: 200, body: { found: false, error: "another verification wait is already in progress for this run" } };
86560
+ }
86561
+ waiting = true;
86562
+ const rawTimeout = typeof body.timeoutMs === "number" && Number.isFinite(body.timeoutMs) ? body.timeoutMs : DEFAULT_WAIT_MS;
86563
+ const timeoutMs = Math.min(Math.max(rawTimeout, MIN_WAIT_MS), MAX_WAIT_MS);
86564
+ const require2 = body.kind === "code" ? "code" : body.kind === "link" ? "link" : "signal";
86565
+ try {
86566
+ const stats = { filteredOut: 0 };
86567
+ const msg = await reader.waitForMessage({
86568
+ toAddress: identity.email,
86569
+ sinceMs,
86570
+ sinceUid,
86571
+ sinceUidValidity,
86572
+ maxWaitMs: timeoutMs,
86573
+ require: require2,
86574
+ stats
86575
+ });
86576
+ const publicLink = msg && msg.link && (0, inbox_tools_js_1.isPublicHttpUrl)(msg.link) ? msg.link : null;
86577
+ const decision = decideInboxWaitResponse(msg, publicLink);
86578
+ if (!decision.found) {
86579
+ opts.log?.(`\u{1F4ED} Inbox broker: no usable verification email for ${identity.tag} within ${Math.round(timeoutMs / 1e3)}s`);
86580
+ return { status: 200, body: { found: false } };
86581
+ }
86582
+ opts.log?.(`\u{1F4EC} Inbox broker: email for ${identity.tag} (code=${decision.code ? "yes" : "no"}, link=${decision.link ? "yes" : "no"})`);
86583
+ return { status: 200, body: { ...decision } };
86584
+ } catch (error2) {
86585
+ if ((0, verification_inbox_js_1.isInboxAuthFailure)(error2)) {
86586
+ authDeadReason = error2 instanceof Error ? error2.message : String(error2);
86587
+ return { status: 200, body: { found: false, error: "inbox authentication failed (revoked/wrong app password)" } };
86588
+ }
86589
+ const message = error2 instanceof Error ? error2.message : String(error2);
86590
+ opts.log?.(`\u26A0 Inbox broker wait failed: ${message}`);
86591
+ return { status: 200, body: { found: false, error: `inbox read failed: ${message}` } };
86592
+ } finally {
86593
+ waiting = false;
86594
+ }
86595
+ };
86596
+ return {
86597
+ identity,
86598
+ childEnv: () => ({
86599
+ [exports2.TEST_INBOX_EMAIL_ENV]: identity.email,
86600
+ [exports2.TEST_INBOX_PASSWORD_ENV]: identity.password
86601
+ }),
86602
+ handleWait,
86603
+ close: () => reader.close()
86604
+ };
86605
+ }
86606
+ }
86607
+ });
86608
+
85802
86609
  // ../runner-core/dist/services/playwright-native.js
85803
86610
  var require_playwright_native = __commonJS({
85804
86611
  "../runner-core/dist/services/playwright-native.js"(exports2) {
@@ -85856,6 +86663,9 @@ var require_playwright_native = __commonJS({
85856
86663
  var har_stream_js_1 = require_har_stream();
85857
86664
  var api_auth_headers_js_1 = require_api_auth_headers();
85858
86665
  var run_callback_proxy_js_1 = require_run_callback_proxy();
86666
+ var inbox_stamp_js_1 = require_inbox_stamp();
86667
+ var test_inbox_broker_js_1 = require_test_inbox_broker();
86668
+ var verification_inbox_js_1 = require_verification_inbox();
85859
86669
  var api_auth_headers_js_2 = require_api_auth_headers();
85860
86670
  Object.defineProperty(exports2, "parseApiAuthHeaders", { enumerable: true, get: function() {
85861
86671
  return api_auth_headers_js_2.parseApiAuthHeaders;
@@ -85890,17 +86700,20 @@ var require_playwright_native = __commonJS({
85890
86700
  let runDir = null;
85891
86701
  let standingNeedleMatcher = null;
85892
86702
  let callbackProxy;
86703
+ let inboxBroker;
85893
86704
  try {
85894
86705
  runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
85895
86706
  await (0, promises_1.mkdir)(runDir, { recursive: true });
85896
86707
  const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
86708
+ const usesInboxHelper = !isApiTest2 && (0, inbox_stamp_js_1.codeUsesInboxHelper)(playwrightCode);
85897
86709
  const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
85898
86710
  const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
85899
86711
  explicit: extraHttpHeaders,
85900
86712
  standingHeadersMode: effectiveStandingMode,
85901
86713
  apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
85902
86714
  });
85903
- standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : []);
86715
+ const needleValues = resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : [];
86716
+ standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(needleValues);
85904
86717
  const cssDisableSetup = `
85905
86718
  const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
85906
86719
  let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
@@ -86139,7 +86952,7 @@ test.afterEach(async ({ page }, testInfo) => {
86139
86952
  module.exports = {
86140
86953
  testDir: ${JSON.stringify(absTestDir)},
86141
86954
  outputDir: ${JSON.stringify(absOutputDir)},
86142
- timeout: 60000,
86955
+ timeout: ${usesInboxHelper ? 24e4 : 6e4},
86143
86956
  retries: 0,
86144
86957
  use: {
86145
86958
  baseURL: ${JSON.stringify(baseUrl)},
@@ -86165,6 +86978,27 @@ module.exports = {
86165
86978
  const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
86166
86979
  const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
86167
86980
  const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
86981
+ if (usesInboxHelper && validateContext?.runId) {
86982
+ const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
86983
+ if (!inboxConfig) {
86984
+ logs2.push("\u{1F4EA} Test uses __vqaWaitForEmail but this runner has no verification inbox configured \u2014 the helper will fail as CONFIG_ISSUE");
86985
+ } else {
86986
+ try {
86987
+ inboxBroker = await (0, test_inbox_broker_js_1.startTestInboxBroker)({
86988
+ config: inboxConfig,
86989
+ baseUrl,
86990
+ projectId: options.projectId ?? null,
86991
+ runId: validateContext.runId,
86992
+ log: (line) => logs2.push(line)
86993
+ });
86994
+ standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)([...needleValues, inboxBroker.identity.password]);
86995
+ } catch (error3) {
86996
+ logs2.push(`\u26A0 Inbox broker failed to start: ${error3 instanceof Error ? error3.message : String(error3)}`);
86997
+ }
86998
+ }
86999
+ } else if (usesInboxHelper) {
87000
+ logs2.push("\u{1F4EA} Test uses __vqaWaitForEmail but no run callback context is available \u2014 the helper will fail as CONFIG_ISSUE");
87001
+ }
86168
87002
  if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
86169
87003
  try {
86170
87004
  callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
@@ -86173,7 +87007,8 @@ module.exports = {
86173
87007
  upstreamAuthHeader: validateContext.authHeader,
86174
87008
  taskArn: process.env.VALIDATE_TASK_ARN,
86175
87009
  claimEpoch: callbackClaimEpoch,
86176
- authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
87010
+ authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH,
87011
+ ...inboxBroker ? { localRoutes: { [test_inbox_broker_js_1.INBOX_WAIT_ROUTE]: inboxBroker.handleWait } } : {}
86177
87012
  });
86178
87013
  } catch (error3) {
86179
87014
  logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
@@ -86200,14 +87035,19 @@ module.exports = {
86200
87035
  // Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
86201
87036
  // both parent and generated __vqaLogin code; "false" must still cross this process
86202
87037
  // boundary to disable the early publish POST. It remains a per-run toggle.
86203
- ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
87038
+ ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {},
87039
+ // Register-and-verify tests: the run's minted registration identity. An
87040
+ // address + a DERIVED throwaway-app password — never the IMAP secret,
87041
+ // which stays in this parent (test-inbox-broker.ts). Useless without the
87042
+ // loopback proxy the helper reads mail through, hence the guard.
87043
+ ...inboxBroker && callbackProxy ? inboxBroker.childEnv() : {}
86204
87044
  };
86205
87045
  delete childTestVariables.API_AUTH_HEADERS;
86206
87046
  logs2.push("\u{1F680} Spawning playwright test...");
86207
87047
  const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
86208
87048
  let playResult;
86209
87049
  try {
86210
- playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
87050
+ playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv, usesInboxHelper ? 3e5 : void 0);
86211
87051
  } finally {
86212
87052
  await stopLiveBrowserPolling?.();
86213
87053
  }
@@ -86298,6 +87138,8 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
86298
87138
  } finally {
86299
87139
  await callbackProxy?.close().catch(() => {
86300
87140
  });
87141
+ await inboxBroker?.close().catch(() => {
87142
+ });
86301
87143
  if (runDir) {
86302
87144
  await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
86303
87145
  });
@@ -86349,7 +87191,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
86349
87191
  stopped = true;
86350
87192
  };
86351
87193
  }
86352
- function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
87194
+ function runPlaywright(configPath, testVariables, trustedRuntimeEnv, killAfterMs) {
86353
87195
  return new Promise((resolvePromise) => {
86354
87196
  const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
86355
87197
  const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
@@ -86367,11 +87209,13 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
86367
87209
  proc.stderr?.on("data", (chunk) => {
86368
87210
  stderr += chunk.toString();
86369
87211
  });
86370
- const cancelKill = (0, spawn_with_tree_kill_js_1.scheduleTreeKill)(proc, 12e4, {
87212
+ const killMs = killAfterMs ?? 12e4;
87213
+ const cancelKill = (0, spawn_with_tree_kill_js_1.scheduleTreeKill)(proc, killMs, {
86371
87214
  graceMs: 1e4,
86372
87215
  onTimeout: () => {
86373
87216
  timedOut = true;
86374
- stderr += "\n[playwright killed after 120s safety timeout]";
87217
+ stderr += `
87218
+ [playwright killed after ${Math.round(killMs / 1e3)}s safety timeout]`;
86375
87219
  }
86376
87220
  });
86377
87221
  const settle = (result) => {
@@ -90070,402 +90914,6 @@ var require_anchor_harvester = __commonJS({
90070
90914
  }
90071
90915
  });
90072
90916
 
90073
- // ../runner-core/dist/services/discovery-lint-flags.js
90074
- var require_discovery_lint_flags = __commonJS({
90075
- "../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
90076
- "use strict";
90077
- Object.defineProperty(exports2, "__esModule", { value: true });
90078
- exports2.structuralLocatorLintMode = structuralLocatorLintMode;
90079
- exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
90080
- exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
90081
- function strictOn(value) {
90082
- return value === "true" || value === "1";
90083
- }
90084
- function structuralLocatorLintMode() {
90085
- const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
90086
- if (v === "warn")
90087
- return "warn";
90088
- if (v === "true" || v === "1" || v === "enforce")
90089
- return "enforce";
90090
- return "off";
90091
- }
90092
- function isPwMethodPreflightEnabled() {
90093
- return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
90094
- }
90095
- function isVqaLoginDedupeEnabled() {
90096
- return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
90097
- }
90098
- }
90099
- });
90100
-
90101
- // ../runner-core/dist/services/auth-recipe.js
90102
- var require_auth_recipe = __commonJS({
90103
- "../runner-core/dist/services/auth-recipe.js"(exports2) {
90104
- "use strict";
90105
- Object.defineProperty(exports2, "__esModule", { value: true });
90106
- exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = void 0;
90107
- exports2.computeRecipeHash = computeRecipeHash;
90108
- exports2.buildAuthRecipe = buildAuthRecipe;
90109
- exports2.authProofLocator = authProofLocator;
90110
- exports2.urlProofPredicateExpr = urlProofPredicateExpr;
90111
- exports2.renderAuthProofAssertion = renderAuthProofAssertion;
90112
- exports2.renderAuthFn = renderAuthFn;
90113
- exports2.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
90114
- exports2.absoluteRoute = absoluteRoute;
90115
- exports2.renderReplayClickArrow = renderReplayClickArrow;
90116
- exports2.renderReplaySettleArrow = renderReplaySettleArrow;
90117
- exports2.renderAuthUrlWaitLine = renderAuthUrlWaitLine;
90118
- exports2.commandToReplayStep = commandToReplayStep;
90119
- exports2.buildReplayPlan = buildReplayPlan;
90120
- exports2.extractAuthRegion = extractAuthRegion;
90121
- exports2.stripAuthRegions = stripAuthRegions;
90122
- exports2.ensureExpectImported = ensureExpectImported;
90123
- exports2.ensureLoginCall = ensureLoginCall;
90124
- exports2.upsertAuthBlock = upsertAuthBlock;
90125
- exports2.buildHealAuthStamp = buildHealAuthStamp;
90126
- var crypto_1 = require("crypto");
90127
- var shared_1 = (init_dist(), __toCommonJS(dist_exports));
90128
- var discovery_lint_flags_js_1 = require_discovery_lint_flags();
90129
- exports2.AUTH_MARKER_VERSION = "v3";
90130
- var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
90131
- var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
90132
- var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
90133
- var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
90134
- function computeRecipeHash(input) {
90135
- const canonical = JSON.stringify({
90136
- preNavRoute: input.preNavRoute,
90137
- preNavCmds: input.preNavCmds,
90138
- route: input.route,
90139
- cmds: input.cmds,
90140
- redirectRoute: input.redirectRoute,
90141
- authProof: input.authProof
90142
- });
90143
- return (0, crypto_1.createHash)("sha256").update(canonical).digest("hex").slice(0, 12);
90144
- }
90145
- function buildAuthRecipe(login, authProof, verifiedAt) {
90146
- const preNavRoute = login.preNavRoute ?? "/";
90147
- const preNavCmds = login.preNavCmds ?? [];
90148
- const recipeHash = computeRecipeHash({
90149
- preNavRoute,
90150
- preNavCmds,
90151
- route: login.route,
90152
- cmds: login.cmds,
90153
- redirectRoute: login.redirectRoute,
90154
- authProof
90155
- });
90156
- return {
90157
- version: 1,
90158
- recipeHash,
90159
- preNavRoute,
90160
- preNavCmds,
90161
- route: login.route,
90162
- cmds: login.cmds,
90163
- redirectRoute: login.redirectRoute,
90164
- authProof,
90165
- replayPlan: login.replayPlan ?? [],
90166
- verifiedAt
90167
- };
90168
- }
90169
- function authProofLocator(proof) {
90170
- if (proof.kind === "url") {
90171
- throw new Error("authProofLocator: url proof has no element locator; callers must branch on proof.kind");
90172
- }
90173
- if (proof.kind === "testid") {
90174
- return `page.getByTestId(${JSON.stringify(proof.testId)})`;
90175
- }
90176
- if (proof.name) {
90177
- return `page.getByRole(${JSON.stringify(proof.role)}, { name: ${JSON.stringify(proof.name)} })`;
90178
- }
90179
- return `page.getByRole(${JSON.stringify(proof.role)}).first()`;
90180
- }
90181
- function stripToPathname(route2) {
90182
- return route2.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/";
90183
- }
90184
- function urlProofPredicateExpr(route2) {
90185
- const p = stripToPathname(route2);
90186
- if (p === "/")
90187
- return `() => false`;
90188
- const r = JSON.stringify(p);
90189
- return `(u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/')`;
90190
- }
90191
- function renderAuthProofAssertion(proof) {
90192
- if (proof.kind === "url") {
90193
- return `await page.waitForURL(${urlProofPredicateExpr(proof.route)}, { timeout: 15000 });`;
90194
- }
90195
- return `await expect(${authProofLocator(proof)}).toBeVisible({ timeout: 15000 });`;
90196
- }
90197
- function normalizeCmd(cmd) {
90198
- let c = cmd.trim();
90199
- if (c.endsWith(";"))
90200
- c = c.slice(0, -1);
90201
- return ` ${c};`;
90202
- }
90203
- function renderAuthFn(recipe, opts) {
90204
- const lines = [];
90205
- lines.push(START_MARKER);
90206
- lines.push(`async function __vqaLogin(page) {`);
90207
- if (opts?.warmGuard) {
90208
- const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
90209
- const loginRoute = recipe.route ?? recipe.preNavRoute;
90210
- lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
90211
- lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
90212
- if (recipe.authProof.kind === "url") {
90213
- const pred = urlProofPredicateExpr(recipe.authProof.route);
90214
- lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
90215
- lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
90216
- } else if (loginRoute) {
90217
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
90218
- } else {
90219
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
90220
- }
90221
- lines.push(` }`);
90222
- }
90223
- lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
90224
- for (const cmd of recipe.preNavCmds)
90225
- lines.push(normalizeCmd(cmd));
90226
- if (recipe.preNavCmds.length === 0 && recipe.route && recipe.route !== recipe.preNavRoute) {
90227
- lines.push(` await page.goto(${JSON.stringify(recipe.route)}, { waitUntil: 'domcontentloaded' });`);
90228
- }
90229
- for (const cmd of recipe.cmds)
90230
- lines.push(normalizeCmd(cmd));
90231
- lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
90232
- lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
90233
- if (opts?.warmGuard) {
90234
- lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
90235
- 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) {`);
90236
- lines.push(` try {`);
90237
- lines.push(` const __vqaState = await page.context().storageState();`);
90238
- 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 });`);
90239
- lines.push(` } catch {}`);
90240
- lines.push(` }`);
90241
- }
90242
- lines.push(`}`);
90243
- lines.push(END_MARKER);
90244
- return lines.join("\n");
90245
- }
90246
- exports2.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
90247
- "AUTH_FILE",
90248
- "SHARED_AUTH_INJECT",
90249
- "VALIDATE_RUN_ID",
90250
- "VALIDATE_CLAIM_EPOCH",
90251
- "VALIDATE_AUTH_CONFIG_HASH",
90252
- "VALIDATE_TASK_ARN",
90253
- "VALIDATE_RUNNER_AUTH",
90254
- "VALIDATE_SERVER_URL"
90255
- ]);
90256
- function isReservedRunnerEnvVar(key) {
90257
- return exports2.RESERVED_RUNNER_ENV_VARS.has(key);
90258
- }
90259
- function absoluteRoute(route2, baseUrl) {
90260
- try {
90261
- return new URL(route2, baseUrl).href;
90262
- } catch {
90263
- return route2;
90264
- }
90265
- }
90266
- function renderReplayClickArrow(step) {
90267
- const loc = step.locator.trim();
90268
- if (step.kind === "wait") {
90269
- return `async (page) => {
90270
- await ${loc}.waitFor({ state: 'visible', timeout: 15000 });
90271
- }`;
90272
- }
90273
- if (step.kind === "press") {
90274
- return `async (page) => {
90275
- await ${loc}.press(${JSON.stringify(step.key ?? "Enter")});
90276
- }`;
90277
- }
90278
- return `async (page) => {
90279
- await ${loc}.click();
90280
- }`;
90281
- }
90282
- function renderReplaySettleArrow(proof) {
90283
- if (proof.kind === "url") {
90284
- return `async (page) => {
90285
- await page.waitForURL(${urlProofPredicateExpr(proof.route)}, { timeout: 15000 });
90286
- }`;
90287
- }
90288
- return `async (page) => {
90289
- await ${authProofLocator(proof)}.waitFor({ state: 'visible', timeout: 15000 });
90290
- }`;
90291
- }
90292
- function renderAuthUrlWaitLine(redirectRoute) {
90293
- const opts = `{ waitUntil: 'domcontentloaded', timeout: 8000 }`;
90294
- const tail = `.catch(() => { /* URL drift \u2014 the auth-proof assertion below is the gate */ });`;
90295
- const rr = redirectRoute ? stripToPathname(redirectRoute) : null;
90296
- if (rr && rr !== "/") {
90297
- const r = JSON.stringify(rr);
90298
- return ` await page.waitForURL((u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/'), ${opts})${tail}`;
90299
- }
90300
- if (rr === "/") {
90301
- return ` await page.waitForURL((u) => !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
90302
- }
90303
- return ` await page.waitForURL((u) => u.pathname !== '/' && !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
90304
- }
90305
- function locatorChainOf(cmd) {
90306
- const m = cmd.match(/\bpage\.[\s\S]*?(?=\.(?:fill|click|check|press|selectOption|setChecked|waitFor)\s*\()/);
90307
- return m ? m[0].trim() : null;
90308
- }
90309
- function parseRoleName(locator) {
90310
- const role = locator.match(/getByRole\(\s*['"`]([^'"`]+)['"`]/)?.[1];
90311
- const name = locator.match(/getByRole\([^)]*name:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByLabel\(\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByPlaceholder\(\s*['"`]([^'"`]+)['"`]/)?.[1];
90312
- return { role, name };
90313
- }
90314
- function credentialKeyOf(cmd) {
90315
- return cmd.match(/\.fill\(\s*process\.env\.([A-Za-z_$][\w$]*)!?\s*\)/)?.[1] ?? null;
90316
- }
90317
- function commandToReplayStep(cmd) {
90318
- const chain = locatorChainOf(cmd);
90319
- if (!chain)
90320
- return null;
90321
- const credKey = credentialKeyOf(cmd);
90322
- if (cmd.includes(".fill(")) {
90323
- if (!credKey)
90324
- return null;
90325
- const { role, name } = parseRoleName(chain);
90326
- return { kind: "fill", credentialKey: credKey, locator: chain, role, name, element: name ?? role ?? "credential field" };
90327
- }
90328
- if (/\.waitFor\s*\(/.test(cmd))
90329
- return { kind: "wait", locator: chain };
90330
- const pressKey = cmd.match(/\.press\(\s*['"`]([^'"`]+)['"`]\s*\)/)?.[1];
90331
- if (pressKey)
90332
- return { kind: "press", locator: chain, key: pressKey };
90333
- if (/\.(?:click|check|setChecked|selectOption)\s*\(/.test(cmd))
90334
- return { kind: "click", locator: chain };
90335
- return null;
90336
- }
90337
- function buildReplayPlan(login) {
90338
- const plan = [];
90339
- const preNavRoute = login.preNavRoute ?? "/";
90340
- plan.push({ kind: "nav", route: preNavRoute });
90341
- for (const c of login.preNavCmds ?? []) {
90342
- const step = commandToReplayStep(c);
90343
- if (step)
90344
- plan.push(step);
90345
- }
90346
- if ((login.preNavCmds ?? []).length === 0 && login.route && login.route !== preNavRoute) {
90347
- plan.push({ kind: "nav", route: login.route });
90348
- }
90349
- for (const c of login.cmds) {
90350
- const step = commandToReplayStep(c);
90351
- if (step)
90352
- plan.push(step);
90353
- }
90354
- return plan;
90355
- }
90356
- function extractAuthRegion(code) {
90357
- const m = code.match(new RegExp(AUTH_REGION_RE.source));
90358
- return m ? m[0] : null;
90359
- }
90360
- function countAuthRegions(code) {
90361
- const m = code.match(AUTH_REGION_RE);
90362
- return m ? m.length : 0;
90363
- }
90364
- function stripAuthRegions(code) {
90365
- return code.replace(AUTH_REGION_RE, "");
90366
- }
90367
- var PW_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?/;
90368
- function ensureExpectImported(code) {
90369
- const m = code.match(PW_IMPORT_RE);
90370
- if (!m)
90371
- return code;
90372
- const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
90373
- if (names.includes("expect"))
90374
- return code;
90375
- const next = [...names, "expect"];
90376
- return code.replace(PW_IMPORT_RE, `import { ${next.join(", ")} } from '@playwright/test';`);
90377
- }
90378
- function insertAfterPwImport(code, block) {
90379
- const m = code.match(PW_IMPORT_RE);
90380
- if (!m || m.index === void 0) {
90381
- return `${block}
90382
-
90383
- ${code.replace(/^\r?\n+/, "")}`;
90384
- }
90385
- const end = m.index + m[0].length;
90386
- const head = code.slice(0, end);
90387
- const tail = code.slice(end).replace(/^\r?\n+/, "");
90388
- return `${head}
90389
-
90390
- ${block}
90391
-
90392
- ${tail}`;
90393
- }
90394
- var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
90395
- var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
90396
- function ensureLoginCall(code) {
90397
- if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
90398
- return code;
90399
- let result = code;
90400
- if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
90401
- result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
90402
- }
90403
- const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
90404
- for (let i = matches.length - 1; i >= 0; i--) {
90405
- const m = matches[i];
90406
- if (m.index === void 0)
90407
- continue;
90408
- const insertAt = m.index + m[0].length;
90409
- const following = result.slice(insertAt, insertAt + 60);
90410
- if (/^\s*await\s+__vqaLogin\s*\(\s*page\s*\)/.test(following))
90411
- continue;
90412
- result = `${result.slice(0, insertAt)}
90413
- await __vqaLogin(page);${result.slice(insertAt)}`;
90414
- }
90415
- return result;
90416
- }
90417
- function upsertAuthBlock(code, ctx) {
90418
- if (ctx.mode === "generate") {
90419
- if (!ctx.recipe || !ctx.isAuthNeeded)
90420
- return code;
90421
- let result = stripAuthRegions(code);
90422
- result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
90423
- result = ensureExpectImported(result);
90424
- const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
90425
- result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
90426
- result = ensureLoginCall(result);
90427
- const n = countAuthRegions(result);
90428
- if (n !== 1) {
90429
- throw new Error(`upsertAuthBlock(generate): expected exactly 1 auth region after stamp, got ${n}`);
90430
- }
90431
- if (!LOGIN_CALL_RE.test(result)) {
90432
- throw new Error("upsertAuthBlock(generate): stamped __vqaLogin but found no test() opener to inject the call");
90433
- }
90434
- return result;
90435
- }
90436
- try {
90437
- if (!ctx.preservedAuthRegion)
90438
- return code;
90439
- let result = ensureExpectImported(code);
90440
- if (countAuthRegions(result) >= 1) {
90441
- result = result.replace(AUTH_REGION_RE, "");
90442
- result = insertAfterPwImport(result, ctx.preservedAuthRegion);
90443
- } else {
90444
- result = insertAfterPwImport(result, ctx.preservedAuthRegion);
90445
- }
90446
- result = ensureLoginCall(result);
90447
- if (countAuthRegions(result) !== 1) {
90448
- throw new Error("heal re-stamp produced non-unique auth region");
90449
- }
90450
- return result;
90451
- } catch (err) {
90452
- const reason = err instanceof Error ? err.message : String(err);
90453
- ctx.onSignal?.({ type: "auth-stamp-degraded", reason });
90454
- return code;
90455
- }
90456
- }
90457
- function buildHealAuthStamp(originalCode, failureError, logs2) {
90458
- const region = extractAuthRegion(originalCode);
90459
- const loginFailed = !!failureError && /__vqaLogin/.test(failureError);
90460
- return {
90461
- mode: "heal",
90462
- preservedAuthRegion: region && !loginFailed ? region : null,
90463
- onSignal: logs2 ? (s) => logs2.push(`\u26A0 auth-stamp ${s.type}: ${s.reason}`) : void 0
90464
- };
90465
- }
90466
- }
90467
- });
90468
-
90469
90917
  // ../runner-core/dist/services/auth-replay.js
90470
90918
  var require_auth_replay = __commonJS({
90471
90919
  "../runner-core/dist/services/auth-replay.js"(exports2) {
@@ -94342,9 +94790,41 @@ var require_discover_generator = __commonJS({
94342
94790
  var reveal_diff_js_1 = require_reveal_diff();
94343
94791
  var anchor_harvester_js_1 = require_anchor_harvester();
94344
94792
  var auth_recipe_js_1 = require_auth_recipe();
94793
+ var inbox_stamp_js_1 = require_inbox_stamp();
94345
94794
  var auth_replay_js_1 = require_auth_replay();
94346
94795
  var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
94347
94796
  var discovery_lint_flags_js_1 = require_discovery_lint_flags();
94797
+ var inbox_tools_js_1 = require_inbox_tools();
94798
+ var verification_inbox_js_1 = require_verification_inbox();
94799
+ var test_inbox_broker_js_1 = require_test_inbox_broker();
94800
+ 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;
94801
+ function scenarioLooksLikeRegistration(scenario) {
94802
+ const hay = `${scenario.name ?? ""} ${scenario.description ?? ""} ${scenario.entryRoute ?? ""} ${(scenario.targetPages ?? []).join(" ")}`;
94803
+ return REGISTER_SCENARIO_RE.test(hay);
94804
+ }
94805
+ function buildEmailVerifyGenBlock(genEmail, genPassword) {
94806
+ return `
94807
+
94808
+ ## EMAIL VERIFICATION TEST \u2014 register a fresh account and verify it (LIVE, then EMIT)
94809
+ 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.
94810
+
94811
+ LIVE (to prove the flow while generating):
94812
+ - Register on the app's signup form using this throwaway account:
94813
+ - email: \`${genEmail}\`
94814
+ - password: \`${genPassword}\`
94815
+ Type them with browser_type and submit the form ONCE.
94816
+ - 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\`.
94817
+ - Reach the verified / signed-in state and confirm it with a browser_verify_* call, so your final assertion is proven.
94818
+
94819
+ EMIT (the code you put in finish_generate) \u2014 do NOT hardcode the throwaway values above:
94820
+ - 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.
94821
+ - Read the verification email at runtime with the injected helper (it is provided for you \u2014 do NOT define it, do NOT import it):
94822
+ - code flow: \`const code = await __vqaWaitForEmail('code');\` then type \`code\` into the verification field.
94823
+ - link flow: \`const link = await __vqaWaitForEmail('link');\` then \`await page.goto(link);\`.
94824
+ - if unsure: \`const email = await __vqaWaitForEmail();\` returns \`{ code, link, subject, from }\`.
94825
+ - Tag the test \`@email-verify\`. Emit EXACTLY ONE test() in this file (one registration per run).
94826
+ - 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.`;
94827
+ }
94348
94828
  exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
94349
94829
  "browser_install",
94350
94830
  "browser_close",
@@ -95531,6 +96011,11 @@ ${testOpen}`;
95531
96011
  if (options?.authStamp) {
95532
96012
  result = (0, auth_recipe_js_1.upsertAuthBlock)(result, options.authStamp);
95533
96013
  }
96014
+ try {
96015
+ result = (0, inbox_stamp_js_1.upsertInboxBlock)(result);
96016
+ } catch {
96017
+ result = (0, inbox_stamp_js_1.stripInboxRegions)(result);
96018
+ }
95534
96019
  return result;
95535
96020
  }
95536
96021
  function ensureDiscoveredTag(tags) {
@@ -95617,6 +96102,16 @@ ${testOpen}`;
95617
96102
  logs2?.push(` \u2717 Rejected "${t.name}": JS parse error \u2014 ${msg}`);
95618
96103
  return false;
95619
96104
  }
96105
+ {
96106
+ const modelBody = (0, inbox_stamp_js_1.stripInboxRegions)(code);
96107
+ if (inbox_stamp_js_1.INBOX_HELPER_TOKEN_RE.test(modelBody)) {
96108
+ const openerCount = (stripStringsAndComments(modelBody).match(/\btest(?:\.(?:only|skip|fixme))?\s*\(/g) ?? []).length;
96109
+ if (openerCount > 1) {
96110
+ 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`);
96111
+ return false;
96112
+ }
96113
+ }
96114
+ }
95620
96115
  const codeForArgOrderScan = stripStringsAndComments(code);
95621
96116
  if (/test\s*\(\s*,\s*(?:async\s*)?(?:\([^)]*\)|function\s*\([^)]*\))\s*(?:=>)?\s*\{[\s\S]*?\}\s*,\s*\{\s*(?:tags?|annotations)\s*:/.test(codeForArgOrderScan)) {
95622
96117
  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)`);
@@ -96437,6 +96932,23 @@ ${testOpen}`;
96437
96932
  return { offendingLines: offending };
96438
96933
  }
96439
96934
  var ACTION_METHODS = ["click", "dblclick", "tap", "fill", "type", "pressSequentially", "press", "selectOption", "hover", "check", "uncheck", "setInputFiles", "dragTo", "focus", "goto", "waitForURL"];
96935
+ var DYNAMIC_VERIFICATION_ARG = "<verification-dynamic>";
96936
+ function collectVerificationHelperVars(code) {
96937
+ const vars = /* @__PURE__ */ new Set();
96938
+ const assignRe = /(?:(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*await\s+__vqaWaitForEmail\s*\(/g;
96939
+ let m;
96940
+ while ((m = assignRe.exec(code)) !== null)
96941
+ vars.add(m[1]);
96942
+ const destructRe = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*await\s+__vqaWaitForEmail\s*\(/g;
96943
+ while ((m = destructRe.exec(code)) !== null) {
96944
+ for (const part of m[1].split(",")) {
96945
+ const name = part.split(":").pop()?.trim().replace(/\s.*$/, "");
96946
+ if (name && /^[A-Za-z_$][\w$]*$/.test(name))
96947
+ vars.add(name);
96948
+ }
96949
+ }
96950
+ return vars;
96951
+ }
96440
96952
  function extractStringArg(text) {
96441
96953
  const m = text.match(/^\s*(['"`])((?:\\.|(?!\1).)*)\1/);
96442
96954
  if (!m)
@@ -96706,6 +97218,8 @@ ${testOpen}`;
96706
97218
  if (!locatorsMatch(ledger.locator, submitted.locator))
96707
97219
  return false;
96708
97220
  if (ledger.method === "fill" || ledger.method === "type") {
97221
+ if (submitted.arg === DYNAMIC_VERIFICATION_ARG)
97222
+ return true;
96709
97223
  if (ledger.arg === "<env>" || ledger.arg === "<test-value>") {
96710
97224
  return submitted.arg === "<env>" || submitted.arg === "<test-value>";
96711
97225
  }
@@ -96800,6 +97314,25 @@ ${testOpen}`;
96800
97314
  }
96801
97315
  }
96802
97316
  const submittedSigs = extractActionSignatures(playwrightCode, credentials);
97317
+ const helperVars = collectVerificationHelperVars(playwrightCode);
97318
+ if (helperVars.size > 0) {
97319
+ const usesHelperVar = (text) => {
97320
+ for (const v of helperVars) {
97321
+ if (new RegExp(`\\b${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text))
97322
+ return true;
97323
+ }
97324
+ return false;
97325
+ };
97326
+ for (const sub of submittedSigs) {
97327
+ if ((sub.method === "fill" || sub.method === "type") && sub.arg && usesHelperVar(sub.arg)) {
97328
+ sub.arg = DYNAMIC_VERIFICATION_ARG;
97329
+ } else if (sub.method === "goto") {
97330
+ const gotoArg = sub.raw.match(/\.goto\s*\(\s*([^),]+)/);
97331
+ if (gotoArg && usesHelperVar(gotoArg[1]))
97332
+ sub.arg = DYNAMIC_VERIFICATION_ARG;
97333
+ }
97334
+ }
97335
+ }
96803
97336
  const offending = [];
96804
97337
  let firstGotoConsumed = false;
96805
97338
  let cursor = 0;
@@ -96807,6 +97340,8 @@ ${testOpen}`;
96807
97340
  for (const sub of submittedSigs) {
96808
97341
  if (sub.method === "waitForURL")
96809
97342
  continue;
97343
+ if (sub.method === "goto" && sub.arg === DYNAMIC_VERIFICATION_ARG)
97344
+ continue;
96810
97345
  if (sub.method === "goto" && !firstGotoConsumed) {
96811
97346
  firstGotoConsumed = true;
96812
97347
  if (expectedEntry !== null && sub.arg !== expectedEntry) {
@@ -97132,7 +97667,7 @@ expect(secondTitle).toBeTruthy();
97132
97667
  ${tailBlock}`;
97133
97668
  }
97134
97669
  function evaluateAmbiguityGate(finalCode, alreadyAttempted, hasRevisionBudget) {
97135
- const findings = (0, agentic_discovery_prompts_js_1.scanAmbiguousLocators)((0, auth_recipe_js_1.stripAuthRegions)(finalCode));
97670
+ const findings = (0, agentic_discovery_prompts_js_1.scanAmbiguousLocators)((0, auth_recipe_js_1.stripAuthRegions)((0, inbox_stamp_js_1.stripInboxRegions)(finalCode)));
97136
97671
  return { fire: findings.length > 0 && !alreadyAttempted && hasRevisionBudget, findings };
97137
97672
  }
97138
97673
  async function generateTestFromScenario(mcpClient, scenario, siteMap, baseUrl, credentials, aiClient, agentModel, logs2, onAICall, opts) {
@@ -97156,6 +97691,7 @@ ${tailBlock}`;
97156
97691
  }
97157
97692
  }
97158
97693
  }
97694
+ let genInboxReader = null;
97159
97695
  try {
97160
97696
  const startTime = Date.now();
97161
97697
  const groupId = `scenario-gen-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
@@ -97206,12 +97742,28 @@ ${tailBlock}`;
97206
97742
  });
97207
97743
  const baseSkeletonHadRegression = tags.includes("@regression");
97208
97744
  const hasVerifiedRecipe = !!opts?.recipe && tags.includes("@needs-auth");
97209
- const systemPrompt = buildScenarioSystemPrompt(scenario, baseUrl, credentials, tags, siteMap.pages, { appBrief: opts?.appBrief, availableVariableKeys: opts?.availableVariableKeys, hasVerifiedRecipe, knowledgeContext: opts?.knowledgeContext });
97745
+ const projectHasRegisterIntent = credentials?.DISCOVERY_SHOULD_REGISTER === "true";
97746
+ const inboxGenConfig = (0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() && projectHasRegisterIntent && !hasVerifiedRecipe && scenarioLooksLikeRegistration(scenario) ? (0, verification_inbox_js_1.loadVerificationInboxConfig)() : null;
97747
+ let genInboxState = null;
97748
+ let genInboxCredentials = credentials;
97749
+ let genInboxSecret = null;
97750
+ if (inboxGenConfig) {
97751
+ const genTag = `vqag${(0, crypto_1.createHash)("sha256").update(`testgen|${(0, verification_inbox_js_1.seedToHostKey)(baseUrl)}|${groupId}`).digest("hex").slice(0, 10)}`;
97752
+ const genEmail = (0, verification_inbox_js_1.applyPlusTag)(inboxGenConfig.address, genTag);
97753
+ const genPassword = (0, verification_inbox_js_1.deriveRegistrationPassword)(genEmail);
97754
+ genInboxReader = new verification_inbox_js_1.VerificationInboxReader(inboxGenConfig);
97755
+ genInboxState = (0, inbox_tools_js_1.createInboxRunState)(inboxGenConfig, { tag: genTag, passwordKey: test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV });
97756
+ genInboxCredentials = { ...credentials, [test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV]: genEmail, [test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV]: genPassword };
97757
+ genInboxSecret = genPassword;
97758
+ openaiTools.push(...inbox_tools_js_1.INBOX_TOOLS);
97759
+ logs2.push(` \u{1F4EC} Email-verification generation ON for "${scenario.name}" \u2014 registering throwaway ${genEmail}`);
97760
+ }
97761
+ 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]) : "");
97210
97762
  const messages = [
97211
97763
  { role: "system", content: systemPrompt },
97212
97764
  {
97213
97765
  role: "user",
97214
- 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.`
97766
+ 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.`
97215
97767
  }
97216
97768
  ];
97217
97769
  let iteration = 0;
@@ -97339,6 +97891,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97339
97891
  const iterStart = Date.now();
97340
97892
  const requestMessageCount = messages.length;
97341
97893
  const requestMessages = messages.slice();
97894
+ const scrubAudit = (payload) => {
97895
+ if (!genInboxSecret)
97896
+ return payload;
97897
+ try {
97898
+ return JSON.parse(JSON.stringify(payload).split(genInboxSecret).join("[REDACTED:VQA_TEST_ACCOUNT_PASSWORD]"));
97899
+ } catch {
97900
+ return payload;
97901
+ }
97902
+ };
97342
97903
  let response;
97343
97904
  try {
97344
97905
  response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
@@ -97380,7 +97941,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97380
97941
  messages.push(assistantMessage);
97381
97942
  if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
97382
97943
  try {
97383
- onAICall?.({
97944
+ onAICall?.(scrubAudit({
97384
97945
  iteration,
97385
97946
  groupId,
97386
97947
  callType: "scenarioGenerate",
@@ -97394,7 +97955,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97394
97955
  requestMessageCount,
97395
97956
  requestMessages,
97396
97957
  ledger: actionLedger.slice()
97397
- });
97958
+ }));
97398
97959
  } catch {
97399
97960
  }
97400
97961
  logs2.push(` No tool calls \u2014 agent stopped`);
@@ -97421,7 +97982,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97421
97982
  const suspectedBugDeclared = toolArgs.suspectedBug === true;
97422
97983
  const bugDescription = String(toolArgs.bugDescription || "").slice(0, 500).replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/\s+/g, " ").trim();
97423
97984
  const testObj = { name, description, playwrightCode: code, tags };
97424
- const validated = validateTests([testObj], logs2, { sensitiveEnvMap: credentials, authRecipe: opts?.recipe ?? null, basePath: gotoBasePathFromBaseUrl(baseUrl) });
97985
+ const validated = validateTests([testObj], logs2, { sensitiveEnvMap: genInboxCredentials, authRecipe: opts?.recipe ?? null, basePath: gotoBasePathFromBaseUrl(baseUrl) });
97425
97986
  if (validated.length > 0) {
97426
97987
  if (actionLedger.length > lastRenderedLedgerEntryCount && actionLedger.length > 0) {
97427
97988
  const renderedLedger = renderSessionRecording(actionLedger, credentials);
@@ -97473,7 +98034,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
97473
98034
  });
97474
98035
  continue;
97475
98036
  }
97476
- const actionCheck = validateFinalActionFidelity(code, actionLedger, credentials, { entryRoute: scenario.entryRoute });
98037
+ const actionCheck = validateFinalActionFidelity(code, actionLedger, genInboxCredentials, { entryRoute: scenario.entryRoute });
97477
98038
  if (actionCheck.offendingActions.length > 0 && !actionRevisionAttempted) {
97478
98039
  actionRevisionAttempted = true;
97479
98040
  const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
@@ -97786,6 +98347,15 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
97786
98347
  });
97787
98348
  continue;
97788
98349
  }
98350
+ if (genInboxReader && genInboxState && inbox_tools_js_1.INBOX_TOOL_NAMES.has(toolName)) {
98351
+ const inboxResult = await (0, inbox_tools_js_1.dispatchInboxTool)(toolName, toolArgs, {
98352
+ reader: genInboxReader,
98353
+ state: genInboxState,
98354
+ mcpClient
98355
+ });
98356
+ toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: inboxResult.content });
98357
+ continue;
98358
+ }
97789
98359
  let toolResultContent = "";
97790
98360
  try {
97791
98361
  const mcpResult = await mcpClient.callTool({ name: toolName, arguments: toolArgs });
@@ -97948,7 +98518,7 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
97948
98518
  tool: assistantMessage.tool_calls?.find((tc) => tc.id === tr.tool_call_id)?.function.name ?? "unknown",
97949
98519
  result: typeof tr.content === "string" ? tr.content.slice(0, 5e4) : String(tr.content).slice(0, 5e4)
97950
98520
  }));
97951
- onAICall?.({
98521
+ onAICall?.(scrubAudit({
97952
98522
  iteration,
97953
98523
  groupId,
97954
98524
  callType: "scenarioGenerate",
@@ -97964,7 +98534,7 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
97964
98534
  requestMessageCount,
97965
98535
  requestMessages,
97966
98536
  ledger: actionLedger.slice()
97967
- });
98537
+ }));
97968
98538
  } catch {
97969
98539
  }
97970
98540
  messages.push(...toolResults);
@@ -97972,8 +98542,12 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
97972
98542
  if (!result) {
97973
98543
  logs2.push(` Scenario generation did not produce a test after ${iteration} iterations`);
97974
98544
  }
98545
+ await genInboxReader?.close().catch(() => {
98546
+ });
97975
98547
  return result;
97976
98548
  } catch (err) {
98549
+ await genInboxReader?.close().catch(() => {
98550
+ });
97977
98551
  const msg = err instanceof Error ? err.message : String(err);
97978
98552
  logs2.push(` Scenario generation failed: ${msg}`);
97979
98553
  if ((0, ai_retry_js_1.isAIProviderError)(err)) {
@@ -324908,7 +325482,7 @@ ${currentSummary}`;
324908
325482
  const distinctPathsVisited = new Set([...state2.visitedRoutes].map((r) => (0, exploration_parser_js_1.normalizeRouteForFocusScope)(r))).size;
324909
325483
  const fb = (0, survey_link_frontier_js_1.evaluateFinishBounce)(finishBounce, distinctPathsVisited);
324910
325484
  finishBounce = fb.state;
324911
- const forceAcceptFinish = fb.escape;
325485
+ const forceAcceptFinish = fb.escape && !finishStreakCountedThisBatch;
324912
325486
  if (forceAcceptFinish) {
324913
325487
  const stuck = adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route);
324914
325488
  const reasonText = String(toolArgs.summary || "").replace(/\s+/g, " ").trim().slice(0, 400);
@@ -325030,7 +325604,7 @@ Exploration complete: ${summary}`);
325030
325604
  continue;
325031
325605
  }
325032
325606
  if (toolName === "fill_credential" || toolName === "fill_credential_form") {
325033
- if (emailVerificationUnavailable) {
325607
+ if (emailVerificationUnavailable || inboxState?.authDead) {
325034
325608
  const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
325035
325609
  if (codeGuard.blocked) {
325036
325610
  logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
@@ -325124,7 +325698,7 @@ Exploration complete: ${summary}`);
325124
325698
  continue;
325125
325699
  }
325126
325700
  }
325127
- if (emailVerificationUnavailable) {
325701
+ if (emailVerificationUnavailable || inboxState?.authDead) {
325128
325702
  const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
325129
325703
  if (codeGuard.blocked) {
325130
325704
  logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
@@ -326539,7 +327113,10 @@ var require_auth_capture_run = __commonJS({
326539
327113
  exports2.getAuthCaptureTimeoutMs = getAuthCaptureTimeoutMs;
326540
327114
  exports2.getAuthCaptureRunRetryMaxAttempts = getAuthCaptureRunRetryMaxAttempts;
326541
327115
  exports2.classifyCaptureMiss = classifyCaptureMiss;
327116
+ exports2.harvestCollapsedAuthProof = harvestCollapsedAuthProof;
327117
+ exports2.selectAuthLandingRoute = selectAuthLandingRoute;
326542
327118
  exports2.harvestAuthProofLive = harvestAuthProofLive;
327119
+ exports2.detectRedirectBackCollapse = detectRedirectBackCollapse;
326543
327120
  exports2.findCredentialLiteralLeaks = findCredentialLiteralLeaks;
326544
327121
  exports2.runLoginCaptureRunWithOutcome = runLoginCaptureRunWithOutcome;
326545
327122
  exports2.runLoginCaptureRun = runLoginCaptureRun;
@@ -326660,35 +327237,26 @@ ${keyLines || "- (none provided)"}
326660
327237
  return false;
326661
327238
  return true;
326662
327239
  }
326663
- function harvestAuthProofLive(siteMap, redirectRoute, loginRoute, credentials, logs2) {
326664
- const LOGOUT_RE = /\b(log\s?out|sign\s?out)\b/i;
326665
- const ACCOUNT_RE = /\b(account|profile|settings|avatar|notifications?|dashboard|inbox|my\s+account|user\s+menu)\b/i;
326666
- const GREETING_RE = /\b(welcome|hello|hey|good\s+(morning|afternoon|evening|day))\b/i;
326667
- const AUTH_ROUTE_RE = /\/(log[\s_-]?in|sign[\s_-]?in|sign[\s_-]?up|register|auth|forgot|reset)\b/i;
326668
- const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
326669
- const authedPages = siteMap.pages.filter((p) => (p.requiresAuth || redirectRoute !== null && p.route === redirectRoute) && !isLoginPage(p.route));
326670
- const landing = (redirectRoute !== null ? authedPages.find((p) => p.route === redirectRoute) : void 0) ?? authedPages[0] ?? null;
326671
- if (!landing) {
326672
- logs2(" \u26A0 auth-proof: no authenticated landing page captured \u2014 cannot derive a durable proof");
326673
- return null;
326674
- }
326675
- const loginRoleNames = /* @__PURE__ */ new Set();
326676
- const loginTestIds = /* @__PURE__ */ new Set();
326677
- for (const p of siteMap.pages) {
326678
- if (!isLoginPage(p.route) || p.route === landing.route)
326679
- continue;
326680
- for (const el of p.elements ?? []) {
326681
- const nm = (el.label ?? "").trim().toLowerCase();
326682
- if (el.role && nm)
326683
- loginRoleNames.add(`${el.role}|${nm}`);
326684
- if (el.testId)
326685
- loginTestIds.add(el.testId);
326686
- }
326687
- }
327240
+ var LOGOUT_RE = /\b(log\s?out|sign\s?out)\b/i;
327241
+ var ACCOUNT_RE = /\b(account|profile|settings|avatar|notifications?|dashboard|inbox|my\s+account|user\s+menu)\b/i;
327242
+ var GREETING_RE = /\b(welcome|hello|hey|good\s+(morning|afternoon|evening|day))\b/i;
327243
+ function describeAuthProof(proof) {
327244
+ switch (proof.kind) {
327245
+ case "role":
327246
+ return `role:${proof.role}${proof.name ? `(${proof.name})` : ""}`;
327247
+ case "testid":
327248
+ return `testid:${proof.testId}`;
327249
+ case "endpoint":
327250
+ return `endpoint:${proof.url}`;
327251
+ case "url":
327252
+ return `url:${proof.route}`;
327253
+ }
327254
+ }
327255
+ function rankDurableAuthProof(landingElements, loginRoleNames, loginTestIds, credentials, logs2) {
326688
327256
  const haveLoginDiff = loginRoleNames.size > 0 || loginTestIds.size > 0;
326689
327257
  const roleNameFreq = /* @__PURE__ */ new Map();
326690
327258
  const testIdFreq = /* @__PURE__ */ new Map();
326691
- for (const el of landing.elements ?? []) {
327259
+ for (const el of landingElements) {
326692
327260
  const nm = (el.label ?? "").trim().toLowerCase();
326693
327261
  if (el.role && nm)
326694
327262
  roleNameFreq.set(`${el.role}|${nm}`, (roleNameFreq.get(`${el.role}|${nm}`) ?? 0) + 1);
@@ -326728,7 +327296,7 @@ ${keyLines || "- (none provided)"}
326728
327296
  return true;
326729
327297
  };
326730
327298
  const ranked = [];
326731
- for (const el of landing.elements ?? []) {
327299
+ for (const el of landingElements) {
326732
327300
  const label = (el.label ?? "").trim();
326733
327301
  const role = el.role ?? "";
326734
327302
  const testId = el.testId ?? "";
@@ -326740,7 +327308,7 @@ ${keyLines || "- (none provided)"}
326740
327308
  const r = role === "link" ? "link" : "button";
326741
327309
  ranked.push({ tier: 1, proof: { kind: "role", role: r, name: label }, desc: `${r} "${label}" (logout control)` });
326742
327310
  }
326743
- if (label && ACCOUNT_RE.test(label) && !personalized && uniqueRoleName && (role === "button" || role === "link") && absentOnLogin(roleNameKey, "")) {
327311
+ if (label && ACCOUNT_RE.test(label) && !personalized && uniqueRoleName && haveLoginDiff && (role === "button" || role === "link") && absentOnLogin(roleNameKey, "")) {
326744
327312
  ranked.push({ tier: 2, proof: { kind: "role", role, name: label }, desc: `${role} "${label}" (account control)` });
326745
327313
  }
326746
327314
  if (testId && isSemanticTestId(testId) && uniqueTestId && haveLoginDiff && absentOnLogin("", testId)) {
@@ -326754,25 +327322,110 @@ ${keyLines || "- (none provided)"}
326754
327322
  }
326755
327323
  }
326756
327324
  if (ranked.length === 0) {
326757
- const loginPathname = loginRoute ? loginRoute.replace(/[?#].*$/, "").replace(/\/+$/, "") : null;
326758
- const toProofPathname = (r) => {
326759
- if (!r)
326760
- return null;
326761
- const p = r.replace(/[?#].*$/, "").replace(/\/+$/, "");
326762
- return p && p !== "/" && !isLoginPage(p) && p !== loginPathname ? p : null;
326763
- };
326764
- const urlRoute = toProofPathname(redirectRoute) ?? toProofPathname(landing.route);
326765
- if (urlRoute) {
326766
- logs2(` \u2713 url auth-proof: authenticated landing route ${urlRoute} (no durable element proof on the default-state page)`);
326767
- return { kind: "url", route: urlRoute };
326768
- }
326769
- 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)");
327325
+ logs2(" \u26A0 auth-proof: no durable login-absent element proof on the authenticated page (no url fallback \u2014 coverage deferred to a positive proof)");
326770
327326
  return null;
326771
327327
  }
326772
327328
  ranked.sort((a, b) => a.tier - b.tier);
326773
327329
  logs2(` \u2713 live auth-proof: ${ranked[0].desc}`);
326774
327330
  return ranked[0].proof;
326775
327331
  }
327332
+ function harvestCollapsedAuthProof(landingElements, loginAbsentElements, credentials, logs2) {
327333
+ const loginRoleNames = /* @__PURE__ */ new Set();
327334
+ const loginTestIds = /* @__PURE__ */ new Set();
327335
+ for (const el of loginAbsentElements) {
327336
+ const nm = (el.label ?? "").trim().toLowerCase();
327337
+ if (el.role && nm)
327338
+ loginRoleNames.add(`${el.role}|${nm}`);
327339
+ if (el.testId)
327340
+ loginTestIds.add(el.testId);
327341
+ }
327342
+ return rankDurableAuthProof(landingElements, loginRoleNames, loginTestIds, credentials, logs2);
327343
+ }
327344
+ function pickAuthLandingPage(siteMap, redirectRoute, loginRoute) {
327345
+ const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
327346
+ const authedPages = siteMap.pages.filter((p) => (p.requiresAuth || redirectRoute !== null && p.route === redirectRoute) && !isLoginPage(p.route));
327347
+ return (redirectRoute !== null ? authedPages.find((p) => p.route === redirectRoute) : void 0) ?? authedPages[0] ?? null;
327348
+ }
327349
+ function selectAuthLandingRoute(siteMap, redirectRoute, loginRoute) {
327350
+ return pickAuthLandingPage(siteMap, redirectRoute, loginRoute)?.route ?? null;
327351
+ }
327352
+ function harvestAuthProofLive(siteMap, redirectRoute, loginRoute, credentials, logs2) {
327353
+ const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
327354
+ const landing = pickAuthLandingPage(siteMap, redirectRoute, loginRoute);
327355
+ if (!landing) {
327356
+ logs2(" \u26A0 auth-proof: no authenticated landing page captured \u2014 cannot derive a durable proof");
327357
+ return null;
327358
+ }
327359
+ const loginRoleNames = /* @__PURE__ */ new Set();
327360
+ const loginTestIds = /* @__PURE__ */ new Set();
327361
+ for (const p of siteMap.pages) {
327362
+ if (!isLoginPage(p.route) || p.route === landing.route)
327363
+ continue;
327364
+ for (const el of p.elements ?? []) {
327365
+ const nm = (el.label ?? "").trim().toLowerCase();
327366
+ if (el.role && nm)
327367
+ loginRoleNames.add(`${el.role}|${nm}`);
327368
+ if (el.testId)
327369
+ loginTestIds.add(el.testId);
327370
+ }
327371
+ }
327372
+ return rankDurableAuthProof(landing.elements ?? [], loginRoleNames, loginTestIds, credentials, logs2);
327373
+ }
327374
+ var AUTH_ROUTE_RE = /\/(log[\s_-]?in|sign[\s_-]?in|sign[\s_-]?up|register|auth|forgot|reset)\b/i;
327375
+ var AUTO_NAV_TRIGGERS = /* @__PURE__ */ new Set(["AUTO", "REDIRECT"]);
327376
+ var USER_ACTION_TRIGGERS = /* @__PURE__ */ new Set(["CLICK", "SUBMIT", "NAVIGATION"]);
327377
+ var stripToBarePath = (route2) => {
327378
+ if (!route2)
327379
+ return null;
327380
+ let path = route2;
327381
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(route2)) {
327382
+ try {
327383
+ path = new URL(route2).pathname;
327384
+ } catch {
327385
+ }
327386
+ }
327387
+ const p = path.replace(/[?#].*$/, "").replace(/\/+$/, "");
327388
+ return p || "/";
327389
+ };
327390
+ function detectRedirectBackCollapse(baseUrl, transitions) {
327391
+ let basePath;
327392
+ try {
327393
+ basePath = stripToBarePath(new URL(baseUrl).pathname);
327394
+ } catch {
327395
+ return null;
327396
+ }
327397
+ if (!basePath || basePath === "/" || AUTH_ROUTE_RE.test(basePath))
327398
+ return null;
327399
+ const userActionEdges = new Set(transitions.filter((t) => USER_ACTION_TRIGGERS.has(t.triggerType)).map((t) => `${stripToBarePath(t.fromRoute) ?? ""} ${stripToBarePath(t.toRoute) ?? ""}`));
327400
+ 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) ?? ""}`));
327401
+ if (bounceIdx < 0)
327402
+ return null;
327403
+ const authRoute = stripToBarePath(transitions[bounceIdx].toRoute);
327404
+ if (!authRoute)
327405
+ return null;
327406
+ const returned = transitions.slice(bounceIdx + 1).some((t) => AUTH_ROUTE_RE.test(stripToBarePath(t.fromRoute) ?? "") && stripToBarePath(t.toRoute) === basePath);
327407
+ if (!returned)
327408
+ return null;
327409
+ return { basePath, authRoute };
327410
+ }
327411
+ function pickRouteElements(routeElements, wantRoute) {
327412
+ const want = stripToBarePath(wantRoute);
327413
+ for (const [route2, els] of routeElements) {
327414
+ if (stripToBarePath(route2) === want && (els?.length ?? 0) > 0)
327415
+ return els;
327416
+ }
327417
+ return [];
327418
+ }
327419
+ function pickLoginFormElements(routeElements, authRoute) {
327420
+ const exact = pickRouteElements(routeElements, authRoute);
327421
+ if (exact.length > 0)
327422
+ return exact;
327423
+ for (const [route2, els] of routeElements) {
327424
+ if (AUTH_ROUTE_RE.test(stripToBarePath(route2) ?? "") && (els?.length ?? 0) > 0)
327425
+ return els;
327426
+ }
327427
+ return [];
327428
+ }
326776
327429
  function findCredentialLiteralLeaks(rendered, credentials) {
326777
327430
  const leaked = [];
326778
327431
  for (const [key, value] of Object.entries(credentials)) {
@@ -326785,12 +327438,22 @@ ${keyLines || "- (none provided)"}
326785
327438
  return leaked;
326786
327439
  }
326787
327440
  async function verifyAuthRecipe(recipe, baseUrl, credentials, logs2) {
327441
+ const baselineRoute = recipe.redirectRoute ?? baseUrl;
326788
327442
  const spec = [
326789
327443
  `import { test, expect } from '@playwright/test';`,
326790
327444
  ``,
326791
327445
  (0, auth_recipe_js_1.renderAuthFn)(recipe),
326792
327446
  ``,
326793
327447
  `test('__vqa_auth_verify', async ({ page }) => {`,
327448
+ ` // DIFFERENTIAL BASELINE (logged out): the durable proof must NOT already hold here, else it`,
327449
+ ` // does not discriminate an authenticated session from an anonymous one \u2014 reject rather than`,
327450
+ ` // publish a proof that passes while logged out (the cardinal invariant). The goto is NOT`,
327451
+ ` // .catch-swallowed: a genuine navigation failure must fail the verify (fail-closed) rather than`,
327452
+ ` // leave the baseline running on about:blank where the proof is vacuously "absent". An auth-gated`,
327453
+ ` // route resolves fine (it follows the redirect to /login).`,
327454
+ ` await page.goto(${JSON.stringify(baselineRoute)}, { waitUntil: 'domcontentloaded' });`,
327455
+ ` ${(0, auth_recipe_js_1.renderProofBaselineCheck)(recipe.authProof, " ")}`,
327456
+ ` // AUTHED: real login + the durable proof assertion (inside __vqaLogin).`,
326794
327457
  ` await __vqaLogin(page);`,
326795
327458
  `});`,
326796
327459
  ``
@@ -326892,21 +327555,50 @@ ${keyLines || "- (none provided)"}
326892
327555
  cmds: login.cmds.map((c) => (0, scrub_credentials_js_1.scrubCredentials)(c, credentials)),
326893
327556
  preNavCmds: login.preNavCmds?.map((c) => (0, scrub_credentials_js_1.scrubCredentials)(c, credentials))
326894
327557
  };
326895
- const authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
327558
+ let authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
327559
+ let effectiveLogin = scrubbedLogin;
327560
+ if (authProof && (authProof.kind === "role" || authProof.kind === "testid")) {
327561
+ const landingRoute = selectAuthLandingRoute(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route);
327562
+ if (landingRoute)
327563
+ effectiveLogin = { ...effectiveLogin, redirectRoute: landingRoute };
327564
+ }
326896
327565
  if (!authProof) {
326897
- logs2(" \u26A0 auth unverified (no durable signed-in proof on the authenticated page) \u2014 falling back to model-authored login");
326898
- return { recipe: null, outcome: exploreMiss("verify_failed") };
327566
+ const collapse = detectRedirectBackCollapse(baseUrl, exploreResult.collectedTransitions);
327567
+ if (collapse) {
327568
+ const landingEls = pickRouteElements(handle.routeElements, collapse.basePath);
327569
+ const loginEls = pickLoginFormElements(handle.routeElements, collapse.authRoute);
327570
+ const collapsedProof = harvestCollapsedAuthProof(landingEls, loginEls, credentials, logs2);
327571
+ if (collapsedProof) {
327572
+ 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)`);
327573
+ authProof = collapsedProof;
327574
+ effectiveLogin = { ...effectiveLogin, redirectRoute: collapse.basePath };
327575
+ } else {
327576
+ logs2(" \u26A0 redirect-back-to-entry collapse detected but no durable authed-only element differenced (no url fallback) \u2014 deferring to model-authored login");
327577
+ }
327578
+ }
326899
327579
  }
326900
- const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(scrubbedLogin, authProof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
326901
- const rendered = (0, auth_recipe_js_1.renderAuthFn)(candidate);
326902
- const leaks = findCredentialLiteralLeaks(rendered, credentials);
326903
- if (leaks.length > 0) {
326904
- logs2(` \u26A0 auth recipe rejected: rendered __vqaLogin contains literal credential value(s) for ${leaks.join(", ")} \u2014 falling back to model-authored login`);
326905
- return { recipe: null, outcome: "credential_leak" };
327580
+ let identityEndpointProof = null;
327581
+ try {
327582
+ const cookies = await handle.page.context().cookies();
327583
+ if (cookies.some((c) => /(next-auth|authjs)\.session-token/.test(c.name))) {
327584
+ identityEndpointProof = { kind: "endpoint", url: "/api/auth/session" };
327585
+ logs2(" \u2139 NextAuth/Auth.js session cookie detected \u2014 trying an identity-endpoint proof (/api/auth/session) before the element proof");
327586
+ }
327587
+ } catch {
326906
327588
  }
326907
- const planLeaks = findCredentialLiteralLeaks(JSON.stringify(candidate.replayPlan ?? []), credentials);
326908
- if (planLeaks.length > 0) {
326909
- logs2(` \u26A0 auth recipe rejected: structured replayPlan contains literal credential value(s) for ${planLeaks.join(", ")} \u2014 falling back to model-authored login`);
327589
+ const proofCandidates = [];
327590
+ if (identityEndpointProof)
327591
+ proofCandidates.push(identityEndpointProof);
327592
+ if (authProof)
327593
+ proofCandidates.push(authProof);
327594
+ if (proofCandidates.length === 0) {
327595
+ logs2(" \u26A0 auth unverified (no positive proof \u2014 no identity endpoint, no login-absent element) \u2014 falling back to model-authored login");
327596
+ return { recipe: null, outcome: exploreMiss("verify_failed") };
327597
+ }
327598
+ const loginText = [...effectiveLogin.cmds, ...effectiveLogin.preNavCmds ?? []].join("\n") + "\n" + JSON.stringify(effectiveLogin.replayPlan ?? []);
327599
+ const loginLeaks = findCredentialLiteralLeaks(loginText, credentials);
327600
+ if (loginLeaks.length > 0) {
327601
+ logs2(` \u26A0 auth recipe rejected: login sequence contains literal credential value(s) for ${loginLeaks.join(", ")} \u2014 falling back to model-authored login`);
326910
327602
  return { recipe: null, outcome: "credential_leak" };
326911
327603
  }
326912
327604
  try {
@@ -326914,14 +327606,25 @@ ${keyLines || "- (none provided)"}
326914
327606
  } catch {
326915
327607
  }
326916
327608
  handle = null;
326917
- const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
326918
- if (!ok) {
326919
- logs2(" \u26A0 auth unverified (live-verify replay failed \u2014 fleet-config drift) \u2014 falling back to model-authored login");
326920
- return { recipe: null, outcome: exploreMiss("verify_failed") };
327609
+ let lastReject = "no candidate verified";
327610
+ for (const proof of proofCandidates) {
327611
+ const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(effectiveLogin, proof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
327612
+ const renderLeaks = findCredentialLiteralLeaks((0, auth_recipe_js_1.renderAuthFn)(candidate), credentials);
327613
+ if (renderLeaks.length > 0) {
327614
+ lastReject = `${describeAuthProof(proof)} render coincidentally matched credential value(s) ${renderLeaks.join(", ")}`;
327615
+ logs2(` \u26A0 auth-proof candidate skipped (${lastReject}) \u2014 trying the next candidate`);
327616
+ continue;
327617
+ }
327618
+ const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
327619
+ if (ok) {
327620
+ logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${describeAuthProof(proof)}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
327621
+ return { recipe: candidate, outcome: "captured" };
327622
+ }
327623
+ lastReject = `${describeAuthProof(proof)} failed the differential verify`;
327624
+ logs2(` \u26A0 auth-proof candidate rejected (${lastReject}) \u2014 trying the next candidate`);
326921
327625
  }
326922
- const proofDesc = authProof.kind === "role" ? `role:${authProof.role}${authProof.name ? `(${authProof.name})` : ""}` : authProof.kind === "testid" ? `testid:${authProof.testId}` : `url:${authProof.route}`;
326923
- logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${proofDesc}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
326924
- return { recipe: candidate, outcome: "captured" };
327626
+ logs2(` \u26A0 auth unverified (all proof candidates failed: ${lastReject}) \u2014 falling back to model-authored login`);
327627
+ return { recipe: null, outcome: exploreMiss("verify_failed") };
326925
327628
  } catch (err) {
326926
327629
  logs2(` \u26A0 auth-capture run failed: ${err instanceof Error ? err.message : String(err)} \u2014 falling back to model-authored login`);
326927
327630
  return { recipe: null, outcome: (0, ai_retry_js_1.isTransientRateError)(err) ? "rate_limited" : "error" };
@@ -334048,8 +334751,13 @@ Respond with ONLY valid JSON:
334048
334751
  if (!(0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && ctx.mode !== "capture")
334049
334752
  return null;
334050
334753
  if (ctx.authRecipe) {
334051
- pushLog(`auth-prologue: reusing persisted login recipe ${ctx.authRecipe.recipeHash} \u2014 skipping capture`);
334052
- return Promise.resolve(ctx.authRecipe);
334754
+ if (ctx.authRecipe.authProof?.kind === "url") {
334755
+ pushLog(`auth-prologue: persisted recipe ${ctx.authRecipe.recipeHash} uses a demoted url proof \u2014 re-capturing with the differential pipeline (legacy invalidation)`);
334756
+ ctx.authRecipe = void 0;
334757
+ } else {
334758
+ pushLog(`auth-prologue: reusing persisted login recipe ${ctx.authRecipe.recipeHash} \u2014 skipping capture`);
334759
+ return Promise.resolve(ctx.authRecipe);
334760
+ }
334053
334761
  }
334054
334762
  const exploreModel = ctx.exploreModel ?? "";
334055
334763
  const authCaptureModel = ctx.authCaptureModel ?? "";
@@ -349596,7 +350304,7 @@ var require_package4 = __commonJS({
349596
350304
  "package.json"(exports2, module2) {
349597
350305
  module2.exports = {
349598
350306
  name: "@validate.qa/runner",
349599
- version: "1.0.56",
350307
+ version: "1.0.58",
349600
350308
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
349601
350309
  bin: {
349602
350310
  "validate-runner": "dist/cli.js"
@@ -351275,6 +351983,12 @@ function isKeyboardAlreadyHiddenError(err) {
351275
351983
  return /keyboard.*(not|already).*(shown|visible|present|open)|no.*keyboard/i.test(message);
351276
351984
  }
351277
351985
  async function dismissKeyboard(sessionPath, platform3) {
351986
+ try {
351987
+ const shown = await appiumRequest(`${sessionPath}/appium/device/is_keyboard_shown`, { method: "GET" });
351988
+ if (readValue(shown) === false) return;
351989
+ } catch (err) {
351990
+ if (isAppiumDriverDeadError(err)) throw err;
351991
+ }
351278
351992
  if (platform3 === "ANDROID") {
351279
351993
  await pressBack(sessionPath);
351280
351994
  return;
@@ -355858,11 +356572,17 @@ var MobileBridgeDriver = class {
355858
356572
  }
355859
356573
  /** Type `value`; pass `bounds` to tap-to-focus first when nothing is focused. */
355860
356574
  async type(value, bounds) {
355861
- return this.runAction(() => typeText(this.sessionPath, value, bounds));
356575
+ return this.runAction(async () => {
356576
+ await typeText(this.sessionPath, value, bounds);
356577
+ await hideKeyboardIfShown(this.sessionPath);
356578
+ });
355862
356579
  }
355863
356580
  /** Invoke the focused field's Enter/Done action without clearing its value. */
355864
356581
  async submitKeyboard(bounds) {
355865
- return this.runAction(() => submitKeyboard(this.sessionPath, bounds));
356582
+ return this.runAction(async () => {
356583
+ await submitKeyboard(this.sessionPath, bounds);
356584
+ await hideKeyboardIfShown(this.sessionPath);
356585
+ });
355866
356586
  }
355867
356587
  /** Clear the focused element; pass `bounds` to tap-to-focus first. */
355868
356588
  async clear(bounds) {