@validate.qa/runner 1.0.52 → 1.0.53

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 +1175 -131
  2. package/dist/cli.mjs +1175 -131
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -7707,6 +7707,7 @@ var require_credential_tools = __commonJS({
7707
7707
  exports.buildCredentialPromptBlock = buildCredentialPromptBlock;
7708
7708
  exports.isLikelyCredentialKey = isLikelyCredentialKey;
7709
7709
  exports.partitionTestVariables = partitionTestVariables;
7710
+ exports.hasUsableLoginIdentity = hasUsableLoginIdentity;
7710
7711
  exports.renderPublicVariablesBlock = renderPublicVariablesBlock;
7711
7712
  exports.handleFillCredential = handleFillCredential;
7712
7713
  exports.handleFillCredentialForm = handleFillCredentialForm;
@@ -7854,6 +7855,25 @@ CREDENTIAL ROTATION (rare \u2014 read this carefully):
7854
7855
  }
7855
7856
  return { secrets, publics };
7856
7857
  }
7858
+ var IDENTITY_KEY_PATTERN = /^(TEST_|E2E_|QA_)?(USER_?EMAIL|USER_?NAME|USERNAME|LOGIN_?ID|LOGIN_?EMAIL|ACCOUNT_?EMAIL)$/i;
7859
+ var BOOLEAN_LIKE_VALUE = /^(true|false|1|0|yes|no|on|off|enabled|disabled)$/i;
7860
+ var EMAIL_SHAPED_VALUE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
7861
+ function hasUsableLoginIdentity(vars, opts) {
7862
+ if (!vars)
7863
+ return false;
7864
+ return Object.entries(vars).some(([k, rawValue]) => {
7865
+ const v = (rawValue ?? "").trim();
7866
+ if (!v)
7867
+ return false;
7868
+ if (!IDENTITY_KEY_PATTERN.test(k.trim()))
7869
+ return false;
7870
+ if (BOOLEAN_LIKE_VALUE.test(v))
7871
+ return false;
7872
+ if (/EMAIL/i.test(k) && !EMAIL_SHAPED_VALUE.test(v))
7873
+ return false;
7874
+ return !opts?.isPlaceholderIdentityValue?.(v);
7875
+ });
7876
+ }
7857
7877
  function renderPublicVariablesBlock(publics) {
7858
7878
  const entries = Object.entries(publics);
7859
7879
  if (entries.length === 0)
@@ -72456,6 +72476,7 @@ var require_verification_inbox = __commonJS({
72456
72476
  Object.defineProperty(exports, "__esModule", { value: true });
72457
72477
  exports.VerificationInboxReader = void 0;
72458
72478
  exports.isVerificationInboxConfigured = isVerificationInboxConfigured;
72479
+ exports.describeVerificationInboxConfigGap = describeVerificationInboxConfigGap;
72459
72480
  exports.loadVerificationInboxConfig = loadVerificationInboxConfig;
72460
72481
  exports.shouldUseUidWatermark = shouldUseUidWatermark;
72461
72482
  exports.recipientMatchesAddress = recipientMatchesAddress;
@@ -72477,6 +72498,21 @@ var require_verification_inbox = __commonJS({
72477
72498
  function isVerificationInboxConfigured(env = process.env) {
72478
72499
  return !!(readEnv(env, "VERIFICATION_INBOX_HOST") && readEnv(env, "VERIFICATION_INBOX_USER") && readEnv(env, "VERIFICATION_INBOX_PASSWORD") && readEnv(env, "VERIFICATION_INBOX_ADDRESS"));
72479
72500
  }
72501
+ var REQUIRED_INBOX_ENV_KEYS = [
72502
+ "VERIFICATION_INBOX_HOST",
72503
+ "VERIFICATION_INBOX_USER",
72504
+ "VERIFICATION_INBOX_PASSWORD",
72505
+ "VERIFICATION_INBOX_ADDRESS"
72506
+ ];
72507
+ function describeVerificationInboxConfigGap(env = process.env) {
72508
+ const missing = REQUIRED_INBOX_ENV_KEYS.filter((key) => !readEnv(env, key));
72509
+ if (missing.length === 0)
72510
+ return null;
72511
+ if (missing.length === REQUIRED_INBOX_ENV_KEYS.length) {
72512
+ return "verification inbox not configured (no VERIFICATION_INBOX_* env)";
72513
+ }
72514
+ return `verification inbox PARTIALLY configured \u2014 missing: ${missing.join(", ")}`;
72515
+ }
72480
72516
  function loadVerificationInboxConfig(env = process.env) {
72481
72517
  if (!isVerificationInboxConfigured(env))
72482
72518
  return null;
@@ -73085,14 +73121,16 @@ var require_inbox_tools = __commonJS({
73085
73121
  "../runner-core/dist/services/inbox-tools.js"(exports) {
73086
73122
  "use strict";
73087
73123
  Object.defineProperty(exports, "__esModule", { value: true });
73088
- exports.INBOX_TOOL_NAMES = exports.INBOX_TOOLS = exports.OPEN_VERIFICATION_LINK_TOOL = exports.GET_VERIFICATION_LINK_TOOL = exports.EXTRACT_VERIFICATION_CODE_TOOL = exports.READ_VERIFICATION_EMAIL_TOOL = exports.WAIT_FOR_VERIFICATION_TOOL = exports.CHECK_INBOX_TOOL = exports.PROVISION_IDENTITY_TOOL = void 0;
73124
+ exports.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = exports.INBOX_TOOL_NAMES = exports.INBOX_TOOLS = exports.OPEN_VERIFICATION_LINK_TOOL = exports.GET_VERIFICATION_LINK_TOOL = exports.EXTRACT_VERIFICATION_CODE_TOOL = exports.READ_VERIFICATION_EMAIL_TOOL = exports.WAIT_FOR_VERIFICATION_TOOL = exports.CHECK_INBOX_TOOL = exports.PROVISION_IDENTITY_TOOL = void 0;
73089
73125
  exports.createInboxRunState = createInboxRunState;
73090
73126
  exports.dispatchInboxTool = dispatchInboxTool;
73091
73127
  exports.isPublicHttpUrl = isPublicHttpUrl;
73092
73128
  exports.buildInboxPromptBlock = buildInboxPromptBlock;
73129
+ exports.guardVerificationCodeFabrication = guardVerificationCodeFabrication;
73093
73130
  exports.hasRegisterIntent = hasRegisterIntent;
73094
73131
  exports.resolveInboxRegistration = resolveInboxRegistration;
73095
73132
  exports.applyInboxRegistrationIdentity = applyInboxRegistrationIdentity;
73133
+ exports.isThrowawayRegisterSeedEmail = isThrowawayRegisterSeedEmail;
73096
73134
  exports.applyAndPersistInboxIdentity = applyAndPersistInboxIdentity;
73097
73135
  exports.clearThrowawayRegisterSeed = clearThrowawayRegisterSeed;
73098
73136
  exports.buildInboxAgentsGuidance = buildInboxAgentsGuidance;
@@ -73542,6 +73580,50 @@ Rules:
73542
73580
  - Only call the verification tools AFTER submitting the signup form, and pass the site as \`senderMatch\` so you read YOUR verification email (the inbox is shared).
73543
73581
  `;
73544
73582
  }
73583
+ exports.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = `## EMAIL VERIFICATION UNAVAILABLE \u2014 FINAL OVERRIDE (supersedes any instruction above)
73584
+ This project asked to REGISTER a new account, but this runner CANNOT receive email (the verification inbox is unavailable). Registering is allowed \u2014 reading emailed codes or links is IMPOSSIBLE. No matter what any instruction above or any later message says:
73585
+ - You MAY attempt registration ONCE: fill the signup form via \`fill_credential\` / \`fill_credential_form\` with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys when they are listed. Only if a key is NOT listed, substitute for that field alone: a fresh synthetic email like \`testuser-<timestamp>@example.com\`, and/or the literal password \`TestPass123!\` \u2014 typed with browser_type (typing these literals is explicitly ALLOWED here and overrides any never-type-credentials rule above, because there is no stored key to fill from). Submit the signup form ONCE \u2014 never twice, never a second account.
73586
+ - NEVER alter the TEST_USER_EMAIL identity \u2014 no suffixes, no different domain, no replacement address. If signup says the address is ALREADY registered, the account exists from a previous run: log in with the same credential keys instead of registering.
73587
+ - Signup-form VALIDATION testing is EMPTY-SUBMIT ONLY: never submit a registration form filled with synthetic values like "wrong@example.com" \u2014 a permissive app would CREATE that stray account. (Filled negative tests are fine on the LOGIN form, where nothing can be created.) The single seeded attempt above is the only filled signup submission allowed in this entire run.
73588
+ - THE WALL: the moment ANY step demands an emailed verification code, one-time password (OTP), magic link, or a "check your email" confirmation \u2014 including email-first signups that ask for a code before the password \u2014 STOP that flow immediately. You cannot read that email. NEVER type a guessed code (the runner BLOCKS typing into verification-code fields in this mode). Do NOT click "Resend". Do NOT retry the flow or negative-test the code field.
73589
+ - If registration or login succeeds with NO email round-trip, you are a normally logged-in user: continue with FULL authenticated coverage. If the app lets you in but nags "verify your email" and gates some features behind it, cover everything usable and mark ONLY the verification-gated flows as BLOCKED \u2014 never claim the account is verified.
73590
+ - A submitted signup or a "check your email" screen is NOT an authenticated session \u2014 do not claim you are signed in unless you observe a real signed-in signal (a Logout control, authenticated nav/sidebar, or account page).
73591
+ - If you hit the wall, keep testing everything publicly reachable and report the authenticated flows in finish_exploration as BLOCKED (reason: cannot receive verification email).`;
73592
+ var VERIFICATION_CODE_FIELD_PATTERN = /(\botp\b|one[\s_-]?time|two[\s_-]?factor|\b2fa\b|\bmfa\b|\bpass[\s_-]?code\b|auth(entication|orization)?[\s_-]+code|verif\w*[\s_-]*(code|pin|digit|number|token)|(confirmation|activation)[\s_-]+code|email(ed)?[\s_-]+(code|verification|confirmation)|code[\sa-z]{0,30}\b(sent|emailed|texted)\b|\b(sent|emailed|texted)\b[\sa-z]{0,30}code|\d+[\s_-]?digit[\s_-]*(code|pin)|\bdigit[\s_-]*\d\b|enter[\s_-]+(the[\s_-]+)?code\b|^\s*verification\s*$)/i;
73593
+ var GENERIC_CODE_OR_PIN = /\b(code|pin)\b/i;
73594
+ var BENIGN_QUALIFIED_CODE = /\b(promo(tional)?|coupon|discount|referr?al|invite|invitation|gift([\s_-]+card)?|voucher|redeem|redemption|zip|postal|area|country|dial(ing)?|currency|swift|iban|sort|routing|tax|vat|security|cvv|cvc|company|employee|org|organization|team|store|branch|dealer|partner|product|item|serial|model|colou?r|embed|source)[\s_-]+code\b|(create|choose|set|new)[\s_-]+(a[\s_-]+)?pin\b/i;
73595
+ function guardVerificationCodeFabrication(toolName, toolArgs) {
73596
+ const isRawTool = toolName === "browser_type" || toolName === "browser_fill_form";
73597
+ const isCredentialTool = toolName === "fill_credential" || toolName === "fill_credential_form";
73598
+ if (!isRawTool && !isCredentialTool)
73599
+ return { blocked: false };
73600
+ const descriptors = [];
73601
+ if (toolName === "browser_type" || toolName === "fill_credential") {
73602
+ descriptors.push(String(toolArgs.element ?? ""));
73603
+ } else {
73604
+ const fields = Array.isArray(toolArgs.fields) ? toolArgs.fields : [];
73605
+ for (const f of fields) {
73606
+ if (!f || typeof f !== "object")
73607
+ continue;
73608
+ const entry = f;
73609
+ descriptors.push([entry.name, entry.element, entry.field, entry.label, entry.placeholder, entry.ariaLabel, entry.description].filter((part) => typeof part === "string").join(" "));
73610
+ }
73611
+ }
73612
+ for (const descriptor of descriptors) {
73613
+ if (!descriptor)
73614
+ continue;
73615
+ const otpSpecific = VERIFICATION_CODE_FIELD_PATTERN.test(descriptor);
73616
+ const genericCodeField = GENERIC_CODE_OR_PIN.test(descriptor) && !BENIGN_QUALIFIED_CODE.test(descriptor);
73617
+ if (otpSpecific || genericCodeField) {
73618
+ return {
73619
+ blocked: true,
73620
+ matchedField: descriptor.slice(0, 120),
73621
+ message: "BLOCKED: this looks like an email verification / one-time code field, but this runner CANNOT receive email \u2014 any code you type would be fabricated. Stop this flow now (do not click Resend, do not retry), continue with publicly reachable coverage, and report this flow as BLOCKED (cannot receive verification email) in finish_exploration."
73622
+ };
73623
+ }
73624
+ }
73625
+ return { blocked: false };
73626
+ }
73545
73627
  function hasRegisterIntent(variableContext) {
73546
73628
  const v = variableContext.allTestVariables?.DISCOVERY_SHOULD_REGISTER ?? variableContext.publicTestVariables?.DISCOVERY_SHOULD_REGISTER ?? "";
73547
73629
  return String(v).trim().toLowerCase() === "true";
@@ -73579,7 +73661,11 @@ Rules:
73579
73661
  vars[passwordKey] = derived;
73580
73662
  }
73581
73663
  }
73582
- var THROWAWAY_REGISTER_EMAIL = /^testuser\+\d+@validate\.qa$/i;
73664
+ var THROWAWAY_REGISTER_EMAIL = /^testuser\+(\d{10,16}|[0-9a-f]{10})@validate\.qa$/i;
73665
+ function isThrowawayRegisterSeedEmail(value) {
73666
+ const v = (value ?? "").trim();
73667
+ return !!v && THROWAWAY_REGISTER_EMAIL.test(v);
73668
+ }
73583
73669
  async function applyAndPersistInboxIdentity(opts) {
73584
73670
  const { variableContext, registration, persist, log: log2 } = opts;
73585
73671
  const vars = variableContext.allTestVariables ??= {};
@@ -318095,6 +318181,8 @@ var require_exploration_state = __commonJS({
318095
318181
  existing.protected = true;
318096
318182
  if (entry.label && !existing.label)
318097
318183
  existing.label = entry.label;
318184
+ if (entry.provenance && !existing.provenance)
318185
+ existing.provenance = entry.provenance;
318098
318186
  protectedCount++;
318099
318187
  }
318100
318188
  continue;
@@ -318104,10 +318192,10 @@ var require_exploration_state = __commonJS({
318104
318192
  continue;
318105
318193
  let evictKey = null;
318106
318194
  let evictIter = -1;
318107
- for (const [key, item] of state2.frontier) {
318108
- if (item.source === "link-seen" && !item.protected && item.discoveredAtIteration >= evictIter) {
318195
+ for (const [key, item2] of state2.frontier) {
318196
+ if (item2.source === "link-seen" && !item2.protected && item2.discoveredAtIteration >= evictIter) {
318109
318197
  evictKey = key;
318110
- evictIter = item.discoveredAtIteration;
318198
+ evictIter = item2.discoveredAtIteration;
318111
318199
  }
318112
318200
  }
318113
318201
  if (evictKey === null)
@@ -318116,14 +318204,17 @@ var require_exploration_state = __commonJS({
318116
318204
  linkSeenCount--;
318117
318205
  }
318118
318206
  const canProtect = entry.protected && protectedCount < exports.PROTECTED_FRONTIER_CAP;
318119
- state2.frontier.set(entry.route, {
318207
+ const item = {
318120
318208
  route: entry.route,
318121
318209
  source: "link-seen",
318122
318210
  discoveredAtIteration: state2.iteration,
318123
318211
  label: entry.label,
318124
318212
  protected: canProtect,
318125
318213
  familyKey: entry.familyKey
318126
- });
318214
+ };
318215
+ if (entry.provenance)
318216
+ item.provenance = entry.provenance;
318217
+ state2.frontier.set(entry.route, item);
318127
318218
  linkSeenCount++;
318128
318219
  if (canProtect)
318129
318220
  protectedCount++;
@@ -318830,9 +318921,11 @@ var require_survey_link_frontier = __commonJS({
318830
318921
  exports.isSurveyLinkFrontierEnabled = isSurveyLinkFrontierEnabled;
318831
318922
  exports.extractSnapshotLinks = extractSnapshotLinks;
318832
318923
  exports.deriveLinkScope = deriveLinkScope;
318924
+ exports.sameAppOrigin = sameAppOrigin;
318833
318925
  exports.computeFamilyKey = computeFamilyKey;
318834
318926
  exports.basePathDepthOf = basePathDepthOf;
318835
318927
  exports.ingestSnapshotLinks = ingestSnapshotLinks;
318928
+ exports.ingestSeenLinks = ingestSeenLinks;
318836
318929
  exports.visitedSatisfies = visitedSatisfies;
318837
318930
  exports.markLinkNavigationAttempt = markLinkNavigationAttempt;
318838
318931
  exports.getFinishBlockingFrontier = getFinishBlockingFrontier;
@@ -318891,6 +318984,20 @@ var require_survey_link_frontier = __commonJS({
318891
318984
  }
318892
318985
  }
318893
318986
  var NON_HTTP_SCHEME_RE = /^(mailto|tel|sms|javascript|data|blob|ftp|file|about|chrome|intent):/i;
318987
+ function sameAppOrigin(a, b) {
318988
+ if (!a || !b)
318989
+ return false;
318990
+ if (a === b)
318991
+ return true;
318992
+ try {
318993
+ const ua = new URL(a);
318994
+ const ub = new URL(b);
318995
+ const host = (u) => u.host.replace(/^www\./i, "");
318996
+ return ua.protocol === ub.protocol && host(ua) === host(ub);
318997
+ } catch {
318998
+ return false;
318999
+ }
319000
+ }
318894
319001
  var GENERATOR_QUERY_KEYS = /* @__PURE__ */ new Set([
318895
319002
  "page",
318896
319003
  "p",
@@ -318959,9 +319066,15 @@ var require_survey_link_frontier = __commonJS({
318959
319066
  return (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).split("/").filter(Boolean).length;
318960
319067
  }
318961
319068
  function ingestSnapshotLinks(state2, params) {
318962
- const { rawSnapshot, scope, isEntryOrHub, isErrorContext, pageUrl, log: log2 } = params;
319069
+ const { rawSnapshot, ...rest } = params;
319070
+ ingestSeenLinks(state2, { ...rest, links: extractSnapshotLinks(rawSnapshot), provenance: "snapshot" });
319071
+ }
319072
+ var SESSION_KILLER_PATH_RE = /(^|\/)(log[-_]?out|sign[-_]?out|sign[-_]?off|sessions?\/(destroy|delete))([/?#.]|$)/i;
319073
+ function ingestSeenLinks(state2, params) {
319074
+ const { links, scope, isEntryOrHub, isErrorContext, pageUrl, log: log2, provenance = "snapshot" } = params;
318963
319075
  if (isErrorContext)
318964
319076
  return;
319077
+ const originsMatch = (a, b) => provenance === "dom" ? sameAppOrigin(a, b) : a === b;
318965
319078
  let pageBase = null;
318966
319079
  if (pageUrl) {
318967
319080
  try {
@@ -318970,11 +319083,11 @@ var require_survey_link_frontier = __commonJS({
318970
319083
  pageBase = null;
318971
319084
  }
318972
319085
  }
318973
- if (pageBase && scope.origin && pageBase.origin !== scope.origin) {
319086
+ if (pageBase && scope.origin && !originsMatch(pageBase.origin, scope.origin)) {
318974
319087
  log2?.(` [link-frontier] skipped off-origin page ${pageBase.origin}`);
318975
319088
  return;
318976
319089
  }
318977
- const seen = extractSnapshotLinks(rawSnapshot);
319090
+ const seen = links;
318978
319091
  if (seen.length === 0)
318979
319092
  return;
318980
319093
  const suppressed = [];
@@ -319020,14 +319133,14 @@ var require_survey_link_frontier = __commonJS({
319020
319133
  suppressed.push(`${raw} (unparseable)`);
319021
319134
  continue;
319022
319135
  }
319023
- if (scope.origin && abs.origin !== scope.origin) {
319136
+ if (scope.origin && !originsMatch(abs.origin, scope.origin)) {
319024
319137
  suppressed.push(`${raw} (external)`);
319025
319138
  continue;
319026
319139
  }
319027
319140
  if (abs.hash && !abs.hash.startsWith("#/"))
319028
319141
  abs.hash = "";
319029
319142
  const route2 = (0, exploration_parser_js_1.canonicalizeRoute)(abs.pathname + abs.search + abs.hash);
319030
- candidates.push({ route: route2, label: link.label });
319143
+ candidates.push({ route: route2, label: link.label, hidden: link.hidden, inNav: link.inNav, inFooter: link.inFooter });
319031
319144
  }
319032
319145
  candidates.sort((a, b) => {
319033
319146
  const aVariant = a.route === (0, exploration_parser_js_1.normalizeRouteForFocusScope)(a.route) ? 0 : 1;
@@ -319041,6 +319154,10 @@ var require_survey_link_frontier = __commonJS({
319041
319154
  if (stripped)
319042
319155
  suppressed.push(`${candidate.route} (generator-stripped -> ${route2})`);
319043
319156
  const scopePath = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2);
319157
+ if (provenance === "dom" && SESSION_KILLER_PATH_RE.test(scopePath)) {
319158
+ suppressed.push(`${route2} (session-killer)`);
319159
+ continue;
319160
+ }
319044
319161
  if (scope.basePath !== "/" && !(0, exploration_parser_js_1.routeIsWithinFocusAreas)(route2, [scope.basePath])) {
319045
319162
  suppressed.push(`${route2} (out-of-scope)`);
319046
319163
  continue;
@@ -319049,9 +319166,10 @@ var require_survey_link_frontier = __commonJS({
319049
319166
  suppressed.push(`${route2} (root)`);
319050
319167
  continue;
319051
319168
  }
319169
+ const doorLike = provenance === "dom" && candidate.inNav === true && candidate.hidden === true;
319052
319170
  const existing = state2.frontier.get(route2);
319053
319171
  if (existing) {
319054
- const upgradeCandidate = isEntryOrHub && existing.source === "link-seen" && !existing.protected;
319172
+ const upgradeCandidate = (isEntryOrHub || doorLike) && existing.source === "link-seen" && !existing.protected;
319055
319173
  if (!upgradeCandidate)
319056
319174
  continue;
319057
319175
  }
@@ -319073,9 +319191,17 @@ var require_survey_link_frontier = __commonJS({
319073
319191
  familiesInBatch.add(familyKey);
319074
319192
  seenScopedPaths.add(scopePath);
319075
319193
  const relPath = scope.basePath === "/" ? scopePath : scopePath.slice(scope.basePath.length) || "/";
319076
- const isFooterLegal = FOOTER_LEGAL_PATH_RE.test(relPath);
319077
- const isProtected = isEntryOrHub && !isTemplate && !isFooterLegal;
319078
- entries.push({ route: route2, label, protected: isProtected, familyKey });
319194
+ const isFooterLegal = FOOTER_LEGAL_PATH_RE.test(relPath) || provenance === "dom" && candidate.inFooter === true;
319195
+ const isProtected = (isEntryOrHub || doorLike) && !isTemplate && !isFooterLegal;
319196
+ entries.push({
319197
+ route: route2,
319198
+ label,
319199
+ protected: isProtected,
319200
+ familyKey,
319201
+ // Snapshot entries stay provenance-free — the legacy state shape must be
319202
+ // byte-identical when the DOM-harvest flag is off (flag-off invariance).
319203
+ provenance: provenance === "dom" ? candidate.hidden ? "dom-hidden" : "dom" : void 0
319204
+ });
319079
319205
  }
319080
319206
  if (suppressed.length > 0 && log2) {
319081
319207
  log2(` [link-frontier] suppressed ${suppressed.length}: ${suppressed.slice(0, 8).join(", ")}${suppressed.length > 8 ? ` \u2026+${suppressed.length - 8}` : ""}`);
@@ -319087,7 +319213,10 @@ var require_survey_link_frontier = __commonJS({
319087
319213
  (0, exploration_state_js_1.reduceEvent)(state2, { type: "LINKS_DISCOVERED", entries });
319088
319214
  const admitted = countLinkSeen(state2) - before;
319089
319215
  if (admitted > 0 && log2) {
319090
- log2(` [link-frontier] +${admitted} link-seen route(s) (${countLinkSeen(state2)}/${exploration_state_js_1.LINK_SEEN_FRONTIER_CAP}): ${entries.slice(0, 6).map((e) => e.route).join(", ")}${entries.length > 6 ? " \u2026" : ""}`);
319216
+ const tag = provenance === "dom" ? " [dom-harvest]" : "";
319217
+ const hiddenCount = entries.filter((e) => e.provenance === "dom-hidden").length;
319218
+ const hiddenNote = hiddenCount > 0 ? ` (${hiddenCount} hidden-menu door(s))` : "";
319219
+ log2(` [link-frontier]${tag} +${admitted} link-seen route(s)${hiddenNote} (${countLinkSeen(state2)}/${exploration_state_js_1.LINK_SEEN_FRONTIER_CAP}): ${entries.slice(0, 6).map((e) => e.route).join(", ")}${entries.length > 6 ? " \u2026" : ""}`);
319091
319220
  }
319092
319221
  const deniedProtected = entries.filter((e) => e.protected && state2.frontier.get(e.route) && !state2.frontier.get(e.route)?.protected);
319093
319222
  if (deniedProtected.length > 0 && log2) {
@@ -319214,10 +319343,10 @@ var require_exploration_prompt_builder = __commonJS({
319214
319343
  sections.push(`Mode: ${config.mode} | Budget remaining: ${config.maxIterations - state2.iteration} iterations`);
319215
319344
  const budgetPct = state2.iteration / config.maxIterations;
319216
319345
  const budgetBlocking = config.linkFrontierEnabled ? (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2) : [];
319217
- if (budgetPct >= 0.95) {
319346
+ if (!config.adaptiveBudget && budgetPct >= 0.95) {
319218
319347
  sections.push("");
319219
319348
  sections.push(budgetBlocking.length > 0 ? `**CRITICAL: FINAL ITERATIONS. Unvisited discovered routes remain: ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(budgetBlocking, 3)}. Visit ONLY these (capture_page each), then call finish_exploration. Nothing else.**` : "**CRITICAL: FINAL ITERATIONS. Call finish_exploration NOW with a summary of what you found. Do NOT start any new exploration.**");
319220
- } else if (budgetPct >= 0.9) {
319349
+ } else if (!config.adaptiveBudget && budgetPct >= 0.9) {
319221
319350
  sections.push("");
319222
319351
  sections.push(budgetBlocking.length > 0 ? `**CRITICAL: You have used 90%+ of your iteration budget. Unvisited discovered routes remain: ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(budgetBlocking, 4)}. Visit ONLY these (capture_page each), then call finish_exploration.**` : "**CRITICAL: You have used 90%+ of your iteration budget. Wrap up immediately and call finish_exploration.**");
319223
319352
  }
@@ -319603,9 +319732,9 @@ var require_exploration_prompt_builder = __commonJS({
319603
319732
  sections.push("## Control Brief");
319604
319733
  sections.push(`Current route: ${state2.currentRoute} | Iteration ${state2.iteration}/${config.maxIterations} | Budget remaining: ${config.maxIterations - state2.iteration}`);
319605
319734
  const budgetPct = state2.iteration / config.maxIterations;
319606
- if (budgetPct >= 0.95) {
319735
+ if (!config.adaptiveBudget && budgetPct >= 0.95) {
319607
319736
  sections.push("**CRITICAL: FINAL ITERATIONS. Call finish_exploration NOW.**");
319608
- } else if (budgetPct >= 0.9) {
319737
+ } else if (!config.adaptiveBudget && budgetPct >= 0.9) {
319609
319738
  sections.push("**CRITICAL: 90%+ budget used. Wrap up and call finish_exploration.**");
319610
319739
  }
319611
319740
  if (state2.routeState.size > 0) {
@@ -321573,6 +321702,332 @@ var require_evidence_harvester = __commonJS({
321573
321702
  }
321574
321703
  });
321575
321704
 
321705
+ // ../runner-core/dist/services/survey-door-harvester.js
321706
+ var require_survey_door_harvester = __commonJS({
321707
+ "../runner-core/dist/services/survey-door-harvester.js"(exports) {
321708
+ "use strict";
321709
+ Object.defineProperty(exports, "__esModule", { value: true });
321710
+ exports.PAGE_URL_PROBE_FN = exports.MENU_EXPAND_COLLECT_FN = exports.DOM_LINK_COLLECT_FN = void 0;
321711
+ exports.isDomHarvestEnabled = isDomHarvestEnabled;
321712
+ exports.isMenuExpandEnabled = isMenuExpandEnabled;
321713
+ exports.parseEvaluateResult = parseEvaluateResult;
321714
+ exports.sortDoorsFirst = sortDoorsFirst;
321715
+ exports.harvestDomLinks = harvestDomLinks;
321716
+ exports.toSeenLinks = toSeenLinks;
321717
+ function isDomHarvestEnabled() {
321718
+ const v = process.env.DISCOVERY_SURVEY_DOM_HARVEST;
321719
+ return v === "true" || v === "1";
321720
+ }
321721
+ function isMenuExpandEnabled() {
321722
+ const v = process.env.DISCOVERY_SURVEY_MENU_EXPAND;
321723
+ return (v === "true" || v === "1") && isDomHarvestEnabled();
321724
+ }
321725
+ var MAX_DOM_LINKS = 150;
321726
+ var MAX_EXPAND_TRIGGERS = 4;
321727
+ var EVALUATE_TIMEOUT_MS = 1e4;
321728
+ exports.DOM_LINK_COLLECT_FN = `() => {
321729
+ const doors = [];
321730
+ const rest = [];
321731
+ const seen = new Set();
321732
+ const styleHidden = (el) => {
321733
+ try {
321734
+ const s = window.getComputedStyle(el);
321735
+ return s.display === 'none' || s.visibility === 'hidden';
321736
+ } catch { return false; }
321737
+ };
321738
+ const anchors = document.querySelectorAll('a[href], area[href]');
321739
+ for (const a of anchors) {
321740
+ const raw = a.getAttribute('href') || '';
321741
+ if (!raw || raw === '#') continue;
321742
+ if (/^(javascript|mailto|tel|sms|data|blob|about):/i.test(raw)) continue;
321743
+ if (a.hasAttribute('download')) continue;
321744
+ let resolved = '';
321745
+ try { resolved = a.href || ''; } catch {}
321746
+ if (typeof resolved !== 'string') resolved = String(resolved.baseVal || '');
321747
+ const key = raw + '|' + resolved;
321748
+ if (seen.has(key)) continue;
321749
+ seen.add(key);
321750
+ const label = (a.getAttribute('aria-label') || a.textContent || '')
321751
+ .replace(/\\s+/g, ' ').trim().slice(0, 80);
321752
+ let hidden = false;
321753
+ let node = a;
321754
+ while (node && node !== document.documentElement) {
321755
+ if (styleHidden(node)) { hidden = true; break; }
321756
+ node = node.parentElement;
321757
+ }
321758
+ // Footer context WINS over nav: <footer><nav> link farms are boilerplate,
321759
+ // not doors (sol implementation-review P1).
321760
+ const inFooter = !!a.closest('footer,[role="contentinfo"]');
321761
+ const inNav = !inFooter && !!a.closest('nav,[role="navigation"],[role="menubar"],[role="menu"],header,[role="banner"]');
321762
+ const entry = { href: raw.slice(0, 500), resolved: resolved.slice(0, 500), label, hidden, inNav, inFooter };
321763
+ if (inNav || hidden) doors.push(entry); else rest.push(entry);
321764
+ if (doors.length >= ${MAX_DOM_LINKS}) break;
321765
+ }
321766
+ const links = doors.concat(rest).slice(0, ${MAX_DOM_LINKS});
321767
+ return { pageUrl: location.href, links };
321768
+ }`;
321769
+ exports.MENU_EXPAND_COLLECT_FN = `async () => {
321770
+ const startUrl = location.href;
321771
+ // Baseline pass records KEYS ONLY (no output budget consumed) \u2014 a page with
321772
+ // 150+ static anchors must not crowd freshly-mounted menu links out of the
321773
+ // result. Only post-click NEW anchors are returned.
321774
+ const seen = new Set();
321775
+ const out = [];
321776
+ const keyOf = (a) => {
321777
+ const raw = a.getAttribute('href') || '';
321778
+ if (!raw || raw === '#') return null;
321779
+ if (/^(javascript|mailto|tel|sms|data|blob|about):/i.test(raw)) return null;
321780
+ if (a.hasAttribute('download')) return null;
321781
+ let resolved = '';
321782
+ try { resolved = a.href || ''; } catch {}
321783
+ if (typeof resolved !== 'string') resolved = String(resolved.baseVal || '');
321784
+ return { raw, resolved, key: raw + '|' + resolved };
321785
+ };
321786
+ for (const a of document.querySelectorAll('a[href], area[href]')) {
321787
+ const k = keyOf(a);
321788
+ if (k) seen.add(k.key);
321789
+ }
321790
+ const collectNew = () => {
321791
+ for (const a of document.querySelectorAll('a[href], area[href]')) {
321792
+ if (out.length >= ${MAX_DOM_LINKS}) return;
321793
+ const k = keyOf(a);
321794
+ if (!k || seen.has(k.key)) continue;
321795
+ seen.add(k.key);
321796
+ const label = (a.getAttribute('aria-label') || a.textContent || '')
321797
+ .replace(/\\s+/g, ' ').trim().slice(0, 80);
321798
+ // hidden:true \u2014 mounted-on-demand links are absent from a resting
321799
+ // accessibility snapshot, which is exactly what "hidden door" means.
321800
+ out.push({ href: k.raw.slice(0, 500), resolved: k.resolved.slice(0, 500), label, hidden: true, inNav: true, inFooter: false });
321801
+ }
321802
+ };
321803
+ const chrome = 'nav,[role="navigation"],[role="menubar"],header,[role="banner"]';
321804
+ const destructive = /(delete|remove|archive|reset|disconnect|deactivate|destroy|purge|log\\s*out|sign\\s*out)/i;
321805
+ const styleVisible = (el) => {
321806
+ let node = el;
321807
+ while (node && node !== document.documentElement) {
321808
+ try {
321809
+ const s = window.getComputedStyle(node);
321810
+ if (s.display === 'none' || s.visibility === 'hidden') return false;
321811
+ } catch { return false; }
321812
+ node = node.parentElement;
321813
+ }
321814
+ return true;
321815
+ };
321816
+ const candidates = [];
321817
+ for (const el of document.querySelectorAll('button[aria-haspopup], button[aria-expanded="false"], [role="button"][aria-haspopup], [role="button"][aria-expanded="false"]')) {
321818
+ if (candidates.length >= ${MAX_EXPAND_TRIGGERS}) break;
321819
+ if (el.tagName === 'A' || el.closest('a[href]')) continue;
321820
+ const haspopup = el.getAttribute('aria-haspopup');
321821
+ const expanded = el.getAttribute('aria-expanded');
321822
+ if (haspopup === 'false') continue; // explicit no-popup
321823
+ if (expanded === 'true') continue; // already open \u2014 not ours to toggle
321824
+ if (el.form || el.closest('form')) continue; // form-associated (incl. form="" attr)
321825
+ const type = (el.getAttribute('type') || '').toLowerCase();
321826
+ if (type === 'submit' || type === 'reset') continue;
321827
+ if (!el.closest(chrome)) continue;
321828
+ if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') continue;
321829
+ if (!styleVisible(el)) continue; // hidden triggers keep bogus geometry
321830
+ const name = (el.getAttribute('aria-label') || el.textContent || '').replace(/\\s+/g, ' ').trim();
321831
+ if (destructive.test(name)) continue;
321832
+ const rect = el.getBoundingClientRect();
321833
+ if (rect.width === 0 && rect.height === 0) continue;
321834
+ candidates.push(el);
321835
+ }
321836
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
321837
+ let expanded = 0;
321838
+ for (const el of candidates) {
321839
+ let opened = false;
321840
+ try {
321841
+ try { el.click(); opened = true; } catch { continue; }
321842
+ expanded++;
321843
+ await sleep(350);
321844
+ if (location.href !== startUrl) {
321845
+ return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: true, originalUrl: startUrl };
321846
+ }
321847
+ collectNew();
321848
+ } finally {
321849
+ // Restore the exact trigger we opened: aria-expanded still "true" needs a
321850
+ // closing click; an attribute-less toggle gets clicked back once too.
321851
+ if (opened) {
321852
+ try {
321853
+ const nowExpanded = el.getAttribute('aria-expanded');
321854
+ if (nowExpanded !== 'false') el.click();
321855
+ } catch {}
321856
+ }
321857
+ }
321858
+ await sleep(100);
321859
+ if (location.href !== startUrl) {
321860
+ return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: true, originalUrl: startUrl };
321861
+ }
321862
+ }
321863
+ return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: false, originalUrl: startUrl };
321864
+ }`;
321865
+ exports.PAGE_URL_PROBE_FN = `() => location.href`;
321866
+ function parseEvaluateResult(mcpResult) {
321867
+ if (!mcpResult || typeof mcpResult !== "object")
321868
+ return null;
321869
+ if (mcpResult.isError === true)
321870
+ return null;
321871
+ const content = mcpResult.content;
321872
+ if (!Array.isArray(content))
321873
+ return null;
321874
+ for (const item of content) {
321875
+ if (!item || typeof item !== "object")
321876
+ continue;
321877
+ const { type, text } = item;
321878
+ if (type !== "text" || typeof text !== "string" || !text.trim())
321879
+ continue;
321880
+ let payload = null;
321881
+ const section = /(?:^|\n)### Result\s*\n([\s\S]*?)(?=\n### |$)/.exec(text);
321882
+ if (section) {
321883
+ payload = section[1];
321884
+ } else if (!text.trimStart().startsWith("###")) {
321885
+ payload = text;
321886
+ }
321887
+ if (!payload)
321888
+ continue;
321889
+ const cleaned = payload.replace(/^\s*```(json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
321890
+ if (!cleaned)
321891
+ continue;
321892
+ try {
321893
+ return JSON.parse(cleaned);
321894
+ } catch {
321895
+ continue;
321896
+ }
321897
+ }
321898
+ return null;
321899
+ }
321900
+ function normalizeLinks(value) {
321901
+ if (!value || typeof value !== "object")
321902
+ return [];
321903
+ const links = value.links;
321904
+ if (!Array.isArray(links))
321905
+ return [];
321906
+ const out = [];
321907
+ for (const raw of links) {
321908
+ if (!raw || typeof raw !== "object")
321909
+ continue;
321910
+ const l = raw;
321911
+ if (typeof l.href !== "string" || !l.href)
321912
+ continue;
321913
+ out.push({
321914
+ href: l.href,
321915
+ resolved: typeof l.resolved === "string" ? l.resolved : "",
321916
+ label: typeof l.label === "string" && l.label ? l.label : void 0,
321917
+ hidden: l.hidden === true,
321918
+ inNav: l.inNav === true,
321919
+ inFooter: l.inFooter === true
321920
+ });
321921
+ }
321922
+ return out;
321923
+ }
321924
+ function sortDoorsFirst(links) {
321925
+ const tier = (l) => {
321926
+ if (l.inNav && l.hidden)
321927
+ return 0;
321928
+ if (l.inNav)
321929
+ return 1;
321930
+ if (!l.inFooter)
321931
+ return 2;
321932
+ return 3;
321933
+ };
321934
+ return [...links].sort((a, b) => tier(a) - tier(b));
321935
+ }
321936
+ async function callWithTimeout(client, fn, timeoutMs) {
321937
+ let timer;
321938
+ const timeout = new Promise((_, reject) => {
321939
+ timer = setTimeout(() => reject(new Error(`browser_evaluate timed out after ${timeoutMs}ms`)), timeoutMs);
321940
+ });
321941
+ try {
321942
+ return await Promise.race([
321943
+ client.callTool({ name: "browser_evaluate", arguments: { function: fn } }, void 0, { timeout: timeoutMs }),
321944
+ timeout
321945
+ ]);
321946
+ } finally {
321947
+ if (timer)
321948
+ clearTimeout(timer);
321949
+ }
321950
+ }
321951
+ async function harvestDomLinks(client, options = {}) {
321952
+ const { expandMenus = false, log: log2 } = options;
321953
+ const empty = { pageUrl: null, links: [], expandedTriggers: 0, navigated: false, originalUrl: null };
321954
+ let staticResult = empty;
321955
+ try {
321956
+ const parsed = parseEvaluateResult(await callWithTimeout(client, exports.DOM_LINK_COLLECT_FN, EVALUATE_TIMEOUT_MS));
321957
+ const links = normalizeLinks(parsed);
321958
+ const pageUrl = parsed && typeof parsed === "object" && typeof parsed.pageUrl === "string" ? parsed.pageUrl : null;
321959
+ staticResult = { pageUrl, links, expandedTriggers: 0, navigated: false, originalUrl: pageUrl };
321960
+ } catch (err) {
321961
+ log2?.(` [door-harvest] dom read skipped \u2014 ${err instanceof Error ? err.message : String(err)}`);
321962
+ return empty;
321963
+ }
321964
+ if (!expandMenus) {
321965
+ return { ...staticResult, links: sortDoorsFirst(staticResult.links) };
321966
+ }
321967
+ if (!staticResult.pageUrl) {
321968
+ log2?.(" [door-harvest] skipping menu expansion \u2014 no verifiable page URL from the static pass");
321969
+ return { ...staticResult, links: sortDoorsFirst(staticResult.links) };
321970
+ }
321971
+ let extra = [];
321972
+ let expandedTriggers = 0;
321973
+ let expansionFailed = false;
321974
+ try {
321975
+ const parsed = parseEvaluateResult(await callWithTimeout(client, exports.MENU_EXPAND_COLLECT_FN, EVALUATE_TIMEOUT_MS + 4e3));
321976
+ if (parsed === null) {
321977
+ expansionFailed = true;
321978
+ } else {
321979
+ extra = normalizeLinks(parsed);
321980
+ const obj = typeof parsed === "object" ? parsed : {};
321981
+ expandedTriggers = typeof obj.expandedTriggers === "number" ? obj.expandedTriggers : 0;
321982
+ }
321983
+ } catch (err) {
321984
+ expansionFailed = true;
321985
+ log2?.(` [door-harvest] menu expansion skipped \u2014 ${err instanceof Error ? err.message : String(err)}`);
321986
+ }
321987
+ let navigated = false;
321988
+ try {
321989
+ const probe = parseEvaluateResult(await callWithTimeout(client, exports.PAGE_URL_PROBE_FN, 5e3));
321990
+ if (typeof probe === "string" && probe) {
321991
+ navigated = probe !== staticResult.pageUrl;
321992
+ } else {
321993
+ navigated = expansionFailed;
321994
+ }
321995
+ } catch {
321996
+ navigated = expansionFailed;
321997
+ }
321998
+ const known = new Set(staticResult.links.map((l) => l.href));
321999
+ const merged = [...staticResult.links];
322000
+ for (const link of extra) {
322001
+ if (!known.has(link.href)) {
322002
+ known.add(link.href);
322003
+ merged.push(link);
322004
+ }
322005
+ }
322006
+ if (expandedTriggers > 0) {
322007
+ log2?.(` [door-harvest] expanded ${expandedTriggers} menu trigger(s), +${merged.length - staticResult.links.length} link(s)${navigated ? " \u2014 PAGE NAVIGATED, caller must restore" : ""}`);
322008
+ } else if (navigated) {
322009
+ log2?.(" [door-harvest] page moved during expansion attempt \u2014 caller must restore");
322010
+ }
322011
+ return {
322012
+ pageUrl: staticResult.pageUrl,
322013
+ links: sortDoorsFirst(merged),
322014
+ expandedTriggers,
322015
+ navigated,
322016
+ originalUrl: staticResult.pageUrl
322017
+ };
322018
+ }
322019
+ function toSeenLinks(links) {
322020
+ return links.map((l) => ({
322021
+ url: l.resolved || l.href,
322022
+ label: l.label,
322023
+ hidden: l.hidden,
322024
+ inNav: l.inNav,
322025
+ inFooter: l.inFooter
322026
+ }));
322027
+ }
322028
+ }
322029
+ });
322030
+
321576
322031
  // ../runner-core/dist/services/survey-scope.js
321577
322032
  var require_survey_scope = __commonJS({
321578
322033
  "../runner-core/dist/services/survey-scope.js"(exports) {
@@ -321580,7 +322035,9 @@ var require_survey_scope = __commonJS({
321580
322035
  Object.defineProperty(exports, "__esModule", { value: true });
321581
322036
  exports.deriveSurveyPathScope = deriveSurveyPathScope;
321582
322037
  exports.isAuthFlowRoute = isAuthFlowRoute;
322038
+ exports.hasAuthFlowSegment = hasAuthFlowSegment;
321583
322039
  exports.buildSurveyScopeBoundary = buildSurveyScopeBoundary;
322040
+ exports.routeWithinSurveyBasePath = routeWithinSurveyBasePath;
321584
322041
  exports.filterFeatureGroupsToPathScope = filterFeatureGroupsToPathScope;
321585
322042
  var exploration_parser_js_1 = require_exploration_parser();
321586
322043
  function deriveSurveyPathScope(baseUrl) {
@@ -321608,22 +322065,35 @@ var require_survey_scope = __commonJS({
321608
322065
  "callback",
321609
322066
  "login",
321610
322067
  "log-in",
322068
+ "log_in",
321611
322069
  "signin",
321612
322070
  "sign-in",
322071
+ "sign_in",
321613
322072
  "signup",
321614
322073
  "sign-up",
322074
+ "sign_up",
321615
322075
  "register",
321616
322076
  "logout",
321617
322077
  "log-out",
322078
+ "log_out",
321618
322079
  "signout",
321619
322080
  "sign-out",
322081
+ "sign_out",
321620
322082
  "sso",
321621
322083
  "saml",
321622
322084
  "oidc"
321623
322085
  ]);
322086
+ var AUTH_FLOW_PARENT_SEGMENTS = /* @__PURE__ */ new Set(["api", "auth", "users", "user", "account", "accounts", "identity", "id"]);
321624
322087
  function isAuthFlowRoute(route2) {
321625
- const normalized = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).toLowerCase();
321626
- return normalized.split(/[/#]/).some((segment) => AUTH_FLOW_SEGMENTS.has(segment));
322088
+ const segments = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).toLowerCase().split(/[/#]/).filter(Boolean);
322089
+ if (segments.length === 0)
322090
+ return false;
322091
+ if (AUTH_FLOW_SEGMENTS.has(segments[0]))
322092
+ return true;
322093
+ return segments.length > 1 && AUTH_FLOW_PARENT_SEGMENTS.has(segments[0]) && AUTH_FLOW_SEGMENTS.has(segments[1]);
322094
+ }
322095
+ function hasAuthFlowSegment(route2) {
322096
+ return (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).toLowerCase().split(/[/#]/).filter(Boolean).some((segment) => AUTH_FLOW_SEGMENTS.has(segment));
321627
322097
  }
321628
322098
  function routePathForScope(route2, origin) {
321629
322099
  const trimmed = (route2 ?? "").trim();
@@ -321647,7 +322117,7 @@ var require_survey_scope = __commonJS({
321647
322117
  return trimmed;
321648
322118
  }
321649
322119
  }
321650
- if (trimmed.startsWith("/") && trimmed.includes("..")) {
322120
+ if (trimmed.startsWith("/") && (trimmed.includes("..") || /%2e/i.test(trimmed))) {
321651
322121
  try {
321652
322122
  const resolved = new URL(trimmed, "https://scope.invalid");
321653
322123
  return `${resolved.pathname}${resolved.search}${resolved.hash}`;
@@ -321667,20 +322137,21 @@ var require_survey_scope = __commonJS({
321667
322137
  }
321668
322138
  return lines.join("\n");
321669
322139
  }
322140
+ function routeWithinSurveyBasePath(route2, scope) {
322141
+ const path = routePathForScope(route2, scope.origin);
322142
+ if (path === null)
322143
+ return false;
322144
+ if ((0, exploration_parser_js_1.routeIsWithinFocusAreas)(path, [scope.basePath]))
322145
+ return true;
322146
+ const norm = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(path);
322147
+ return norm === `${scope.basePath}#` || norm.startsWith(`${scope.basePath}#/`);
322148
+ }
321670
322149
  function filterFeatureGroupsToPathScope(featureGroups, baseUrl) {
321671
322150
  const scope = deriveSurveyPathScope(baseUrl);
321672
322151
  if (!scope) {
321673
- return { kept: featureGroups, droppedGroups: [], trimmedRoutes: [], trimmedPages: [], basePath: null };
322152
+ return { kept: featureGroups, droppedGroups: [], trimmedRoutes: [], trimmedPages: [], basePath: null, failedOpen: false };
321674
322153
  }
321675
- const withinBase = (route2) => {
321676
- const path = routePathForScope(route2, scope.origin);
321677
- if (path === null)
321678
- return false;
321679
- if ((0, exploration_parser_js_1.routeIsWithinFocusAreas)(path, [scope.basePath]))
321680
- return true;
321681
- const norm = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(path);
321682
- return norm === `${scope.basePath}#` || norm.startsWith(`${scope.basePath}#/`);
321683
- };
322154
+ const withinBase = (route2) => routeWithinSurveyBasePath(route2, scope);
321684
322155
  const allowedAuthFlow = (route2) => {
321685
322156
  const path = routePathForScope(route2, scope.origin);
321686
322157
  return path !== null && isAuthFlowRoute(path);
@@ -321730,7 +322201,23 @@ var require_survey_scope = __commonJS({
321730
322201
  trimmedPages.push({ name: group.name, routes: removedPages });
321731
322202
  }
321732
322203
  }
321733
- return { kept, droppedGroups, trimmedRoutes, trimmedPages, basePath: scope.basePath };
322204
+ if (kept.length === 0 && droppedGroups.length > 0) {
322205
+ const redirectSignature = featureGroups.some((group) => (Array.isArray(group.routes) ? group.routes : []).some((route2) => {
322206
+ const path = routePathForScope(route2, scope.origin);
322207
+ return path !== null && !isAuthFlowRoute(path);
322208
+ }));
322209
+ if (redirectSignature) {
322210
+ return {
322211
+ kept: featureGroups,
322212
+ droppedGroups: [],
322213
+ trimmedRoutes: [],
322214
+ trimmedPages: [],
322215
+ basePath: scope.basePath,
322216
+ failedOpen: true
322217
+ };
322218
+ }
322219
+ }
322220
+ return { kept, droppedGroups, trimmedRoutes, trimmedPages, basePath: scope.basePath, failedOpen: false };
321734
322221
  }
321735
322222
  }
321736
322223
  });
@@ -321751,8 +322238,10 @@ var require_discover_explorer = __commonJS({
321751
322238
  exports.shouldSurfaceSurveyModalTrigger = shouldSurfaceSurveyModalTrigger;
321752
322239
  exports.normalizeAssistantToolCallArgs = normalizeAssistantToolCallArgs;
321753
322240
  exports.getMaxIterations = getMaxIterations;
322241
+ exports.computeBudgetDecision = computeBudgetDecision;
321754
322242
  exports.updatePendingRouteScreenshot = updatePendingRouteScreenshot;
321755
322243
  exports.consumePendingRouteScreenshot = consumePendingRouteScreenshot;
322244
+ exports.buildSystemPrompt = buildSystemPrompt;
321756
322245
  exports.runExplorePhase = runExplorePhase;
321757
322246
  var crypto_1 = __require("crypto");
321758
322247
  var ai_provider_js_1 = require_ai_provider();
@@ -321776,6 +322265,7 @@ var require_discover_explorer = __commonJS({
321776
322265
  var evidence_harvester_js_1 = require_evidence_harvester();
321777
322266
  var exploration_parser_js_1 = require_exploration_parser();
321778
322267
  var survey_link_frontier_js_1 = require_survey_link_frontier();
322268
+ var survey_door_harvester_js_1 = require_survey_door_harvester();
321779
322269
  var survey_scope_js_1 = require_survey_scope();
321780
322270
  var snapshot_context_compactor_js_2 = require_snapshot_context_compactor();
321781
322271
  Object.defineProperty(exports, "formatMCPToolResult", { enumerable: true, get: function() {
@@ -321929,6 +322419,46 @@ var require_discover_explorer = __commonJS({
321929
322419
  }
321930
322420
  return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_DEFAULT", DEFAULT_MAX_EXPLORE_ITERATIONS_DEFAULT);
321931
322421
  }
322422
+ var DEFAULT_ADAPTIVE_BUDGET_HARD_CAP = 200;
322423
+ var ADAPTIVE_BUDGET_FINALIZE_RESERVE = 5;
322424
+ var ADAPTIVE_BUDGET_ITERS_PER_ROUTE = 4;
322425
+ var ADAPTIVE_BUDGET_MIN_LEASE = 10;
322426
+ var ADAPTIVE_BUDGET_MAX_LEASE = 30;
322427
+ var ADAPTIVE_BUDGET_DISCOVERY_LEASE = 20;
322428
+ var ADAPTIVE_BUDGET_RESCUE_WINDOW = 10;
322429
+ var ADAPTIVE_BUDGET_MIN_GAP = 10;
322430
+ var ADAPTIVE_BUDGET_DECISION_PCT = 0.9;
322431
+ var ADAPTIVE_BUDGET_PROGRESS_WINDOW = 15;
322432
+ function computeBudgetDecision(input) {
322433
+ const { iteration, currentCap, hardCap, remainingRoutes, capturing, freshDiscovery, discoveryLeaseUsed, rescueUsed, allowExtension } = input;
322434
+ if (remainingRoutes <= 0)
322435
+ return { action: "finish", reason: "drained" };
322436
+ if (iteration >= hardCap)
322437
+ return { action: "finish", reason: "hard-cap" };
322438
+ const tail = Math.max(0, currentCap - iteration);
322439
+ const needed = ADAPTIVE_BUDGET_FINALIZE_RESERVE + ADAPTIVE_BUDGET_ITERS_PER_ROUTE * remainingRoutes;
322440
+ if (allowExtension && capturing) {
322441
+ if (currentCap >= hardCap)
322442
+ return { action: "none" };
322443
+ const shortfall = needed - tail;
322444
+ if (shortfall <= 0)
322445
+ return { action: "none" };
322446
+ const clamped = Math.min(Math.max(shortfall, ADAPTIVE_BUDGET_MIN_LEASE), ADAPTIVE_BUDGET_MAX_LEASE);
322447
+ const delta = Math.min(hardCap - currentCap, clamped);
322448
+ return { action: "extend", delta, newCap: currentCap + delta, lease: "capture" };
322449
+ }
322450
+ if (allowExtension && freshDiscovery && !discoveryLeaseUsed) {
322451
+ if (currentCap >= hardCap)
322452
+ return { action: "none" };
322453
+ if (tail >= needed)
322454
+ return { action: "none" };
322455
+ const delta = Math.min(hardCap - currentCap, ADAPTIVE_BUDGET_DISCOVERY_LEASE);
322456
+ return { action: "extend", delta, newCap: currentCap + delta, lease: "discovery" };
322457
+ }
322458
+ if (!rescueUsed)
322459
+ return { action: "rescue" };
322460
+ return { action: "finish", reason: "stalled" };
322461
+ }
321932
322462
  function updatePendingRouteScreenshot(pending, { imageData, detectedRoute, fallbackRoute }) {
321933
322463
  if (imageData) {
321934
322464
  return {
@@ -322125,8 +322655,9 @@ NEVER assert: user-specific text (names, emails), counts/numbers, timestamps, ma
322125
322655
  };
322126
322656
  function buildSystemPrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, options) {
322127
322657
  const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
322128
- const { secrets: explorerSecretVars, publics: explorerPublicVars } = (0, credential_tools_js_1.partitionTestVariables)(browserCredentials);
322658
+ const { secrets: explorerSecretVars, publics: explorerPublicVarsRaw } = (0, credential_tools_js_1.partitionTestVariables)(browserCredentials);
322129
322659
  const hasCredentials = Object.keys(explorerSecretVars).length > 0;
322660
+ const { DISCOVERY_SHOULD_REGISTER: _omitRegisterFlag, ...explorerPublicVars } = explorerPublicVarsRaw;
322130
322661
  const explorerPublicLines = (0, credential_tools_js_1.renderPublicVariablesBlock)(explorerPublicVars);
322131
322662
  const credentialBlock = `${explorerPublicLines ? `
322132
322663
  ## Test Variables (public)
@@ -322136,6 +322667,7 @@ ${explorerPublicLines}
322136
322667
  const inboxBlock = options?.inboxPrompt ? `
322137
322668
  ${options.inboxPrompt}` : "";
322138
322669
  const isRegisterMode = !!options?.inboxPrompt;
322670
+ const emailVerificationUnavailable = !isRegisterMode && !!options?.emailVerificationUnavailable;
322139
322671
  const focusAreas = options?.focusAreas;
322140
322672
  let alreadyDiscoveredPages = "";
322141
322673
  if (existingSiteMap && existingSiteMap.pages.length > 0) {
@@ -322157,21 +322689,27 @@ ${options.inboxPrompt}` : "";
322157
322689
  const isIncremental = !!(options?.incremental && mode === "survey");
322158
322690
  const MAX_EXPLORE_ITERATIONS = getMaxIterations(mode, options?.incremental);
322159
322691
  const isSurvey = mode === "survey";
322692
+ const adaptiveBudgetOn = isSurvey && !isIncremental && (process.env.DISCOVERY_ADAPTIVE_BUDGET === "1" || (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_ADAPTIVE_BUDGET)) && (0, survey_link_frontier_js_1.isSurveyLinkFrontierEnabled)();
322693
+ const surveyPathScope = isSurvey ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
322160
322694
  if (isSurvey) {
322161
- return `You are a QA exploration agent. Your goal: MAP an entire web application by visiting every reachable page and cataloging its structure. You are NOT testing - you are building a complete site map so downstream agents know what to test.
322695
+ return `You are a QA exploration agent. Your goal: ${surveyPathScope ? `MAP one flow of a web application in ISOLATION \u2014 the flow under \`${surveyPathScope.basePath}\` \u2014 visiting every reachable page INSIDE that scope and cataloging its structure` : "MAP an entire web application by visiting every reachable page and cataloging its structure"}. You are NOT testing - you are building a ${surveyPathScope ? "site map of this flow" : "complete site map"} so downstream agents know what to test.
322162
322696
 
322163
322697
  ## Strategy
322164
322698
  1. ${isRegisterMode ? `Navigate to the base URL. BEFORE registering, explore the unauthenticated experience first:
322165
322699
  - Find all reachable public auth pages from the logged-out experience: Login, Register/Sign Up, Forgot Password - check menus, drawers, and modal auth switches too
322166
322700
  - For each: browser_snapshot \u2192 capture_page \u2192 record_transition
322167
322701
  - Do NOT submit the password-reset form - only catalog it (survey rule: map, do not mutate)
322168
- - After capturing the public auth pages, REGISTER a new account by following the Account Registration section below, then continue mapping the authenticated app` : hasCredentials ? `Navigate to the base URL. BEFORE logging in, explore the unauthenticated experience first:
322702
+ - After capturing the public auth pages, REGISTER a new account by following the Account Registration section below, then continue mapping the authenticated app` : emailVerificationUnavailable ? `Navigate to the base URL. NOTE: this project asked to REGISTER a new account, but this runner CANNOT receive email (verification inbox unavailable). Registration may still work if the app does not require email verification:
322703
+ - FIRST explore the unauthenticated experience: find all reachable public auth pages (Login, Register/Sign Up, Forgot Password - check menus, drawers, and modal auth switches too) and capture each: browser_snapshot \u2192 capture_page \u2192 record_transition. Do NOT submit the password-reset form
322704
+ - THEN attempt registration ONCE: fill the signup form via \`fill_credential_form\` with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys (only if no TEST_USER_EMAIL key is listed, use a fresh \`testuser-<timestamp>@example.com\`), submit it ONCE, and never alter the email or create a second account. If signup says the address is already registered, log in with the same keys instead
322705
+ - If you end up signed in WITHOUT an email round-trip, continue mapping the authenticated app normally
322706
+ - THE WALL: if any step demands an emailed code/link ("check your email", OTP, magic link), STOP that flow immediately \u2014 never type a guessed code, never click Resend \u2014 keep mapping the public app, and state in finish_exploration that authenticated coverage is BLOCKED (cannot receive verification email)` : hasCredentials ? `Navigate to the base URL. BEFORE logging in, explore the unauthenticated experience first:
322169
322707
  - Find all reachable public auth pages from the logged-out experience: Login, Register/Sign Up, Forgot Password - check menus, drawers, and modal auth switches too
322170
322708
  - For each: browser_snapshot \u2192 capture_page \u2192 record_transition
322171
322709
  - Do NOT submit registration or password-reset forms - only catalog the page structure (survey rule: map, do not mutate)
322172
322710
  - After capturing all reachable public auth pages, use the credentials below to log in and continue mapping the authenticated app` : "Navigate to the base URL"}
322173
322711
  2. Take a \`browser_snapshot\` on the landing page. **IMPORTANT: NEVER pass a filename parameter to browser_snapshot - always call it with no arguments so the snapshot is returned inline.**
322174
- 3. **BUILD YOUR CHECKLIST**: Before clicking anything, list the navigation links visible in the snapshot - sidebar, header, footer, user menus, hamburger menus. If the snapshot output says some navigation chrome was omitted, immediately call \`capture_page\` on the current page and use its structured nav list to complete the checklist before proceeding. This is your exploration checklist. Visit each one in order
322712
+ 3. **BUILD YOUR CHECKLIST**: Before clicking anything, list the navigation links visible in the snapshot - sidebar, header, footer, user menus, hamburger menus. If the snapshot output says some navigation chrome was omitted, immediately call \`capture_page\` on the current page and use its structured nav list to complete the checklist before proceeding. This is your exploration checklist. Visit each one in order${surveyPathScope ? ` \u2014 but ONLY the links inside the entry scope \`${surveyPathScope.basePath}\`. Links leading outside it (global header/footer, a parent hub, sibling sections) are OUT of scope: leave them off the checklist, do not visit them` : ""}
322175
322713
  - **ALWAYS navigate by CLICKING links in the page**, not by typing URLs into browser_navigate. Clicking captures the exact locator that downstream test generators need. Direct URL navigation loses this information.
322176
322714
  4. Visit each navigation target systematically. On each page:
322177
322715
  - \`browser_snapshot\` to see the page
@@ -322182,8 +322720,8 @@ ${options.inboxPrompt}` : "";
322182
322720
  5. **Tabs and sub-sections**: If a page has tabs, click EACH tab and call \`capture_page\` for each tab state. Use a route suffix like \`/settings?tab=profile\`, \`/settings?tab=billing\`
322183
322721
  6. **Hidden navigation**: Look for dropdown menus, user/avatar menus (click the avatar), hamburger menus, "More..." links, breadcrumbs. Click to OPEN these to discover navigation targets
322184
322722
  7. **List pages**: If a page shows a list of items with links (e.g. /users/1, /users/2), click ONE item to discover the detail page template. Do NOT visit every item - they are the same page pattern
322185
- 8. **Error page**: Navigate to a path that does not exist (e.g. \`/this-page-does-not-exist-404\`) to capture the app's error page
322186
- 9. After visiting ALL reachable pages, call \`finish_exploration\`
322723
+ 8. **Error page**: do NOT probe made-up routes to capture the app's error page \u2014 nothing downstream uses a probed 404. Only catalog an error page if the app itself routes you to one (broken link, failed action, redirect)
322724
+ 9. After visiting ALL reachable ${surveyPathScope ? "in-scope " : ""}pages, call \`finish_exploration\`
322187
322725
  You do NOT have a \`record_journey\` tool - your job is mapping, not testing. Downstream agents will create tests from your site map.
322188
322726
 
322189
322727
  ## What to catalog (capture_page)
@@ -322208,7 +322746,7 @@ ${(0, survey_scope_js_1.buildSurveyScopeBoundary)(baseUrl)}
322208
322746
  - **OAuth/SSO**: If login redirects to an external provider (Google, GitHub), do not follow it. Note that the app uses OAuth and explore unauthenticated pages only
322209
322747
  - **Auth failure**: If login${isRegisterMode ? " or account registration" : ""} fails after 2 attempts, capture the auth page and explore unauthenticated pages. Note the failure in your \`finish_exploration\` summary
322210
322748
  - **Parameterized routes**: /users/1 and /users/2 are the same page template. Visit ONE instance, not all of them
322211
- - Max iterations: ${MAX_EXPLORE_ITERATIONS}. You will be warned at 90%. Use your full budget to discover every page thoroughly
322749
+ - ${adaptiveBudgetOn ? `Initial iteration budget: ${MAX_EXPLORE_ITERATIONS}. If the map is still incomplete as you approach it, the harness may GRANT you more iterations (up to a hard cap) - an explicit "GRANTED: ..." message supersedes this number. Use your budget to discover every page thoroughly` : `Max iterations: ${MAX_EXPLORE_ITERATIONS}. You will be warned at 90%. Use your full budget to discover every page thoroughly`}
322212
322750
 
322213
322751
  ${appBrief ? `## Application Brief
322214
322752
  The user provided this description of their application:
@@ -322230,7 +322768,7 @@ If the user mentioned a specific page that changed (e.g. "Settings was redesigne
322230
322768
  - Check for Login, Register/Sign Up, Forgot Password - including behind menus, drawers, and modal auth switches
322231
322769
  - For each NEW auth page: browser_snapshot \u2192 capture_page \u2192 record_transition
322232
322770
  - Do NOT submit the password-reset form (survey rule: map, do not mutate)
322233
- - After capturing any new public auth pages, REGISTER a new account (see the Account Registration section) and continue` : hasCredentials ? `Navigate to the base URL. BEFORE logging in, explore the unauthenticated experience for any NEW public auth pages not in the known routes above:
322771
+ - After capturing any new public auth pages, REGISTER a new account (see the Account Registration section) and continue` : emailVerificationUnavailable ? `Navigate to the base URL. NOTE: registration was requested for this project but this runner CANNOT receive email (verification inbox unavailable). Attempt registration ONCE with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys via \`fill_credential_form\` (if signup says the address is already registered, log in with the same keys). If you get signed in without an email round-trip, map NEW authenticated pages normally. If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code \u2014 map only NEW public pages and note in finish_exploration that authenticated coverage is BLOCKED (cannot receive verification email)` : hasCredentials ? `Navigate to the base URL. BEFORE logging in, explore the unauthenticated experience for any NEW public auth pages not in the known routes above:
322234
322772
  - Check for Login, Register/Sign Up, Forgot Password - including behind menus, drawers, and modal auth switches
322235
322773
  - For each NEW auth page: browser_snapshot \u2192 capture_page \u2192 record_transition
322236
322774
  - Do NOT submit registration or password-reset forms (survey rule: map, do not mutate)
@@ -322248,7 +322786,9 @@ ${surfaceIntelligence ? "## Prior Recordings\n" + surfaceIntelligence + "\n" : "
322248
322786
 
322249
322787
  ## App Details
322250
322788
  - Base URL: ${baseUrl}
322251
- ${credentialBlock}${redactedBlock}${inboxBlock}`;
322789
+ ${credentialBlock}${redactedBlock}${inboxBlock}${emailVerificationUnavailable ? `
322790
+
322791
+ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
322252
322792
  }
322253
322793
  return `You are an expert QA exploration agent. Your job is to USE a web application like a real user - interact with every feature you find, observe what happens, and catalog the results.
322254
322794
 
@@ -322275,7 +322815,7 @@ Every feature you discover should be USED, not just cataloged. The test generato
322275
322815
 
322276
322816
  ## Strategy
322277
322817
  1. Navigate to the BASE URL (the landing/home page). ALWAYS start here - never deep-link directly to sub-pages like /auth/login or /dashboard.
322278
- 2. From the landing page, find and use the app's own navigation to reach features.${isRegisterMode ? " First create your account by following the Account Registration section below, then continue." : hasCredentials ? " Find the login/sign-in link and log in using the credentials below." : ""}
322818
+ 2. From the landing page, find and use the app's own navigation to reach features.${isRegisterMode ? " First create your account by following the Account Registration section below, then continue." : emailVerificationUnavailable ? " NOTE: registration was requested for this project but this runner CANNOT receive email (verification inbox unavailable). First attempt registration ONCE with the `TEST_USER_EMAIL` + `TEST_USER_PASSWORD` credential keys via `fill_credential_form` (never alter the email; if it is already registered, log in with the same keys). If you are signed in without an email round-trip, continue as a normal logged-in user. If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code \u2014 explore what is publicly reachable and state in finish_exploration that authenticated coverage is BLOCKED (cannot receive verification email)." : hasCredentials ? " Find the login/sign-in link and log in using the credentials below." : ""}
322279
322819
  3. On each page: take a \`browser_snapshot\`, identify what features/actions are available
322280
322820
  4. **USE the features on the page:**
322281
322821
  - Forms: fill every field with realistic data, submit, observe the result (success page, validation errors, redirect, toast messages)
@@ -322293,7 +322833,11 @@ Every feature you discover should be USED, not just cataloged. The test generato
322293
322833
  - Do NOT split validation, invalid input, and recovery into separate tests - combine them into ONE comprehensive test
322294
322834
  c) For multi-section pages (profile with tabs, settings with categories): test each DISTINCT section as a separate happy path test
322295
322835
  Focus on meaningful coverage: every form should have at least a happy path and a validation test. Multi-section pages (tabs, categories) should test each distinct section.
322296
- **Auth pages (login/register) - SPECIAL ORDER: test NEGATIVE before positive:**
322836
+ ${emailVerificationUnavailable ? ` **Auth pages (login/register) - registration requested but this runner CANNOT receive email:**
322837
+ - Step 1: Test negative flows FIRST while not logged in: on the LOGIN form, submit empty fields and invalid credentials (synthetic values like "wrong@example.com" / "badpassword" via \`browser_type\` \u2014 nothing can be created there); on the REGISTRATION form, empty-submit validation ONLY (a filled synthetic signup could CREATE a stray account on a permissive app). Call \`record_journey\` ONCE for the combined validation flow
322838
+ - Step 2: Attempt the happy path ONCE via REGISTRATION: \`fill_credential_form\` with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys, submit the signup form ONCE. Never alter the email or create a second account; if signup says the address is already registered, log in with the same keys instead
322839
+ - Step 3: If you are signed in with NO email round-trip, call \`record_journey\` for the happy path and continue exploring as a logged-in user
322840
+ - THE WALL: if any step demands an emailed code/link (verification code, OTP, magic link, "check your email"), STOP immediately \u2014 never type a guessed code, never click Resend \u2014 record only the validation journey and note in \`finish_exploration\` that the authenticated happy path is BLOCKED (cannot receive verification email)` : ` **Auth pages (login/register) - SPECIAL ORDER: test NEGATIVE before positive:**
322297
322841
  - You arrive at the login/register page NOT logged in - use this state to test errors FIRST
322298
322842
  - Step 1: Submit with empty fields \u2192 observe validation errors
322299
322843
  - Step 2: Submit with invalid credentials (wrong password, bad email) \u2192 observe error messages
@@ -322301,7 +322845,7 @@ Every feature you discover should be USED, not just cataloged. The test generato
322301
322845
  - Step 4: NOW do the happy path - log in with valid credentials \u2192 observe redirect to dashboard
322302
322846
  - Step 5: Call \`record_journey\` for the happy path
322303
322847
  - This order avoids needing to log out between tests. After the happy path, you are logged in and ready to explore other features
322304
- - **Login filling**: ALWAYS use \`fill_credential_form\` for the happy-path login \u2014 never \`browser_fill_form\` or \`browser_type\` with the actual email/password (those calls will be blocked). Example: \`fill_credential_form({ fields: [{ field: "Email", credentialKey: "TEST_USER_EMAIL" }, { field: "Password", credentialKey: "TEST_USER_PASSWORD" }] })\`. For sequential fills, use \`fill_credential\` per field. Pure validation flows (empty submit, deliberately wrong password) can use \`browser_type\` with synthetic invalid values like "wrong@example.com" / "badpassword" \u2014 those are not real credentials and won't be blocked.
322848
+ - **Login filling**: ALWAYS use \`fill_credential_form\` for the happy-path login \u2014 never \`browser_fill_form\` or \`browser_type\` with the actual email/password (those calls will be blocked). Example: \`fill_credential_form({ fields: [{ field: "Email", credentialKey: "TEST_USER_EMAIL" }, { field: "Password", credentialKey: "TEST_USER_PASSWORD" }] })\`. For sequential fills, use \`fill_credential\` per field. Pure validation flows (empty submit, deliberately wrong password) can use \`browser_type\` with synthetic invalid values like "wrong@example.com" / "badpassword" \u2014 those are not real credentials and won't be blocked.`}
322305
322849
  **Logging out (when needed for other reasons):**
322306
322850
  - To find the logout button: look in the user/avatar menu (click the profile icon or avatar in the header), check sidebar bottom, check settings page
322307
322851
  - Do NOT try navigating to /logout or /auth/logout directly - these URLs rarely exist. Always find and click the actual logout button in the UI
@@ -322352,25 +322896,38 @@ Every feature you discover should be USED, not just cataloged. The test generato
322352
322896
 
322353
322897
  GOOD (specific, with routes, stable assertions):
322354
322898
  \`\`\`
322355
- record_journey({
322899
+ ${emailVerificationUnavailable ? `record_journey({
322900
+ name: "Contact Form - Validation",
322901
+ route: "/contact",
322902
+ routes: ["/", "/contact"],
322903
+ steps: ["Navigate to /", "Click 'Contact' link", "Submit the empty form", "Observe validation errors"],
322904
+ expectedOutcome: "URL matches /contact. Validation error 'Email is required' is visible.",
322905
+ assertions: [{ type: "url", value: "/contact" }, { type: "error-text", value: "Email is required" }]
322906
+ })` : `record_journey({
322356
322907
  name: "Login - Happy Path",
322357
322908
  route: "/auth/login",
322358
322909
  routes: ["/", "/auth/login", "/dashboard"],
322359
322910
  steps: ["Navigate to /", "Click 'Sign In' link", "Fill email and password with test credentials", "Click 'Log In' button", "Observe redirect to /dashboard"],
322360
322911
  expectedOutcome: "URL matches /dashboard. Sidebar link 'Overview' is visible.",
322361
322912
  assertions: [{ type: "url", value: "/dashboard" }, { type: "nav-link", locator: "getByRole('link', { name: 'Overview' })" }]
322362
- })
322913
+ })`}
322363
322914
  \`\`\`
322364
322915
 
322365
322916
  BAD (vague, missing routes, marketing assertions):
322366
322917
  \`\`\`
322367
- record_journey({
322918
+ ${emailVerificationUnavailable ? `record_journey({
322919
+ name: "Contact test",
322920
+ route: "/contact",
322921
+ routes: ["/contact"],
322922
+ steps: ["Try the contact form"],
322923
+ expectedOutcome: "User sees 'The Best Platform Ever' heading"
322924
+ })` : `record_journey({
322368
322925
  name: "Login test",
322369
322926
  route: "/auth/login",
322370
322927
  routes: ["/auth/login"],
322371
322928
  steps: ["Login to the app"],
322372
322929
  expectedOutcome: "User sees dashboard with 'Welcome to the Best Platform Ever' heading"
322373
- })
322930
+ })`}
322374
322931
  \`\`\`
322375
322932
 
322376
322933
  ## Bug Detection - CRITICAL
@@ -322415,7 +322972,7 @@ Exception: if you need to navigate THROUGH other pages to reach a focus area (e.
322415
322972
  2. Take a \`browser_snapshot\` to understand what's on the page
322416
322973
  3. **HAPPY PATH**: Fill ALL form fields with valid/realistic data, click the primary action button (Submit, Generate, Save, Create, etc.), and observe the result. For AI/async operations, wait for the result using \`browser_wait_for\` before taking a snapshot. Call \`record_journey\` to document this flow
322417
322974
  4. **VALIDATION/ERROR**: Go back to the page. Try submitting with empty required fields. Try invalid data (wrong email format, short password, etc.). Observe ALL validation messages. Fix errors and resubmit successfully. Call \`record_journey\` for the combined validation+recovery flow
322418
- - **Exception for auth pages (login/register)**: Do validation/errors FIRST (step 4 before step 3) while you are not logged in, then happy path last. See the auth-specific instructions above
322975
+ - **Exception for auth pages (login/register)**: ${emailVerificationUnavailable ? "validation FIRST, then ONE registration attempt with the TEST_USER_EMAIL + TEST_USER_PASSWORD credential keys \u2014 if an emailed code/link is demanded, STOP (never guess a code) and note the happy path as BLOCKED (cannot receive verification email). See the auth-specific instructions above" : "Do validation/errors FIRST (step 4 before step 3) while you are not logged in, then happy path last. See the auth-specific instructions above"}
322419
322976
  5. **ADDITIONAL INTERACTIONS**: Click buttons, open dropdowns, test tabs/sections, try search if available. If there are distinct sub-sections (e.g. tabs, accordion panels), test each one
322420
322977
  6. After each interaction, call \`capture_page\` with detailed observations
322421
322978
  7. **ONLY after completing steps 1-6 for ALL focus areas**, call \`finish_exploration\`
@@ -322517,7 +323074,9 @@ ${surfaceIntelligence}
322517
323074
 
322518
323075
  ## App Details
322519
323076
  - Base URL: ${baseUrl}
322520
- ${credentialBlock}${redactedBlock}${inboxBlock}`;
323077
+ ${credentialBlock}${redactedBlock}${inboxBlock}${emailVerificationUnavailable ? `
323078
+
323079
+ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
322521
323080
  }
322522
323081
  async function runExplorePhase(mcpClient, baseUrl, variableContext, existingSiteMap, projectId, existingTestNames, options, config) {
322523
323082
  const logs2 = [];
@@ -322561,6 +323120,7 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322561
323120
  const perceptionEnricherEnabled = ((0, shared_1.parseBoolFlag)(process.env.DISCOVERY_PERCEPTION_ENRICHER) || process.env.DISCOVERY_PERCEPTION_ENRICHER === "1") && typeof options?.getPerceptionPage === "function";
322562
323121
  const linkFrontierEnabled = (0, survey_link_frontier_js_1.isSurveyLinkFrontierEnabled)() && options?.mode === "survey";
322563
323122
  const linkScope = (0, survey_link_frontier_js_1.deriveLinkScope)(baseUrl);
323123
+ const surveyNavScope = options?.mode === "survey" ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
322564
323124
  let finishRedirects = 0;
322565
323125
  let lastLandedPageUrl = null;
322566
323126
  const state2 = (0, exploration_state_js_1.createExplorationState)();
@@ -322589,6 +323149,50 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322589
323149
  logs2.push(` [link-frontier] ingestion skipped - ${err instanceof Error ? err.message : String(err)}`);
322590
323150
  }
322591
323151
  }
323152
+ const domHarvestEnabled = linkFrontierEnabled && (0, survey_door_harvester_js_1.isDomHarvestEnabled)();
323153
+ const menuExpandEnabled = domHarvestEnabled && (0, survey_door_harvester_js_1.isMenuExpandEnabled)();
323154
+ const menuExpandAttemptedRoutes = /* @__PURE__ */ new Set();
323155
+ async function harvestDomLinksIntoFrontier() {
323156
+ if (!domHarvestEnabled)
323157
+ return;
323158
+ const currentScoped = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(state2.currentRoute);
323159
+ const currentRs = state2.routeState.get(state2.currentRoute);
323160
+ try {
323161
+ const expandHere = menuExpandEnabled && !menuExpandAttemptedRoutes.has(currentScoped);
323162
+ if (expandHere)
323163
+ menuExpandAttemptedRoutes.add(currentScoped);
323164
+ const harvest = await (0, survey_door_harvester_js_1.harvestDomLinks)(mcpClient, {
323165
+ expandMenus: expandHere,
323166
+ log: (m) => logs2.push(m)
323167
+ });
323168
+ if (harvest.pageUrl)
323169
+ lastLandedPageUrl = harvest.pageUrl;
323170
+ if (harvest.links.length > 0) {
323171
+ (0, survey_link_frontier_js_1.ingestSeenLinks)(state2, {
323172
+ links: (0, survey_door_harvester_js_1.toSeenLinks)(harvest.links),
323173
+ scope: linkScope,
323174
+ isEntryOrHub: currentScoped === linkScope.basePath || state2.iteration <= 2,
323175
+ isErrorContext: currentRs?.pageType === "ERROR",
323176
+ pageUrl: harvest.pageUrl ?? lastLandedPageUrl,
323177
+ provenance: "dom",
323178
+ log: (m) => logs2.push(m)
323179
+ });
323180
+ }
323181
+ if (harvest.navigated && harvest.originalUrl) {
323182
+ logs2.push(` [door-harvest] page moved during menu expansion \u2014 restoring ${harvest.originalUrl}`);
323183
+ try {
323184
+ const navResult = await mcpClient.callTool({ name: "browser_navigate", arguments: { url: harvest.originalUrl } });
323185
+ if (navResult && typeof navResult === "object" && navResult.isError === true) {
323186
+ logs2.push(" [door-harvest] restore navigate returned an error \u2014 model may be on a different page");
323187
+ }
323188
+ } catch (navErr) {
323189
+ logs2.push(` [door-harvest] restore failed - ${navErr instanceof Error ? navErr.message : String(navErr)}`);
323190
+ }
323191
+ }
323192
+ } catch (err) {
323193
+ logs2.push(` [door-harvest] skipped - ${err instanceof Error ? err.message : String(err)}`);
323194
+ }
323195
+ }
322592
323196
  const recentExchanges = [];
322593
323197
  const transientDirectives = [];
322594
323198
  const MAX_RECENT_EXCHANGES = 28;
@@ -322617,7 +323221,8 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322617
323221
  let inboxState = null;
322618
323222
  let inboxReader = null;
322619
323223
  const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
322620
- if (inboxConfig && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext)) {
323224
+ const registerIntent = (0, inbox_tools_js_1.hasRegisterIntent)(variableContext);
323225
+ if (inboxConfig && registerIntent) {
322621
323226
  inboxState = (0, inbox_tools_js_1.createInboxRunState)(inboxConfig, {
322622
323227
  tagSeed: baseUrl,
322623
323228
  projectScope: projectId,
@@ -322635,8 +323240,19 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322635
323240
  log: (line) => logs2.push(line)
322636
323241
  });
322637
323242
  logs2.push(`Verification inbox ENABLED for registration \u2014 email ${inboxState.email}`);
322638
- } else if ((0, inbox_tools_js_1.clearThrowawayRegisterSeed)(variableContext)) {
322639
- logs2.push("Register intent set but verification inbox not configured here \u2014 dropped the throwaway TEST_USER_EMAIL seed so the agent registers a fresh account.");
323243
+ } else {
323244
+ if (registerIntent && !inboxConfig) {
323245
+ const gap = (0, verification_inbox_js_1.describeVerificationInboxConfigGap)() ?? "verification inbox unavailable";
323246
+ logs2.push(`Register intent set but the ${gap} on this runner \u2014 keeping the seeded register identity for a one-shot registration attempt.`);
323247
+ } else if ((0, inbox_tools_js_1.clearThrowawayRegisterSeed)(variableContext)) {
323248
+ logs2.push("Stale throwaway TEST_USER_EMAIL seed found (register intent not set) \u2014 dropped it so the agent does not log in with a never-registered account.");
323249
+ }
323250
+ }
323251
+ const emailVerificationUnavailable = registerIntent && !inboxConfig && !(0, credential_tools_js_1.hasUsableLoginIdentity)(variableContext.allTestVariables ?? variableContext.publicTestVariables, {
323252
+ isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail
323253
+ });
323254
+ if (emailVerificationUnavailable) {
323255
+ logs2.push("Email verification unavailable \u2014 the agent will ATTEMPT registration once; any flow requiring an emailed code/link will be reported BLOCKED.");
322640
323256
  }
322641
323257
  const inboxPromptBlock = inboxState ? (0, inbox_tools_js_1.buildInboxPromptBlock)({
322642
323258
  email: inboxState.email,
@@ -322674,7 +323290,8 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
322674
323290
  let systemPrompt = config?.systemPrompt ?? buildSystemPrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, {
322675
323291
  ...options,
322676
323292
  canRotateCredentials: !!options?.onCredentialUpdate,
322677
- inboxPrompt: inboxPromptBlock
323293
+ inboxPrompt: inboxPromptBlock,
323294
+ emailVerificationUnavailable
322678
323295
  });
322679
323296
  if (config?.systemPrompt && inboxPromptBlock) {
322680
323297
  systemPrompt += `
@@ -322685,7 +323302,14 @@ ${inboxPromptBlock}`;
322685
323302
  const hasLoginCredentials = Object.keys(browserSecretsForHint).length > 0;
322686
323303
  const aiClient = options?.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
322687
323304
  const isDeepMode = options?.mode === "deep";
322688
- const maxIterations = config?.maxIterationsOverride && config.maxIterationsOverride > 0 ? config.maxIterationsOverride : getMaxIterations(options?.mode ?? "full", options?.incremental);
323305
+ const baseMaxIterations = config?.maxIterationsOverride && config.maxIterationsOverride > 0 ? config.maxIterationsOverride : getMaxIterations(options?.mode ?? "full", options?.incremental);
323306
+ const adaptiveBudgetEnabled = process.env.DISCOVERY_ADAPTIVE_BUDGET === "1" || (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_ADAPTIVE_BUDGET);
323307
+ const adaptiveActive = adaptiveBudgetEnabled && isSurveyMode && linkFrontierEnabled && !options?.incremental && !(config?.maxIterationsOverride && config.maxIterationsOverride > 0);
323308
+ const adaptiveHardCap = adaptiveActive ? Math.max(baseMaxIterations, readPositiveIntEnv2(process.env, "DISCOVERY_ADAPTIVE_BUDGET_HARD_CAP", DEFAULT_ADAPTIVE_BUDGET_HARD_CAP)) : baseMaxIterations;
323309
+ let maxIterations = baseMaxIterations;
323310
+ if (adaptiveActive) {
323311
+ logs2.push(`Adaptive budget: ON (floor ${baseMaxIterations}, hard cap ${adaptiveHardCap})`);
323312
+ }
322689
323313
  const configKeepTail = isDeepMode ? Math.ceil(ctxConfig.keepTail * 1.5) : ctxConfig.keepTail;
322690
323314
  logs2.push(`Context: ${ctxConfig.tier} tier (${Math.round(ctxConfig.contextWindow / 1e3)}k window, emergency at ${Math.round(ctxConfig.emergencyTokenThreshold / 1e3)}k, prune every ${ctxConfig.pruneEveryN || "never"}, keepTail=${configKeepTail})`);
322691
323315
  logs2.push(`Discovery mode: ${useV3 ? "V3 (per-turn rebuild)" : "V2.5 (full transcript + control brief)"}${v3Env ? ` [override: V3=${v3Env}]` : " [auto]"}`);
@@ -322694,7 +323318,8 @@ ${inboxPromptBlock}`;
322694
323318
  const inboxPasswordKey = inboxState?.passwordKey ?? "TEST_USER_PASSWORD";
322695
323319
  const registerHint = ` follow the **Account Registration** steps in the system prompt to create and email-verify a new account (call provision_identity for the email, fill the signup form with that email + the \`${inboxPasswordKey}\` credential, submit, then complete the email verification)`;
322696
323320
  const surveyBreadthHint = linkFrontierEnabled ? " Visit and capture EVERY top-level link on your checklist BEFORE drilling into any section's sub-pages. The state packet's Frontier list tracks discovered-but-unvisited routes - drain it." : "";
322697
- const userMessage = config?.userMessage ?? (isSurveyMode ? registerMode ? `Navigate to ${baseUrl}. BEFORE registering, explore all reachable public auth pages from the logged-out experience (login, register, forgot-password - check menus and drawers too). Capture each with capture_page and record_transition. Do NOT submit the password-reset form. Then create your account:${registerHint}. After your account is verified and you are logged in, take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire application thoroughly.` : `Navigate to ${baseUrl}.${hasLoginCredentials ? ` BEFORE logging in, explore all reachable public auth pages from the logged-out experience (login, register, forgot-password - check menus and drawers too). Capture each with capture_page and record_transition. Do NOT submit registration or password-reset forms. After mapping public auth pages, log in.${loginHint}` : ""} After${hasLoginCredentials ? " logging in" : " landing"}, take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire application thoroughly.` : isDeepMode ? `Start by navigating to ${baseUrl}${registerMode ? ` and create your account:${registerHint}.` : hasLoginCredentials ? ` and log in.${loginHint}` : "."} Then go to the focus areas listed in the system prompt. For EACH focus area, you must: (1) test the happy path with valid data and call record_journey, (2) test validation/errors with invalid/empty data and call record_journey, (3) interact with all buttons/dropdowns/tabs on the page. You have ${maxIterations} iterations - use them all. Do NOT call finish_exploration until you have completed BOTH happy path AND validation testing for every focus area.` : `Start by navigating to ${baseUrl}${registerMode ? ` and create your account:${registerHint}.` : hasLoginCredentials ? ` and log in.${loginHint}` : "."} Then explore the application feature by feature. On each page, take a browser_snapshot and call capture_page. When you find forms or interactive features, actually USE them - fill in fields, click buttons, submit forms, and observe what happens. Capture every resulting page. Remember: visiting a page is not enough - interact with it like a real user would.`);
323321
+ const emailWallNote = ' NOTE: this project asked to REGISTER a new account, but this runner CANNOT receive email (verification inbox unavailable). Attempt registration ONCE with the `TEST_USER_EMAIL` + `TEST_USER_PASSWORD` credential keys via `fill_credential_form` (never alter the email or create a second account; if signup says the address is already registered, log in with the same keys instead). If you are signed in without an email round-trip, continue as a normal logged-in user. The moment any step demands an emailed code/link (verification code, OTP, magic link, "check your email"), STOP that flow - never type a guessed code, never click Resend - continue with publicly reachable pages and state in your finish_exploration summary that authenticated coverage is BLOCKED (cannot receive verification email).';
323322
+ const userMessage = config?.userMessage ?? (isSurveyMode ? registerMode ? `Navigate to ${baseUrl}. BEFORE registering, explore all reachable public auth pages from the logged-out experience (login, register, forgot-password - check menus and drawers too). Capture each with capture_page and record_transition. Do NOT submit the password-reset form. Then create your account:${registerHint}. After your account is verified and you are logged in, take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire application thoroughly.` : emailVerificationUnavailable ? `Navigate to ${baseUrl}.${emailWallNote} BEFORE the registration attempt, explore all reachable public auth pages from the logged-out experience and capture each with capture_page and record_transition (do NOT submit the password-reset form). After the registration attempt resolves (signed in, or blocked at the email wall), take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each reachable page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire reachable application thoroughly.` : `Navigate to ${baseUrl}.${hasLoginCredentials ? ` BEFORE logging in, explore all reachable public auth pages from the logged-out experience (login, register, forgot-password - check menus and drawers too). Capture each with capture_page and record_transition. Do NOT submit registration or password-reset forms. After mapping public auth pages, log in.${loginHint}` : ""} After${hasLoginCredentials ? " logging in" : " landing"}, take a browser_snapshot and list the navigation links visible in the snapshot before clicking anything - this is your checklist.${surveyBreadthHint} Then visit each page systematically, catalog it with capture_page, and record transitions. After capture_page records a page, reuse the captured state facts; only take a fresh browser_snapshot after navigation, loading, or a visible UI state change. Open tabs, and visit one detail page per list. Map the entire application thoroughly.` : isDeepMode ? emailVerificationUnavailable ? `Start by navigating to ${baseUrl}.${emailWallNote} Make the ONE registration attempt first. Then go to the focus areas listed in the system prompt. If you are signed in, cover EACH focus area fully: (1) happy path with valid data \u2192 record_journey, (2) validation/errors with invalid/empty data \u2192 record_journey, (3) interact with all buttons/dropdowns/tabs. If you were blocked at the email wall, cover what is reachable WITHOUT an account: validation/errors with synthetic invalid/empty data \u2192 record_journey, public interactions, and note account-gated flows as BLOCKED in finish_exploration instead of attempting them. You have ${maxIterations} iterations. Do NOT call finish_exploration until you have covered every reachable focus area.` : `Start by navigating to ${baseUrl}${registerMode ? ` and create your account:${registerHint}.` : hasLoginCredentials ? ` and log in.${loginHint}` : "."} Then go to the focus areas listed in the system prompt. For EACH focus area, you must: (1) test the happy path with valid data and call record_journey, (2) test validation/errors with invalid/empty data and call record_journey, (3) interact with all buttons/dropdowns/tabs on the page. You have ${maxIterations} iterations - use them all. Do NOT call finish_exploration until you have completed BOTH happy path AND validation testing for every focus area.` : `Start by navigating to ${baseUrl}${registerMode ? ` and create your account:${registerHint}.` : emailVerificationUnavailable ? `.${emailWallNote}` : hasLoginCredentials ? ` and log in.${loginHint}` : "."} Then explore the application feature by feature. On each page, take a browser_snapshot and call capture_page. When you find forms or interactive features, actually USE them - fill in fields, click buttons, submit forms, and observe what happens. Capture every resulting page. Remember: visiting a page is not enough - interact with it like a real user would.`);
322698
323323
  let messages = [
322699
323324
  { role: "system", content: systemPrompt },
322700
323325
  { role: "user", content: userMessage }
@@ -322703,6 +323328,7 @@ ${inboxPromptBlock}`;
322703
323328
  let lastVisitedSize = 0;
322704
323329
  let lastSurveyProgressMark = 0;
322705
323330
  let staleIterations = 0;
323331
+ let staleKnownKeysMark = 0;
322706
323332
  let lastCaptureIteration = 0;
322707
323333
  let lastJourneyProgress = 0;
322708
323334
  let lastDeepProgressMark = 0;
@@ -322715,9 +323341,151 @@ ${inboxPromptBlock}`;
322715
323341
  const BRIEF_INTERVAL = 5;
322716
323342
  const stuckDetectorEnabled = process.env.DISCOVERY_STUCK_DETECTOR === "1" || (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_STUCK_DETECTOR);
322717
323343
  const stuckDetector = stuckDetectorEnabled ? new stuck_detector_js_1.StuckDetector({ repetitionReachesRecovery: !isDeepMode }) : null;
322718
- while (iteration < maxIterations && !explorationComplete) {
323344
+ let adaptiveDecisionArmedAt = Math.ceil(maxIterations * ADAPTIVE_BUDGET_DECISION_PCT);
323345
+ let adaptiveRescueUsed = false;
323346
+ let adaptiveComplete = false;
323347
+ let adaptivePendingDiscoveryRoutes = [];
323348
+ let adaptiveLastNewRouteIteration = Number.NEGATIVE_INFINITY;
323349
+ let adaptivePrevVisitedSize = 0;
323350
+ let captureRevision = 0;
323351
+ let captureCreditedThrough = 0;
323352
+ const adaptiveKnownKeys = /* @__PURE__ */ new Set();
323353
+ let discoveryRevision = 0;
323354
+ let discoveryCreditedThrough = 0;
323355
+ let adaptiveLastDiscoveryIteration = Number.NEGATIVE_INFINITY;
323356
+ let adaptiveDiscoveryLeaseUsed = false;
323357
+ const adaptiveDebtRoutes = () => {
323358
+ const isAttemptedLinkEntry = (r) => {
323359
+ const item = state2.frontier.get(r);
323360
+ return item?.source === "link-seen" && item.attempted === true;
323361
+ };
323362
+ const ranked = (0, exploration_state_js_1.getRankedFrontier)(state2, { preferShallow: true }).filter((r) => r !== "/" && !isAttemptedLinkEntry(r));
323363
+ const rankedSet = new Set(ranked);
323364
+ const navigatedUncaptured = [];
323365
+ for (const r of state2.frontier.keys()) {
323366
+ if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r))
323367
+ navigatedUncaptured.push(r);
323368
+ }
323369
+ return ranked.concat(navigatedUncaptured);
323370
+ };
323371
+ const describeRankedRoutes = (routes, max) => routes.slice(0, max).map((r) => {
323372
+ const label = state2.frontier.get(r)?.label;
323373
+ return label ? `${label} (${r})` : r;
323374
+ }).join(", ");
323375
+ const refreshAdaptiveDiscovery = () => {
323376
+ if (state2.visitedRoutes.size > adaptivePrevVisitedSize) {
323377
+ adaptivePrevVisitedSize = state2.visitedRoutes.size;
323378
+ adaptiveLastNewRouteIteration = iteration;
323379
+ captureRevision++;
323380
+ }
323381
+ const debt = adaptiveDebtRoutes();
323382
+ let grew = false;
323383
+ for (const route2 of debt) {
323384
+ const key = state2.frontier.get(route2)?.familyKey ?? (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2);
323385
+ if (!adaptiveKnownKeys.has(key)) {
323386
+ adaptiveKnownKeys.add(key);
323387
+ adaptivePendingDiscoveryRoutes.push(route2);
323388
+ grew = true;
323389
+ }
323390
+ }
323391
+ if (grew) {
323392
+ discoveryRevision++;
323393
+ adaptiveLastDiscoveryIteration = iteration;
323394
+ }
323395
+ if (!adaptiveComplete && adaptiveDecisionArmedAt === Number.POSITIVE_INFINITY && debt.length > 0) {
323396
+ adaptiveDecisionArmedAt = iteration;
323397
+ }
323398
+ };
323399
+ const runBudgetController = (opts = {}) => {
323400
+ const allowExtension = opts.allowExtension ?? true;
323401
+ const remainingList = adaptiveDebtRoutes();
323402
+ const capturing = captureRevision > captureCreditedThrough && iteration - adaptiveLastNewRouteIteration <= ADAPTIVE_BUDGET_PROGRESS_WINDOW;
323403
+ const freshDiscovery = discoveryRevision > discoveryCreditedThrough && iteration - adaptiveLastDiscoveryIteration <= ADAPTIVE_BUDGET_PROGRESS_WINDOW;
323404
+ const decision = computeBudgetDecision({
323405
+ iteration,
323406
+ currentCap: maxIterations,
323407
+ hardCap: adaptiveHardCap,
323408
+ remainingRoutes: remainingList.length,
323409
+ capturing,
323410
+ freshDiscovery,
323411
+ discoveryLeaseUsed: adaptiveDiscoveryLeaseUsed,
323412
+ rescueUsed: adaptiveRescueUsed,
323413
+ allowExtension
323414
+ });
323415
+ if (decision.action === "extend") {
323416
+ maxIterations = decision.newCap;
323417
+ adaptiveRescueUsed = false;
323418
+ discoveryCreditedThrough = discoveryRevision;
323419
+ adaptiveDecisionArmedAt = Math.min(maxIterations, Math.max(iteration + ADAPTIVE_BUDGET_MIN_GAP, Math.ceil(maxIterations * ADAPTIVE_BUDGET_DECISION_PCT)));
323420
+ const remainingSet = new Set(remainingList);
323421
+ const freshFirst = [.../* @__PURE__ */ new Set([
323422
+ ...adaptivePendingDiscoveryRoutes.filter((r) => remainingSet.has(r)),
323423
+ ...remainingList
323424
+ ])];
323425
+ adaptivePendingDiscoveryRoutes = [];
323426
+ if (decision.lease === "discovery") {
323427
+ adaptiveDiscoveryLeaseUsed = true;
323428
+ logs2.push(` [adaptive-budget] discovery lease +${decision.delta} \u2192 ${maxIterations}; ${remainingList.length} newly-found route(s)`);
323429
+ return {
323430
+ directive: `GRANTED: ${decision.delta} more iterations to capture routes you discovered late in the crawl. Reach these through the app's own links/menus, then capture_page + record_transition each: ${describeRankedRoutes(freshFirst, 6)}. If one can't be reached after a deliberate attempt, note it in your summary and move on.`,
323431
+ keepGoing: true
323432
+ };
323433
+ }
323434
+ captureCreditedThrough = captureRevision;
323435
+ logs2.push(` [adaptive-budget] capture lease +${decision.delta} \u2192 ${maxIterations}; ${remainingList.length} route(s) still un-captured`);
323436
+ return {
323437
+ directive: `GRANTED: ${decision.delta} additional iterations (your budget is now ${maxIterations}). Coverage is incomplete. Reach these still-unmapped routes through the app's own links and menus, then capture_page + record_transition each: ${describeRankedRoutes(freshFirst, 6)}. Already-captured pages may be traversed, but do not re-catalog them. If a target redirects, errors, or cannot be located after a deliberate attempt, note it in your summary and move to the next.`,
323438
+ keepGoing: true
323439
+ };
323440
+ }
323441
+ if (decision.action === "rescue") {
323442
+ adaptiveRescueUsed = true;
323443
+ const runwayTarget = Math.min(adaptiveHardCap, iteration + ADAPTIVE_BUDGET_RESCUE_WINDOW);
323444
+ if (runwayTarget > maxIterations)
323445
+ maxIterations = runwayTarget;
323446
+ adaptiveDecisionArmedAt = Math.min(maxIterations, iteration + ADAPTIVE_BUDGET_RESCUE_WINDOW);
323447
+ logs2.push(` [adaptive-budget] stall rescue \u2192 steering to ${remainingList.length} un-captured route(s) (cap ${maxIterations})`);
323448
+ return {
323449
+ directive: `You are revisiting pages without capturing anything new. ${remainingList.length} discovered route(s) are still unmapped: ${describeRankedRoutes(remainingList, 4)}. Navigate to each through the app's links/menus and capture_page it now. If one is genuinely unreachable, note it and move on \u2014 do not keep re-loading pages you have already seen.`,
323450
+ keepGoing: true
323451
+ };
323452
+ }
323453
+ if (decision.action === "finish") {
323454
+ if (decision.reason === "drained") {
323455
+ adaptiveDecisionArmedAt = Number.POSITIVE_INFINITY;
323456
+ logs2.push(" [adaptive-budget] frontier drained \u2014 quiescent (will re-arm on new discovery)");
323457
+ } else {
323458
+ adaptiveComplete = true;
323459
+ adaptiveDecisionArmedAt = Number.POSITIVE_INFINITY;
323460
+ logs2.push(` [adaptive-budget] finishing (${decision.reason}); ${remainingList.length} route(s) left un-captured`);
323461
+ }
323462
+ return { directive: null, keepGoing: false };
323463
+ }
323464
+ adaptiveDecisionArmedAt = Math.min(iteration + 5, maxIterations);
323465
+ return { directive: null, keepGoing: true };
323466
+ };
323467
+ const tryAdaptiveBoundaryExtend = () => {
323468
+ if (!adaptiveActive || adaptiveComplete)
323469
+ return false;
323470
+ refreshAdaptiveDiscovery();
323471
+ const { directive, keepGoing } = runBudgetController();
323472
+ if (keepGoing && maxIterations > iteration) {
323473
+ if (directive) {
323474
+ if (useV3) {
323475
+ transientDirectives.push(directive);
323476
+ } else {
323477
+ messages.push({ role: "user", content: directive });
323478
+ }
323479
+ }
323480
+ return true;
323481
+ }
323482
+ return false;
323483
+ };
323484
+ while (!explorationComplete && (iteration < maxIterations || tryAdaptiveBoundaryExtend())) {
322719
323485
  iteration++;
322720
323486
  (0, exploration_state_js_1.reduceEvent)(state2, { type: "ITERATION_STARTED", iteration });
323487
+ if (adaptiveActive)
323488
+ refreshAdaptiveDiscovery();
322721
323489
  const iterStartedAt = Date.now();
322722
323490
  logs2.push(`
322723
323491
  -- Explore iteration ${iteration}/${maxIterations} --`);
@@ -322761,7 +323529,7 @@ ${inboxPromptBlock}`;
322761
323529
  logs2.push(` Deep mode escalation: forced action after ${deepStaleChecks * 5} stale iterations`);
322762
323530
  } else if (deepStaleChecks >= 3 && !deepStaleNudgeSent) {
322763
323531
  deepStaleNudgeSent = true;
322764
- const nudgeMsg = `You haven't recorded any new journeys or captured new interactions in a while. If you've finished testing all focus areas (both happy path AND validation), call finish_exploration. Otherwise, continue - try submitting forms with invalid/empty data to test validation, or interact with elements you haven't tried yet.`;
323532
+ const nudgeMsg = emailVerificationUnavailable ? `You haven't recorded any new journeys or captured new interactions in a while. If you've finished testing everything reachable in your focus areas, call finish_exploration. Otherwise, continue - try submitting forms with invalid/empty synthetic data to test validation, or interact with elements you haven't tried yet. Do NOT re-attempt registration or login flows that were blocked at the email-verification wall - note them as BLOCKED instead.` : `You haven't recorded any new journeys or captured new interactions in a while. If you've finished testing all focus areas (both happy path AND validation), call finish_exploration. Otherwise, continue - try submitting forms with invalid/empty data to test validation, or interact with elements you haven't tried yet.`;
322765
323533
  if (useV3) {
322766
323534
  transientDirectives.push(nudgeMsg);
322767
323535
  } else {
@@ -322795,14 +323563,39 @@ ${inboxPromptBlock}`;
322795
323563
  }
322796
323564
  logs2.push(` Snapshot-loop nudge: ${itersSinceCapture} iterations without capture_page`);
322797
323565
  }
322798
- if (currentSurveyProgressMark === lastSurveyProgressMark) {
323566
+ if (adaptiveActive && !adaptiveComplete && adaptiveKnownKeys.size > staleKnownKeysMark) {
323567
+ staleKnownKeysMark = adaptiveKnownKeys.size;
323568
+ if (currentSurveyProgressMark === lastSurveyProgressMark && staleIterations > 0) {
323569
+ logs2.push(` Stall check: frontier grew to ${adaptiveKnownKeys.size} identities \u2014 counting as progress`);
323570
+ }
323571
+ staleIterations = 0;
323572
+ } else if (currentSurveyProgressMark === lastSurveyProgressMark) {
322799
323573
  staleIterations++;
322800
323574
  if (staleIterations >= 4) {
322801
- logs2.push(` Survey hard stop: no survey progress in ${staleIterations * 5} iterations, forcing completion`);
322802
- explorationComplete = true;
322803
- summary = `Exploration force-stopped after ${iteration} iterations - no survey progress in ${staleIterations * 5} iterations. Visited ${currentVisitedSize} pages.`;
322804
- break;
322805
- } else if (staleIterations >= 2) {
323575
+ if (adaptiveActive && !adaptiveComplete) {
323576
+ const { directive, keepGoing } = runBudgetController({ allowExtension: false });
323577
+ if (keepGoing) {
323578
+ staleIterations = 2;
323579
+ if (directive) {
323580
+ if (useV3) {
323581
+ transientDirectives.push(directive);
323582
+ } else {
323583
+ messages.push({ role: "user", content: directive });
323584
+ }
323585
+ }
323586
+ } else {
323587
+ logs2.push(` Survey hard stop (adaptive terminal): no survey progress in ${staleIterations * 5} iterations`);
323588
+ explorationComplete = true;
323589
+ summary = `Exploration force-stopped after ${iteration} iterations - no survey progress in ${staleIterations * 5} iterations. Visited ${currentVisitedSize} pages.`;
323590
+ break;
323591
+ }
323592
+ } else {
323593
+ logs2.push(` Survey hard stop: no survey progress in ${staleIterations * 5} iterations, forcing completion`);
323594
+ explorationComplete = true;
323595
+ summary = `Exploration force-stopped after ${iteration} iterations - no survey progress in ${staleIterations * 5} iterations. Visited ${currentVisitedSize} pages.`;
323596
+ break;
323597
+ }
323598
+ } else if (staleIterations >= 2 && !adaptiveActive) {
322806
323599
  const loopMsg = "You appear to be revisiting the same pages without discovering new routes, transitions, or useful auth/navigation progress. Call finish_exploration NOW with a summary of what you found.";
322807
323600
  if (useV3) {
322808
323601
  transientDirectives.push(loopMsg);
@@ -322849,7 +323642,23 @@ ${inboxPromptBlock}`;
322849
323642
  lastJourneyProgress = currentJourneyProgress;
322850
323643
  }
322851
323644
  }
322852
- if (iteration >= Math.floor(maxIterations * 0.9)) {
323645
+ if (adaptiveActive && !adaptiveComplete && iteration >= adaptiveDecisionArmedAt) {
323646
+ const { directive, keepGoing } = runBudgetController();
323647
+ if (directive) {
323648
+ if (useV3) {
323649
+ transientDirectives.push(directive);
323650
+ } else {
323651
+ messages.push({ role: "user", content: directive });
323652
+ }
323653
+ }
323654
+ if (!keepGoing && adaptiveComplete) {
323655
+ explorationComplete = true;
323656
+ summary = summary || `Exploration finished after ${iteration} iterations (adaptive budget: coverage incomplete).`;
323657
+ logs2.push(` [adaptive-budget] terminal at iteration ${iteration}`);
323658
+ break;
323659
+ }
323660
+ }
323661
+ if (!adaptiveActive && iteration >= Math.floor(maxIterations * 0.9)) {
322853
323662
  const budgetTestCount = useV2 ? state2.recordedJourneys.length : localJourneys.length;
322854
323663
  const budgetBlocking = linkFrontierEnabled ? (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2) : [];
322855
323664
  const budgetMsg = isDeepMode ? budgetTestCount === 0 ? "CRITICAL: You have used 90% of your iteration budget and have NOT recorded any journeys yet. You MUST call record_journey NOW for every flow you have tested (happy paths, validation, edge cases). After calling record_journey for all completed flows, call finish_exploration. Do NOT call finish_exploration without first calling record_journey." : `CRITICAL: You have used 90% of your iteration budget. You have recorded ${budgetTestCount} journey(s) so far. If you have completed any additional flows that you haven't called record_journey for, do so NOW. Then call finish_exploration with a summary.` : budgetBlocking.length > 0 ? `CRITICAL: You have used 90% of your iteration budget. These discovered routes were never visited: ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(budgetBlocking, 4)}. Visit ONLY these (capture_page each), then call finish_exploration. Do not explore anything else.` : "CRITICAL: You have used 90% of your iteration budget. Wrap up your current interaction and call finish_exploration with a summary of what you found. If you are mid-flow, complete it quickly then finish.";
@@ -322941,7 +323750,7 @@ ${recent}`);
322941
323750
  if (remainingFocusAreas.length > 0)
322942
323751
  progressLines.push(`### Focus Areas NOT YET Visited: ${remainingFocusAreas.join(", ")} - explore these next!`);
322943
323752
  progressLines.push("");
322944
- progressLines.push("IMPORTANT: For each focus area, you must test BOTH the happy path (valid data) AND validation/error handling (invalid/empty data). If you have only done the happy path, go back and test validation now.");
323753
+ progressLines.push(emailVerificationUnavailable ? "IMPORTANT: For each focus area, test everything reachable in your current auth state: validation/error handling with invalid/empty data, plus the happy path if you are signed in. If a flow was blocked at the email-verification wall, note it as BLOCKED \u2014 do not re-attempt it and never type a guessed code." : "IMPORTANT: For each focus area, you must test BOTH the happy path (valid data) AND validation/error handling (invalid/empty data). If you have only done the happy path, go back and test validation now.");
322945
323754
  }
322946
323755
  progressLines.push("");
322947
323756
  progressLines.push("## NEXT ACTIONS");
@@ -322952,7 +323761,7 @@ ${recent}`);
322952
323761
  });
322953
323762
  if (untestedAreas.length > 0) {
322954
323763
  progressLines.push(`These focus areas still need testing: ${untestedAreas.join(", ")}`);
322955
- progressLines.push("For each: (1) happy path with valid data \u2192 record_journey, (2) validation with invalid/empty data \u2192 record_journey");
323764
+ progressLines.push(emailVerificationUnavailable ? "For each: cover what your current auth state reaches \u2014 validation with invalid/empty data \u2192 record_journey, plus happy path if signed in; note email-wall-blocked flows as BLOCKED instead of re-attempting them" : "For each: (1) happy path with valid data \u2192 record_journey, (2) validation with invalid/empty data \u2192 record_journey");
322956
323765
  } else {
322957
323766
  progressLines.push("All focus areas have tests. Look for additional edge cases or call finish_exploration.");
322958
323767
  }
@@ -323005,7 +323814,8 @@ ${currentSummary}`;
323005
323814
  const brief = (0, exploration_prompt_builder_js_2.buildControlBrief)(state2, {
323006
323815
  mode: options?.mode ?? "full",
323007
323816
  maxIterations,
323008
- focusAreas: options?.focusAreas
323817
+ focusAreas: options?.focusAreas,
323818
+ adaptiveBudget: adaptiveActive
323009
323819
  });
323010
323820
  messages.push({ role: "user", content: brief });
323011
323821
  lastBriefRoute = currentStateRoute;
@@ -323024,7 +323834,8 @@ ${currentSummary}`;
323024
323834
  recentExchanges,
323025
323835
  incremental: options?.incremental,
323026
323836
  userNotes: options?.userNotes,
323027
- linkFrontierEnabled
323837
+ linkFrontierEnabled,
323838
+ adaptiveBudget: adaptiveActive
323028
323839
  });
323029
323840
  transientDirectives.length = 0;
323030
323841
  }
@@ -323233,6 +324044,9 @@ ${currentSummary}`;
323233
324044
  sessionState: normalized.sessionState
323234
324045
  });
323235
324046
  ingestLinksFromText(snapshotText.snapshotText);
324047
+ if (domHarvestEnabled) {
324048
+ await harvestDomLinksIntoFrontier();
324049
+ }
323236
324050
  const isSurvey = options?.mode === "survey";
323237
324051
  const subNavItems = [];
323238
324052
  for (const el of normalized.elements) {
@@ -323741,6 +324555,30 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
323741
324555
  logs2.push(` finish accepted with ${blocking.length} uncovered discovered route(s): ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(blocking, 10)}`);
323742
324556
  }
323743
324557
  }
324558
+ if (adaptiveActive && !adaptiveComplete) {
324559
+ refreshAdaptiveDiscovery();
324560
+ const remainingList = adaptiveDebtRoutes();
324561
+ if (remainingList.length > 0 && iteration < adaptiveHardCap) {
324562
+ let reason = null;
324563
+ if (iteration >= adaptiveDecisionArmedAt) {
324564
+ const { directive, keepGoing } = runBudgetController();
324565
+ if (keepGoing) {
324566
+ reason = directive ?? `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
324567
+ }
324568
+ } else {
324569
+ reason = `Not done yet \u2014 ${remainingList.length} discovered route(s) were never captured: ${describeRankedRoutes(remainingList, 6)}. Visit and capture_page each (via the app's links), then call finish_exploration.`;
324570
+ }
324571
+ if (reason) {
324572
+ toolResults.push({
324573
+ role: "tool",
324574
+ tool_call_id: toolCall.id,
324575
+ content: JSON.stringify({ acknowledged: false, keepExploring: true, reason })
324576
+ });
324577
+ logs2.push(` [adaptive-budget] finish bounced: ${remainingList.length} route(s) still un-captured`);
324578
+ continue;
324579
+ }
324580
+ }
324581
+ }
323744
324582
  explorationComplete = true;
323745
324583
  summary = String(toolArgs.summary || "");
323746
324584
  (0, exploration_state_js_1.reduceEvent)(state2, { type: "EXPLORATION_FINISHED", summary });
@@ -323798,6 +324636,18 @@ Exploration complete: ${summary}`);
323798
324636
  continue;
323799
324637
  }
323800
324638
  if (toolName === "fill_credential" || toolName === "fill_credential_form") {
324639
+ if (emailVerificationUnavailable) {
324640
+ const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
324641
+ if (codeGuard.blocked) {
324642
+ logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
324643
+ toolResults.push({
324644
+ role: "tool",
324645
+ tool_call_id: toolCall.id,
324646
+ content: codeGuard.message ?? "BLOCKED: cannot receive verification email"
324647
+ });
324648
+ continue;
324649
+ }
324650
+ }
323801
324651
  const fullVarMap = variableContext.allTestVariables ?? variableContext.publicTestVariables;
323802
324652
  const { secrets: dispatchSecrets } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMap);
323803
324653
  try {
@@ -323880,6 +324730,18 @@ Exploration complete: ${summary}`);
323880
324730
  continue;
323881
324731
  }
323882
324732
  }
324733
+ if (emailVerificationUnavailable) {
324734
+ const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
324735
+ if (codeGuard.blocked) {
324736
+ logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
324737
+ toolResults.push({
324738
+ role: "tool",
324739
+ tool_call_id: toolCall.id,
324740
+ content: codeGuard.message ?? "BLOCKED: cannot receive verification email"
324741
+ });
324742
+ continue;
324743
+ }
324744
+ }
323883
324745
  if (inboxState?.provisioned && toolCallLooksLikeSignupSubmit(toolName, toolArgs)) {
323884
324746
  accountAlreadyExistsRecoveryExpiresAt = iteration + 2;
323885
324747
  }
@@ -324123,6 +324985,11 @@ Exploration complete: ${summary}`);
324123
324985
  logs2.push(` \u26A0 Out of scope: ${detectedRoute} (focus: ${navFocusAreas.join(", ")})`);
324124
324986
  toolResults.__outOfScopeRedirect = `You navigated to ${detectedRoute} which is OUTSIDE your focus areas (${navFocusAreas.join(", ")}). This is a different feature. You may observe the URL for assertion purposes (e.g. "verify redirect to ${detectedRoute}"), but do NOT interact with this page - do not fill forms, click buttons, or call capture_page here. Navigate back to your focus areas or call finish_exploration if you are done.`;
324125
324987
  }
324988
+ } else if (surveyNavScope) {
324989
+ if (!(0, survey_scope_js_1.routeWithinSurveyBasePath)(detectedRoute, surveyNavScope) && !(0, survey_scope_js_1.hasAuthFlowSegment)(detectedRoute)) {
324990
+ logs2.push(` \u26A0 Out of survey scope: ${detectedRoute} (entry scope: ${surveyNavScope.basePath})`);
324991
+ toolResults.__outOfScopeRedirect = `You navigated to ${detectedRoute}, which is OUTSIDE this survey's entry scope (\`${surveyNavScope.basePath}\`). This is a SCOPED survey \u2014 that page is not part of the audited flow: do NOT catalog it (no capture_page), do not follow its links, and do not create a featureGroup for it. Navigate back to a page under \`${surveyNavScope.basePath}\` and continue mapping the in-scope flow. If you have already covered the in-scope pages, call finish_exploration.`;
324992
+ }
324126
324993
  }
324127
324994
  }
324128
324995
  }
@@ -324418,6 +325285,7 @@ Fatal error: ${msg}`);
324418
325285
  const status = fatalErr?.status;
324419
325286
  const lower = msg.toLowerCase();
324420
325287
  const truncatedReason = status === 429 || lower.includes("rate limit") || lower.includes("too many requests") || lower.includes("quota") ? "rate_limit" : status === 408 || lower.includes("timeout") || lower.includes("timed out") || lower.includes("etimedout") ? "timeout" : "ai_error";
325288
+ const truncatedTransient = (0, ai_retry_js_1.isTransientRateError)(fatalErr);
324421
325289
  if (useV2) {
324422
325290
  try {
324423
325291
  (0, exploration_state_js_1.flushUnflushedCommands)(state2, variableContext.allTestVariables ?? variableContext.publicTestVariables);
@@ -324462,7 +325330,8 @@ Fatal error: ${msg}`);
324462
325330
  collectedTransitions: state2.collectedTransitions,
324463
325331
  iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
324464
325332
  truncated: true,
324465
- truncatedReason
325333
+ truncatedReason,
325334
+ truncatedTransient
324466
325335
  };
324467
325336
  }
324468
325337
  return {
@@ -324482,7 +325351,8 @@ Fatal error: ${msg}`);
324482
325351
  collectedTransitions,
324483
325352
  iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
324484
325353
  truncated: true,
324485
- truncatedReason
325354
+ truncatedReason,
325355
+ truncatedTransient
324486
325356
  };
324487
325357
  } finally {
324488
325358
  if (inboxReader) {
@@ -324511,9 +325381,12 @@ var require_discover_feature_explorer = __commonJS({
324511
325381
  "../runner-core/dist/services/discover-feature-explorer.js"(exports) {
324512
325382
  "use strict";
324513
325383
  Object.defineProperty(exports, "__esModule", { value: true });
325384
+ exports.buildFeatureExplorePrompt = buildFeatureExplorePrompt;
325385
+ exports.buildIncrementalExplorePrompt = buildIncrementalExplorePrompt;
324514
325386
  exports.runFeatureExplorePhase = runFeatureExplorePhase;
324515
325387
  var discover_explorer_js_1 = require_discover_explorer();
324516
325388
  var exploration_parser_js_1 = require_exploration_parser();
325389
+ var credential_tools_js_1 = require_credential_tools();
324517
325390
  var verification_inbox_js_1 = require_verification_inbox();
324518
325391
  var inbox_tools_js_1 = require_inbox_tools();
324519
325392
  function buildFeatureExplorerConfig(focusAreas, incremental, otherRoutes) {
@@ -324578,7 +325451,8 @@ var require_discover_feature_explorer = __commonJS({
324578
325451
  const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
324579
325452
  const hasCredentials = Object.keys(browserCredentials).length > 0;
324580
325453
  const registerMode = !!((0, verification_inbox_js_1.loadVerificationInboxConfig)() && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext));
324581
- const credentialLines = Object.entries(browserCredentials).map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
325454
+ const emailVerificationUnavailable = !registerMode && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext) && !(0, credential_tools_js_1.hasUsableLoginIdentity)(browserCredentials, { isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail });
325455
+ const credentialLines = Object.entries(browserCredentials).filter(([k]) => k !== "DISCOVERY_SHOULD_REGISTER").map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
324582
325456
  const otherFeaturesBlock = featureOpts.otherFeatures && featureOpts.otherFeatures.length > 0 ? `
324583
325457
  ## Other Features (DO NOT TEST)
324584
325458
  These are separate features. Do NOT navigate to their pages, do NOT interact with them, do NOT call record_journey for them:
@@ -324689,6 +325563,22 @@ ${transitionBlock}
324689
325563
  ${(() => {
324690
325564
  const isAuthFeature = featureOpts.focusAreas.some((fa) => /\/(auth|login|sign-?in|register|signup)\b/i.test(fa));
324691
325565
  if (isAuthFeature && hasCredentials) {
325566
+ if (emailVerificationUnavailable) {
325567
+ return `## Login/Registration IS Your Test Assignment (email verification UNAVAILABLE)
325568
+ Your assigned pages ARE the login/registration pages. This project registers its own account, but this runner CANNOT receive email \u2014 registration may still work if the app does not require email verification.
325569
+
325570
+ Follow this order:
325571
+ 1. Navigate to your first assigned auth page (e.g., the login page)
325572
+ 2. Take a \`browser_snapshot\` to see what's there
325573
+ 3. **Test VALIDATION FIRST** while you are NOT logged in - on the LOGIN form: empty fields, invalid email, wrong password (synthetic values like "wrong@example.com" / "badpassword"); on the REGISTRATION form: empty-submit validation ONLY (a filled synthetic signup could create a stray account)
325574
+ 4. Call \`record_journey\` for the validation flow
325575
+ 5. **THEN attempt the happy path ONCE via REGISTRATION**: fill the signup form with \`fill_credential_form\` using the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys and submit it ONCE. Never alter the email, never create a second account. If signup says the address is ALREADY registered (a previous run created it), log in with the same credential keys instead
325576
+ 6. **THE WALL**: if any step demands an emailed code/link (verification code, OTP, magic link, "check your email"), STOP immediately - never type a guessed code, never click Resend - and treat the happy path as BLOCKED
325577
+ 7. If you are signed in with NO email round-trip, call \`record_journey\` for the happy path
325578
+ 8. Call \`capture_page\` on each auth page AFTER interacting (this saves the successful commands for test generation)
325579
+
325580
+ When done, call \`finish_exploration\` - if you hit the wall, note that the authenticated happy path is BLOCKED (cannot receive verification email).`;
325581
+ }
324692
325582
  return `## Login/Registration IS Your Test Assignment
324693
325583
  Your assigned pages ARE the login/registration pages. Do NOT log in as a pass-through - the auth pages are what you are testing.
324694
325584
 
@@ -324743,7 +325633,7 @@ ${otherFeaturesBlock}
324743
325633
  4. Take \`browser_snapshot\` of the result page
324744
325634
  - If the page shows a loading skeleton or spinner after navigation, wait for the real content to appear before calling \`capture_page\` or \`record_journey\`
324745
325635
  5. Call \`record_journey\` with:
324746
- - All routes visited in \`routes\` array (e.g., ["/auth/login", "/dashboard"])
325636
+ - All routes visited in \`routes\` array (e.g., ${emailVerificationUnavailable ? '["/", "/contact"]' : '["/auth/login", "/dashboard"]'})
324747
325637
  - Specific steps describing what you did
324748
325638
  - expectedOutcome with the EXACT result you observed
324749
325639
  - assertions with STABLE structural elements from the result page
@@ -324755,11 +325645,15 @@ ${otherFeaturesBlock}
324755
325645
  4. Fix the errors and resubmit successfully \u2192 observe recovery
324756
325646
  5. Call \`record_journey\` ONCE for this entire combined flow
324757
325647
 
324758
- **Auth pages (login/register) - test NEGATIVE before positive:**
325648
+ ${emailVerificationUnavailable ? `**Auth pages (login/register) - this runner CANNOT receive email:**
325649
+ - Test negative flows FIRST: on the LOGIN form empty fields \u2192 invalid synthetic values; on the REGISTRATION form empty-submit only (never submit a filled synthetic signup) \u2192 call record_journey for the validation flow
325650
+ - THEN attempt the happy path ONCE via registration with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys (\`fill_credential_form\`); if the address is already registered, log in with the same keys
325651
+ - If any step demands an emailed code/link, STOP - never type a guessed code - and note in finish_exploration that the authenticated happy path is BLOCKED (cannot receive verification email)
325652
+ - If you are signed in without an email round-trip, call record_journey for the happy path` : `**Auth pages (login/register) - test NEGATIVE before positive:**
324759
325653
  - You arrive at the page NOT logged in - test errors FIRST while in this state
324760
325654
  - Empty fields \u2192 invalid data \u2192 call record_journey
324761
325655
  - THEN do the happy path - valid credentials \u2192 call record_journey
324762
- - This order avoids needing to log out between journeys
325656
+ - This order avoids needing to log out between journeys`}
324763
325657
 
324764
325658
  ### For CRUD Features - Combine into a Single Journey:
324765
325659
  If the feature has Create/View/Edit/Delete operations across pages:
@@ -324784,7 +325678,7 @@ When you have thoroughly explored all the feature pages and tested the key flows
324784
325678
  - Pages you visited and what you found there
324785
325679
 
324786
325680
  ## Test Data Rules
324787
- ${registerMode ? `- **Account credentials**: do NOT invent an email or password \u2014 register and log in with the email returned by \`provision_identity\` and the \`TEST_USER_PASSWORD\` credential (filled via \`fill_credential\`). See the Account Registration section.` : `- **LOGIN** credential fields: use \`process.env.TEST_USER_EMAIL!\` and \`process.env.TEST_USER_PASSWORD!\`
325681
+ ${registerMode ? `- **Account credentials**: do NOT invent an email or password \u2014 register and log in with the email returned by \`provision_identity\` and the \`TEST_USER_PASSWORD\` credential (filled via \`fill_credential\`). See the Account Registration section.` : emailVerificationUnavailable ? `- **Account credentials**: this project registers its own account but this runner CANNOT receive email. Register/log in ONLY with the \`TEST_USER_EMAIL\` + \`TEST_USER_PASSWORD\` credential keys (\`fill_credential\` / \`fill_credential_form\`) \u2014 never invent a different email, never create a second account. If a step demands an emailed verification code or link, STOP that flow (never guess a code) and mark it BLOCKED.` : `- **LOGIN** credential fields: use \`process.env.TEST_USER_EMAIL!\` and \`process.env.TEST_USER_PASSWORD!\`
324788
325682
  - **REGISTRATION/SIGNUP** email fields: MUST be unique every run \u2192 \`testuser-\${Date.now()}@example.com\`
324789
325683
  - Passwords for registration: \`TestPass123!\` (meets typical strength rules)`}
324790
325684
  - **Other form fields**: use realistic static values (Jane Smith, 555-0100, https://example.com)
@@ -324806,7 +325700,17 @@ When calling \`record_journey\`:
324806
325700
 
324807
325701
  **Example record_journey with good assertions:**
324808
325702
  \`\`\`
324809
- record_journey({
325703
+ ${emailVerificationUnavailable ? `record_journey({
325704
+ name: "Login Form - Validation",
325705
+ route: "/auth/login",
325706
+ routes: ["/", "/auth/login"],
325707
+ steps: ["Navigate to /", "Click 'Sign In'", "Submit the empty form", "Observe validation errors"],
325708
+ expectedOutcome: "URL matches /auth/login. Validation error 'Email is required' is visible.",
325709
+ assertions: [
325710
+ { type: "url", value: "/auth/login" },
325711
+ { type: "error-text", value: "Email is required" }
325712
+ ]
325713
+ })` : `record_journey({
324810
325714
  name: "Login - Happy Path",
324811
325715
  route: "/auth/login",
324812
325716
  routes: ["/", "/auth/login", "/dashboard"],
@@ -324817,7 +325721,7 @@ record_journey({
324817
325721
  { type: "nav-link", locator: "getByRole('link', { name: 'Dashboard' })" },
324818
325722
  { type: "heading", locator: "getByRole('heading', { name: 'Dashboard' })" }
324819
325723
  ]
324820
- })
325724
+ })`}
324821
325725
  \`\`\`
324822
325726
  BAD assertions (do NOT use): \`{ type: "heading", locator: "getByRole('heading', { name: 'Welcome back, John!' })" }\` - user-specific text
324823
325727
 
@@ -324853,13 +325757,16 @@ Do NOT re-propose a test with the same name or identical steps as any item above
324853
325757
  ${hasCredentials ? `
324854
325758
  ## Credentials (browser interaction ONLY - NEVER include in record_journey steps)
324855
325759
  ${credentialLines}
325760
+ ` : ""}${emailVerificationUnavailable ? `
325761
+ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}
324856
325762
  ` : ""}`;
324857
325763
  }
324858
325764
  function buildIncrementalExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts) {
324859
325765
  const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
324860
325766
  const hasCredentials = Object.keys(browserCredentials).length > 0;
324861
325767
  const registerMode = !!((0, verification_inbox_js_1.loadVerificationInboxConfig)() && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext));
324862
- const credentialLines = Object.entries(browserCredentials).map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
325768
+ const emailVerificationUnavailable = !registerMode && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext) && !(0, credential_tools_js_1.hasUsableLoginIdentity)(browserCredentials, { isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail });
325769
+ const credentialLines = Object.entries(browserCredentials).filter(([k]) => k !== "DISCOVERY_SHOULD_REGISTER").map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
324863
325770
  const existingTestsBlock = featureOpts.existingTestDetails && featureOpts.existingTestDetails.length > 0 ? featureOpts.existingTestDetails.map((t, i) => {
324864
325771
  const stepsStr = t.steps.length > 0 ? `
324865
325772
  Steps: ${t.steps.join(" \u2192 ")}` : "";
@@ -324931,7 +325838,7 @@ ${featureOpts.appBrief}
324931
325838
  ${featureOpts.focusAreas.map((fa) => `- ${fa}`).join("\n")}
324932
325839
  ${pageElementsBlock}${otherFeaturesBlock}
324933
325840
  ## Process
324934
- 1. ${registerMode ? "Create your account by following the Account Registration section below, then navigate to the feature pages" : isAuthFeature && hasCredentials ? "Navigate to the auth pages - test validation/error scenarios NOT already covered" : hasCredentials ? "Log in using credentials below, then navigate to the feature pages" : "Navigate to the feature pages"}
325841
+ 1. ${registerMode ? "Create your account by following the Account Registration section below, then navigate to the feature pages" : emailVerificationUnavailable ? "This runner CANNOT receive email. Attempt registration ONCE with the `TEST_USER_EMAIL` + `TEST_USER_PASSWORD` credential keys via `fill_credential_form` (if the address is already registered, log in with the same keys), then navigate to the feature pages. If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code \u2014 cover only unauthenticated gaps and note blocked authenticated scenarios in finish_exploration" : isAuthFeature && hasCredentials ? "Navigate to the auth pages - test validation/error scenarios NOT already covered" : hasCredentials ? "Log in using credentials below, then navigate to the feature pages" : "Navigate to the feature pages"}
324935
325842
  2. Take \`browser_snapshot\` on each page
324936
325843
  3. Compare what you see with the existing tests - what flows are NOT tested?
324937
325844
  4. For each gap you find, interact with the page to verify the flow works, then call \`record_journey\`
@@ -324955,11 +325862,14 @@ ${featureOpts.priorInsight}
324955
325862
  ${hasCredentials ? `
324956
325863
  ## Credentials (browser interaction ONLY)
324957
325864
  ${credentialLines}
325865
+ ` : ""}${emailVerificationUnavailable ? `
325866
+ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}
324958
325867
  ` : ""}`;
324959
325868
  }
324960
325869
  async function runFeatureExplorePhase(mcpClient, baseUrl, variableContext, existingSiteMap, projectId, existingTestNames, featureOpts) {
324961
325870
  const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
324962
- const registerMode = !!(inboxConfig && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext));
325871
+ const registerIntent = (0, inbox_tools_js_1.hasRegisterIntent)(variableContext);
325872
+ const registerMode = !!(inboxConfig && registerIntent);
324963
325873
  let inboxPasswordKey = "TEST_USER_PASSWORD";
324964
325874
  if (registerMode && inboxConfig) {
324965
325875
  const inboxIdentity = (0, inbox_tools_js_1.createInboxRunState)(inboxConfig, { tagSeed: baseUrl, projectScope: projectId });
@@ -324970,6 +325880,9 @@ ${credentialLines}
324970
325880
  passwordKey: inboxPasswordKey
324971
325881
  });
324972
325882
  }
325883
+ const emailVerificationUnavailable = registerIntent && !inboxConfig && !(0, credential_tools_js_1.hasUsableLoginIdentity)(variableContext.allTestVariables ?? variableContext.publicTestVariables, {
325884
+ isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail
325885
+ });
324973
325886
  const isIncremental = featureOpts.incremental && existingTestNames.length > 0;
324974
325887
  const systemPrompt = isIncremental ? buildIncrementalExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts) : buildFeatureExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts);
324975
325888
  const hasCredentials = Object.keys(variableContext.allTestVariables ?? variableContext.publicTestVariables).length > 0;
@@ -324980,6 +325893,8 @@ ${credentialLines}
324980
325893
  const focusHint = featureOpts.userNotes ? ` The user asked you to focus on: "${featureOpts.userNotes}".` : "";
324981
325894
  if (registerMode) {
324982
325895
  userMessage = `Navigate to ${baseUrl} and create your account:${registerHint}. Then review the ${existingTestNames.length} existing tests and navigate to: ${featureOpts.focusAreas.join(", ")}. Look for gaps - scenarios, pages, or error cases not yet covered.${focusHint} If all meaningful scenarios are covered, call finish_exploration with "No additional tests needed."`;
325896
+ } else if (emailVerificationUnavailable) {
325897
+ userMessage = `Navigate to ${baseUrl}. NOTE: this project registers its own account but this runner CANNOT receive email. Attempt registration ONCE with the TEST_USER_EMAIL + TEST_USER_PASSWORD credential keys via fill_credential_form (if the address is already registered, log in with the same keys; never alter the email or invent another). If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code. Review the ${existingTestNames.length} existing tests, then look for gaps reachable in your auth state on: ${featureOpts.focusAreas.join(", ")}.${focusHint} Note any email-wall-blocked scenarios as BLOCKED in finish_exploration.`;
324983
325898
  } else if (isAuthFeature) {
324984
325899
  userMessage = `Navigate to ${baseUrl}. Review the ${existingTestNames.length} existing tests listed above, then navigate to the auth pages: ${featureOpts.focusAreas.join(", ")}. Look for untested scenarios - error cases, edge cases, flows not yet covered.${focusHint} If all meaningful scenarios are covered, call finish_exploration with "No additional tests needed."`;
324985
325900
  } else if (hasCredentials) {
@@ -324991,6 +325906,8 @@ ${credentialLines}
324991
325906
  let navigationInstruction;
324992
325907
  if (registerMode) {
324993
325908
  navigationInstruction = `Navigate to ${baseUrl} and create your account:${registerHint}. Then go to your assigned feature pages: ${featureOpts.focusAreas.join(", ")}.`;
325909
+ } else if (emailVerificationUnavailable) {
325910
+ navigationInstruction = `Navigate to ${baseUrl}. NOTE: this project registers its own account but this runner CANNOT receive email. Attempt registration ONCE with the TEST_USER_EMAIL + TEST_USER_PASSWORD credential keys via fill_credential_form (if the address is already registered, log in with the same keys; never alter the email or create a second account). If you are signed in without an email round-trip, test your assigned feature pages fully: ${featureOpts.focusAreas.join(", ")}. If any step demands an emailed code/link, STOP that flow \u2014 never type a guessed code \u2014 test what is reachable without authentication instead and note blocked authenticated flows in finish_exploration.`;
324994
325911
  } else if (isAuthFeature) {
324995
325912
  navigationInstruction = `Navigate to ${baseUrl}. Your test assignment is the authentication pages: ${featureOpts.focusAreas.join(", ")}. Navigate to the login/register page and test it - start with validation (empty/wrong fields) while NOT logged in, then test the happy path.`;
324996
325913
  } else if (hasCredentials) {
@@ -325223,8 +326140,14 @@ var require_auth_capture_run = __commonJS({
325223
326140
  exports.filterLoginCaptureCredentials = filterLoginCaptureCredentials;
325224
326141
  exports.buildLoginCaptureCredentials = buildLoginCaptureCredentials;
325225
326142
  exports.getLoginCaptureMaxIterations = getLoginCaptureMaxIterations;
326143
+ exports.getCaptureRateLimitMaxRetries = getCaptureRateLimitMaxRetries;
326144
+ exports.getCaptureRateLimitMaxDelayMs = getCaptureRateLimitMaxDelayMs;
326145
+ exports.getAuthCaptureTimeoutMs = getAuthCaptureTimeoutMs;
326146
+ exports.getAuthCaptureRunRetryMaxAttempts = getAuthCaptureRunRetryMaxAttempts;
326147
+ exports.classifyCaptureMiss = classifyCaptureMiss;
325226
326148
  exports.harvestAuthProofLive = harvestAuthProofLive;
325227
326149
  exports.findCredentialLiteralLeaks = findCredentialLiteralLeaks;
326150
+ exports.runLoginCaptureRunWithOutcome = runLoginCaptureRunWithOutcome;
325228
326151
  exports.runLoginCaptureRun = runLoginCaptureRun;
325229
326152
  exports.getRegisterCaptureMaxIterations = getRegisterCaptureMaxIterations;
325230
326153
  exports.runRegisterCaptureRun = runRegisterCaptureRun;
@@ -325237,6 +326160,7 @@ var require_auth_capture_run = __commonJS({
325237
326160
  var playwright_native_js_1 = require_playwright_native();
325238
326161
  var scrub_credentials_js_1 = require_scrub_credentials();
325239
326162
  var credential_tools_js_1 = require_credential_tools();
326163
+ var ai_retry_js_1 = require_ai_retry();
325240
326164
  var LOGIN_CAPTURE_CONTROL_VARIABLES = /* @__PURE__ */ new Set([
325241
326165
  "BASE_URL",
325242
326166
  "DISCOVERY_SHOULD_REGISTER",
@@ -325270,6 +326194,36 @@ var require_auth_capture_run = __commonJS({
325270
326194
  }
325271
326195
  var CAPTURE_RATE_LIMIT_MAX_RETRIES = 2;
325272
326196
  var CAPTURE_RATE_LIMIT_MAX_DELAY_MS = 8e3;
326197
+ function readNonNegativeIntEnv(env, name, fallback) {
326198
+ const raw = env[name]?.trim();
326199
+ if (raw === void 0 || raw === "" || !/^\d+$/.test(raw))
326200
+ return fallback;
326201
+ const parsed = Number(raw);
326202
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
326203
+ }
326204
+ function getCaptureRateLimitMaxRetries(env = process.env) {
326205
+ return readNonNegativeIntEnv(env, "AUTH_CAPTURE_RATE_LIMIT_MAX_RETRIES", CAPTURE_RATE_LIMIT_MAX_RETRIES);
326206
+ }
326207
+ function getCaptureRateLimitMaxDelayMs(env = process.env) {
326208
+ return readPositiveIntEnv2(env, "AUTH_CAPTURE_RATE_LIMIT_MAX_DELAY_MS", CAPTURE_RATE_LIMIT_MAX_DELAY_MS);
326209
+ }
326210
+ var DEFAULT_AUTH_CAPTURE_TIMEOUT_MS = 4 * 60 * 1e3;
326211
+ function getAuthCaptureTimeoutMs(env = process.env) {
326212
+ return readPositiveIntEnv2(env, "AUTH_CAPTURE_TIMEOUT_MS", DEFAULT_AUTH_CAPTURE_TIMEOUT_MS);
326213
+ }
326214
+ var DEFAULT_AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS = 2;
326215
+ function getAuthCaptureRunRetryMaxAttempts(env = process.env) {
326216
+ return readPositiveIntEnv2(env, "AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS", DEFAULT_AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS);
326217
+ }
326218
+ function classifyCaptureMiss(explore, fallback) {
326219
+ if (!explore.truncated)
326220
+ return fallback;
326221
+ if (explore.truncatedTransient)
326222
+ return "rate_limited";
326223
+ if (explore.truncatedReason === "timeout")
326224
+ return "timeout";
326225
+ return "error";
326226
+ }
325273
326227
  function buildLoginCaptureSystemPrompt(baseUrl, availableKeys, maxIterations = getLoginCaptureMaxIterations()) {
325274
326228
  const keyLines = availableKeys.map((k) => `- \`${k}\``).join("\n");
325275
326229
  return `You are a login specialist. Your SINGLE objective: navigate to the app, log in with the provided test credentials, and confirm you are authenticated. Then stop.
@@ -325293,6 +326247,7 @@ ${keyLines || "- (none provided)"}
325293
326247
  ## Hard rules
325294
326248
  - Do NOT submit registration, password-reset, or any other forms. Login only.
325295
326249
  - Do NOT fill validation / negative values. Happy-path login only.
326250
+ - If login demands an emailed verification code, one-time password (OTP), or magic link, STOP \u2014 you have no inbox access and NEVER type a guessed code (any code you type is fabricated). Call \`capture_page\` on the verification wall noting it as the blocker, then \`finish_exploration\`.
325296
326251
  - Use RELATIVE routes in capture_page (e.g. \`/login\`, \`/dashboard\`), never absolute URLs.
325297
326252
  - If login fails after 2 attempts, call \`capture_page\` noting the failure and call \`finish_exploration\`.
325298
326253
  - You have a hard limit of ${maxIterations} iterations. Finish well before then.`;
@@ -325463,17 +326418,17 @@ ${keyLines || "- (none provided)"}
325463
326418
  }
325464
326419
  return false;
325465
326420
  }
325466
- async function runLoginCaptureRun(args) {
326421
+ async function runLoginCaptureRunWithOutcome(args) {
325467
326422
  const { loginBrowser, baseUrl, projectId, model, onAudit, logs: logs2 } = args;
325468
326423
  const credentials = filterLoginCaptureCredentials(args.credentials);
325469
326424
  if ((0, agentic_cli_constants_js_1.getAgenticEngine)(model) !== "native") {
325470
326425
  logs2(` auth-capture skipped: engine for ${model} is not native (V1 is native-engine-only)`);
325471
- return null;
326426
+ return { recipe: null, outcome: "skipped" };
325472
326427
  }
325473
326428
  const { secrets } = (0, credential_tools_js_1.partitionTestVariables)(credentials);
325474
326429
  if (Object.keys(secrets).length === 0) {
325475
326430
  logs2(" auth-capture skipped: no secret credentials available to log in with");
325476
- return null;
326431
+ return { recipe: null, outcome: "skipped" };
325477
326432
  }
325478
326433
  const auditRemap = onAudit ? (info) => {
325479
326434
  try {
@@ -325494,7 +326449,7 @@ ${keyLines || "- (none provided)"}
325494
326449
  const mcpClient = handle.mcpClient;
325495
326450
  if (!mcpClient) {
325496
326451
  logs2(" auth-capture aborted: instrumented context has no MCP client");
325497
- return null;
326452
+ return { recipe: null, outcome: "skipped" };
325498
326453
  }
325499
326454
  const availableKeys = Object.keys(credentials);
325500
326455
  const maxIterations = getLoginCaptureMaxIterations();
@@ -325504,8 +326459,8 @@ ${keyLines || "- (none provided)"}
325504
326459
  enableRecordJourney: false,
325505
326460
  // login-only — no feature journeys to record
325506
326461
  maxIterationsOverride: maxIterations,
325507
- rateLimitMaxRetries: CAPTURE_RATE_LIMIT_MAX_RETRIES,
325508
- rateLimitMaxDelayMs: CAPTURE_RATE_LIMIT_MAX_DELAY_MS
326462
+ rateLimitMaxRetries: getCaptureRateLimitMaxRetries(),
326463
+ rateLimitMaxDelayMs: getCaptureRateLimitMaxDelayMs()
325509
326464
  };
325510
326465
  const exploreResult = await (0, discover_explorer_js_1.runExplorePhase)(mcpClient, baseUrl, {
325511
326466
  publicTestVariables: credentials,
@@ -325520,6 +326475,7 @@ ${keyLines || "- (none provided)"}
325520
326475
  onProgress: void 0
325521
326476
  }, loginConfig);
325522
326477
  logs2(` auth-capture explore: ${exploreResult.pagesDiscovered} pages, ${exploreResult.collectedPages.length} captured`);
326478
+ const exploreMiss = (fallback) => classifyCaptureMiss(exploreResult, fallback);
325523
326479
  const loginObservedOrigins = exploreResult.observedOrigins ?? [];
325524
326480
  try {
325525
326481
  args.onObservedOrigins?.(loginObservedOrigins);
@@ -325535,7 +326491,7 @@ ${keyLines || "- (none provided)"}
325535
326491
  const login = (0, discover_generator_js_1.getLoginCommands)(loginSiteMap.pages, credentials);
325536
326492
  if (!login) {
325537
326493
  logs2(" \u26A0 auth unverified (login run produced no derivable login sequence) \u2014 falling back to model-authored login");
325538
- return null;
326494
+ return { recipe: null, outcome: exploreMiss("no_login") };
325539
326495
  }
325540
326496
  const scrubbedLogin = {
325541
326497
  ...login,
@@ -325545,19 +326501,19 @@ ${keyLines || "- (none provided)"}
325545
326501
  const authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
325546
326502
  if (!authProof) {
325547
326503
  logs2(" \u26A0 auth unverified (no durable signed-in proof on the authenticated page) \u2014 falling back to model-authored login");
325548
- return null;
326504
+ return { recipe: null, outcome: exploreMiss("verify_failed") };
325549
326505
  }
325550
326506
  const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(scrubbedLogin, authProof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
325551
326507
  const rendered = (0, auth_recipe_js_1.renderAuthFn)(candidate);
325552
326508
  const leaks = findCredentialLiteralLeaks(rendered, credentials);
325553
326509
  if (leaks.length > 0) {
325554
326510
  logs2(` \u26A0 auth recipe rejected: rendered __vqaLogin contains literal credential value(s) for ${leaks.join(", ")} \u2014 falling back to model-authored login`);
325555
- return null;
326511
+ return { recipe: null, outcome: "credential_leak" };
325556
326512
  }
325557
326513
  const planLeaks = findCredentialLiteralLeaks(JSON.stringify(candidate.replayPlan ?? []), credentials);
325558
326514
  if (planLeaks.length > 0) {
325559
326515
  logs2(` \u26A0 auth recipe rejected: structured replayPlan contains literal credential value(s) for ${planLeaks.join(", ")} \u2014 falling back to model-authored login`);
325560
- return null;
326516
+ return { recipe: null, outcome: "credential_leak" };
325561
326517
  }
325562
326518
  try {
325563
326519
  await handle.close();
@@ -325567,14 +326523,14 @@ ${keyLines || "- (none provided)"}
325567
326523
  const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
325568
326524
  if (!ok) {
325569
326525
  logs2(" \u26A0 auth unverified (live-verify replay failed \u2014 fleet-config drift) \u2014 falling back to model-authored login");
325570
- return null;
326526
+ return { recipe: null, outcome: exploreMiss("verify_failed") };
325571
326527
  }
325572
326528
  const proofDesc = authProof.kind === "role" ? `role:${authProof.role}${authProof.name ? `(${authProof.name})` : ""}` : authProof.kind === "testid" ? `testid:${authProof.testId}` : `url:${authProof.route}`;
325573
326529
  logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${proofDesc}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
325574
- return candidate;
326530
+ return { recipe: candidate, outcome: "captured" };
325575
326531
  } catch (err) {
325576
326532
  logs2(` \u26A0 auth-capture run failed: ${err instanceof Error ? err.message : String(err)} \u2014 falling back to model-authored login`);
325577
- return null;
326533
+ return { recipe: null, outcome: (0, ai_retry_js_1.isTransientRateError)(err) ? "rate_limited" : "error" };
325578
326534
  } finally {
325579
326535
  if (handle) {
325580
326536
  try {
@@ -325584,6 +326540,9 @@ ${keyLines || "- (none provided)"}
325584
326540
  }
325585
326541
  }
325586
326542
  }
326543
+ async function runLoginCaptureRun(args) {
326544
+ return (await runLoginCaptureRunWithOutcome(args)).recipe;
326545
+ }
325587
326546
  var DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS = 18;
325588
326547
  function getRegisterCaptureMaxIterations(env = process.env) {
325589
326548
  return readPositiveIntEnv2(env, "AUTH_CAPTURE_REGISTER_MAX_ITERATIONS", DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS);
@@ -325655,8 +326614,8 @@ ${keyLines || "- (none provided)"}
325655
326614
  userMessage: buildRegisterCaptureUserMessage(baseUrl),
325656
326615
  enableRecordJourney: false,
325657
326616
  maxIterationsOverride: maxIterations,
325658
- rateLimitMaxRetries: CAPTURE_RATE_LIMIT_MAX_RETRIES,
325659
- rateLimitMaxDelayMs: CAPTURE_RATE_LIMIT_MAX_DELAY_MS,
326617
+ rateLimitMaxRetries: getCaptureRateLimitMaxRetries(),
326618
+ rateLimitMaxDelayMs: getCaptureRateLimitMaxDelayMs(),
325660
326619
  // Register + poll the PROJECT-SCOPED address so it matches the one the
325661
326620
  // standalone authored tests log in with (see #922).
325662
326621
  inboxTagOverride: args.inboxTag
@@ -332388,6 +333347,8 @@ var require_discover_agent = __commonJS({
332388
333347
  Object.defineProperty(exports, "__esModule", { value: true });
332389
333348
  exports.runMobileDiscoveryOnRunner = exports.buildInMemorySiteMap = void 0;
332390
333349
  exports.runCanvasPreflight = runCanvasPreflight;
333350
+ exports.runCaptureAttemptLoop = runCaptureAttemptLoop;
333351
+ exports.isCaptureRunRetryEnabled = isCaptureRunRetryEnabled;
332391
333352
  exports.getFatalExploreError = getFatalExploreError;
332392
333353
  exports.runSiteDiscoveryOnRunner = runSiteDiscoveryOnRunner2;
332393
333354
  exports.runFeatureDiscoveryOnRunner = runFeatureDiscoveryOnRunner2;
@@ -332636,8 +333597,56 @@ Respond with ONLY valid JSON:
332636
333597
  }
332637
333598
  return merged;
332638
333599
  }
332639
- var AUTH_CAPTURE_TIMEOUT_MS = 4 * 60 * 1e3;
332640
- function maybeStartLoginCaptureRun(ctx, callbackOptions, onAICall, pushLog, onObservedOrigins) {
333600
+ function computeCaptureRetryBackoffMs(attempt) {
333601
+ const capped = Math.min(3e3 * Math.pow(2, attempt - 1), 15e3);
333602
+ const jitter = 0.5 + Math.random() * 0.5;
333603
+ return Math.round(capped * jitter);
333604
+ }
333605
+ async function runCaptureAttemptLoop(args) {
333606
+ const { runCapture, maxAttempts, deadline, minRetryBudgetMs, now, sleep, backoffMs, log: log2, onAttempt } = args;
333607
+ let attemptsMade = 0;
333608
+ let lastOutcome = "error";
333609
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
333610
+ if (now() >= deadline) {
333611
+ lastOutcome = "timeout";
333612
+ break;
333613
+ }
333614
+ attemptsMade = attempt;
333615
+ onAttempt?.(attempt);
333616
+ const { recipe, outcome } = await runCapture(attempt);
333617
+ lastOutcome = outcome;
333618
+ if (recipe)
333619
+ return { recipe, outcome, attempts: attemptsMade };
333620
+ if (attempt >= maxAttempts || outcome !== "rate_limited")
333621
+ break;
333622
+ const remaining = deadline - now();
333623
+ if (remaining < minRetryBudgetMs) {
333624
+ log2(`auth-prologue: transient rate-limit miss, but only ${Math.round(remaining / 1e3)}s of the capture wall remain \u2014 not retrying`);
333625
+ break;
333626
+ }
333627
+ const backoff = Math.min(backoffMs(attempt), remaining - minRetryBudgetMs);
333628
+ if (backoff > 0) {
333629
+ log2(`auth-prologue: login-capture hit a transient rate limit (attempt ${attempt}/${maxAttempts}) \u2014 retrying in ${Math.round(backoff / 1e3)}s`);
333630
+ await sleep(backoff);
333631
+ }
333632
+ if (deadline - now() < minRetryBudgetMs) {
333633
+ log2("auth-prologue: capture wall nearly exhausted after backoff \u2014 not retrying");
333634
+ break;
333635
+ }
333636
+ }
333637
+ return { recipe: null, outcome: lastOutcome, attempts: attemptsMade };
333638
+ }
333639
+ function logAuthGenerationStats(logs2, tests, authRecipe) {
333640
+ const needsAuth = tests.filter((t) => t.tags.includes("@needs-auth"));
333641
+ if (needsAuth.length === 0)
333642
+ return;
333643
+ const recipeStamped = needsAuth.filter((t) => /VALIDATE_QA_AUTH/.test(t.playwrightCode ?? "")).length;
333644
+ logs2.push(`[auth-capture] needsAuthTests=${needsAuth.length} recipeStamped=${recipeStamped} inlineLogin=${needsAuth.length - recipeStamped} recipeAvailable=${authRecipe ? "yes" : "no"}`);
333645
+ }
333646
+ function isCaptureRunRetryEnabled(env = process.env) {
333647
+ return (0, shared_1.parseBoolFlag)(env.AUTH_CAPTURE_RUN_RETRY, true);
333648
+ }
333649
+ function maybeStartLoginCaptureRun(ctx, callbackOptions, onAICall, pushLog, onObservedOrigins, onOutcome) {
332641
333650
  if (!(0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && ctx.mode !== "capture")
332642
333651
  return null;
332643
333652
  if (ctx.authRecipe) {
@@ -332662,25 +333671,44 @@ Respond with ONLY valid JSON:
332662
333671
  return null;
332663
333672
  }
332664
333673
  const credentials = (0, auth_capture_run_js_1.buildLoginCaptureCredentials)(ctx.variableContext);
333674
+ const runStart = Date.now();
333675
+ const timeoutMs = (0, auth_capture_run_js_1.getAuthCaptureTimeoutMs)();
333676
+ const deadline = runStart + timeoutMs;
333677
+ const retryEnabled = isCaptureRunRetryEnabled();
333678
+ const maxAttempts = retryEnabled ? (0, auth_capture_run_js_1.getAuthCaptureRunRetryMaxAttempts)() : 1;
333679
+ const MIN_RETRY_BUDGET_MS = 45e3;
333680
+ let attemptsMade = 0;
332665
333681
  const run2 = (async () => {
332666
333682
  let lease = null;
332667
333683
  try {
332668
333684
  lease = await acquire();
332669
- const recipe = await (0, auth_capture_run_js_1.runLoginCaptureRun)({
332670
- loginBrowser: lease.browser,
332671
- baseUrl: ctx.baseUrl,
332672
- credentials,
332673
- projectId: ctx.projectId,
332674
- model,
332675
- existingSiteMap: ctx.existingSiteMap,
332676
- onAudit: onAICall,
332677
- onObservedOrigins,
332678
- logs: pushLog
333685
+ const leased = lease;
333686
+ return await runCaptureAttemptLoop({
333687
+ maxAttempts,
333688
+ deadline,
333689
+ minRetryBudgetMs: MIN_RETRY_BUDGET_MS,
333690
+ now: Date.now,
333691
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
333692
+ backoffMs: computeCaptureRetryBackoffMs,
333693
+ log: pushLog,
333694
+ onAttempt: (n) => {
333695
+ attemptsMade = n;
333696
+ },
333697
+ runCapture: () => (0, auth_capture_run_js_1.runLoginCaptureRunWithOutcome)({
333698
+ loginBrowser: leased.browser,
333699
+ baseUrl: ctx.baseUrl,
333700
+ credentials,
333701
+ projectId: ctx.projectId,
333702
+ model,
333703
+ existingSiteMap: ctx.existingSiteMap,
333704
+ onAudit: onAICall,
333705
+ onObservedOrigins,
333706
+ logs: pushLog
333707
+ })
332679
333708
  });
332680
- return recipe;
332681
333709
  } catch (err) {
332682
333710
  pushLog(`auth-prologue: login-capture run threw (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
332683
- return null;
333711
+ return { recipe: null, outcome: "error", attempts: attemptsMade };
332684
333712
  } finally {
332685
333713
  if (lease) {
332686
333714
  try {
@@ -332693,16 +333721,19 @@ Respond with ONLY valid JSON:
332693
333721
  let timer;
332694
333722
  const timeout = new Promise((resolve2) => {
332695
333723
  timer = setTimeout(() => {
332696
- pushLog(`auth-prologue: login-capture run exceeded ${AUTH_CAPTURE_TIMEOUT_MS / 1e3}s \u2014 proceeding without a recipe`);
332697
- resolve2(null);
332698
- }, AUTH_CAPTURE_TIMEOUT_MS);
333724
+ pushLog(`auth-prologue: login-capture run exceeded ${Math.round(timeoutMs / 1e3)}s \u2014 proceeding without a recipe`);
333725
+ resolve2({ recipe: null, outcome: "timeout", attempts: attemptsMade });
333726
+ }, Math.max(0, deadline - Date.now()));
332699
333727
  timer.unref?.();
332700
333728
  });
332701
- const timed = Promise.race([run2, timeout]).finally(() => {
333729
+ return Promise.race([run2, timeout]).finally(() => {
332702
333730
  if (timer)
332703
333731
  clearTimeout(timer);
332704
- });
332705
- return timed.catch(() => null);
333732
+ }).then((res) => {
333733
+ pushLog(`[auth-capture] outcome=${res.outcome} attempts=${res.attempts} durationMs=${Date.now() - runStart} mode=${ctx.mode ?? "discovery"}`);
333734
+ onOutcome?.(res.outcome);
333735
+ return res.recipe;
333736
+ }).catch(() => null);
332706
333737
  }
332707
333738
  function getFatalExploreError(exploreResult) {
332708
333739
  const prefix = "Fatal error:";
@@ -332717,12 +333748,15 @@ Respond with ONLY valid JSON:
332717
333748
  logs2.push(`Base URL: ${ctx.baseUrl}`);
332718
333749
  onLog?.("starting", logs2.join("\n"));
332719
333750
  const loginObservedOrigins = [];
333751
+ let captureOutcome;
332720
333752
  const loginRun = maybeStartLoginCaptureRun(ctx, callbackOptions, onAICall, (line) => {
332721
333753
  logs2.push(line);
332722
333754
  onLog?.("progress", logs2.join("\n"));
332723
333755
  }, (origins) => {
332724
333756
  for (const origin of origins)
332725
333757
  loginObservedOrigins.push(origin);
333758
+ }, (outcome) => {
333759
+ captureOutcome = outcome;
332726
333760
  });
332727
333761
  const authRecipe = loginRun ? await loginRun : null;
332728
333762
  const duration = (Date.now() - startTime) / 1e3;
@@ -332747,7 +333781,7 @@ Respond with ONLY valid JSON:
332747
333781
  },
332748
333782
  duration,
332749
333783
  mode: "capture",
332750
- ...authRecipe ? {} : { error: "Login capture did not produce a verified recipe" }
333784
+ ...authRecipe ? {} : { error: `Login capture did not produce a verified recipe${captureOutcome ? ` (${captureOutcome})` : ""}` }
332751
333785
  };
332752
333786
  }
332753
333787
  const mode = ctx.mode;
@@ -332927,14 +333961,18 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
332927
333961
  const updatedSiteMap = (0, sitemap_builder_js_1.buildInMemorySiteMap)(ctx.projectId, ctx.baseUrl, exploreResult.collectedPages, exploreResult.collectedTransitions, ctx.existingSiteMap);
332928
333962
  if (updatedSiteMap.pages.length === 0) {
332929
333963
  logs2.push("No site map data available - skipping plan and generate phases");
332930
- if (loginRunP)
332931
- await loginRunP;
333964
+ const earlyAuthRecipe = loginRunP ? await loginRunP : null;
332932
333965
  const duration2 = (Date.now() - startTime) / 1e3;
332933
333966
  const fatalExploreError = getFatalExploreError(exploreResult);
332934
333967
  return {
332935
333968
  collectedPages: exploreResult.collectedPages,
332936
333969
  collectedTransitions: exploreResult.collectedTransitions,
332937
333970
  tests: [],
333971
+ // Persist only a FRESHLY-captured recipe — never re-persist a reused cached one
333972
+ // (ctx.authRecipe set) or a late warm run could overwrite/re-verify a newer recipe.
333973
+ authRecipe: ctx.authRecipe ? void 0 : earlyAuthRecipe,
333974
+ observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
333975
+ resolvedAppOrigin: earlyAuthRecipe?.appOrigin ?? void 0,
332938
333976
  healthObservations: healthObserver?.getSummary(),
332939
333977
  logs: logs2.join("\n"),
332940
333978
  screenshots,
@@ -333022,12 +334060,6 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
333022
334060
  logs2.push(surveyHealth.totalIssues > 0 ? `
333023
334061
  Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.length} console errors, ${surveyHealth.failedRequests.length} failed requests` : "\nHealth observer: no issues detected");
333024
334062
  }
333025
- const duration2 = (Date.now() - startTime) / 1e3;
333026
- const featureCount = featureGroups?.length || finalPlan.testSuites.length;
333027
- const summary2 = `Survey complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elementsFound} elements, ${featureCount} features identified in ${duration2.toFixed(1)}s`;
333028
- logs2.push(`
333029
- === Survey Complete (${duration2.toFixed(1)}s) ===`);
333030
- logs2.push(summary2);
333031
334063
  let mergedWarning = planResult.warning;
333032
334064
  if (exploreResult.truncated) {
333033
334065
  const reasonLabel = exploreResult.truncatedReason === "rate_limit" ? "AI rate-limit" : exploreResult.truncatedReason === "timeout" ? "AI timeout" : "AI error";
@@ -333048,6 +334080,12 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
333048
334080
  if (surveyAuthRecipe) {
333049
334081
  logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
333050
334082
  }
334083
+ const duration2 = (Date.now() - startTime) / 1e3;
334084
+ const featureCount = featureGroups?.length || finalPlan.testSuites.length;
334085
+ const summary2 = `Survey complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elementsFound} elements, ${featureCount} features identified in ${duration2.toFixed(1)}s`;
334086
+ logs2.push(`
334087
+ === Survey Complete (${duration2.toFixed(1)}s) ===`);
334088
+ logs2.push(summary2);
333051
334089
  const plannerFailedError = planResult.plannerFailed ? `Survey planning failed (${planResult.plannerFailureKind ?? "unknown"}): ${planResult.fallbackReason ?? "planner did not produce a usable plan"}. Explore evidence was saved; re-run the survey to retry planning.` : void 0;
333052
334090
  return {
333053
334091
  observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
@@ -333110,6 +334148,7 @@ Generation complete: ${generateResult.tests.length} tests generated (${generateE
333110
334148
  const tags = t.tags.join(", ");
333111
334149
  logs2.push(` ${i + 1}. ${t.name} [${tags}]${t.verified ? " \u2713live" : ""}`);
333112
334150
  });
334151
+ logAuthGenerationStats(logs2, generateResult.tests, authRecipe);
333113
334152
  onLog?.("generate", logs2.join("\n"));
333114
334153
  const postGenClose = await handle.close();
333115
334154
  handle = null;
@@ -333479,8 +334518,7 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
333479
334518
  }
333480
334519
  if (!isPlanReuseResume && updatedSiteMap.pages.length === 0) {
333481
334520
  logs2.push("No site map data - skipping plan and generate phases");
333482
- if (loginRunP)
333483
- await loginRunP;
334521
+ const earlyAuthRecipe = loginRunP ? await loginRunP : null;
333484
334522
  const earlyClose = await handle.close();
333485
334523
  handle = null;
333486
334524
  discoveryApiCalls = earlyClose.apiCalls;
@@ -333491,6 +334529,11 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
333491
334529
  collectedPages: exploreResult.collectedPages,
333492
334530
  collectedTransitions: exploreResult.collectedTransitions,
333493
334531
  tests: [],
334532
+ // Persist only a FRESHLY-captured recipe — never re-persist a reused cached one
334533
+ // (ctx.authRecipe set) or a late warm run could overwrite/re-verify a newer recipe.
334534
+ authRecipe: ctx.authRecipe ? void 0 : earlyAuthRecipe,
334535
+ observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
334536
+ resolvedAppOrigin: earlyAuthRecipe?.appOrigin ?? void 0,
333494
334537
  logs: logs2.join("\n"),
333495
334538
  screenshots,
333496
334539
  apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
@@ -333631,6 +334674,7 @@ Generation complete: ${generateResult.tests.length} tests (${generateElapsed}s e
333631
334674
  const tags = t.tags.join(", ");
333632
334675
  logs2.push(` ${i + 1}. ${t.name} [${tags}]${t.verified ? " \u2713live" : ""}`);
333633
334676
  });
334677
+ logAuthGenerationStats(logs2, generateResult.tests, authRecipe);
333634
334678
  onLog?.("generate", logs2.join("\n"));
333635
334679
  const postGenClose = await handle.close();
333636
334680
  handle = null;
@@ -348095,7 +349139,7 @@ var require_package4 = __commonJS({
348095
349139
  "package.json"(exports, module) {
348096
349140
  module.exports = {
348097
349141
  name: "@validate.qa/runner",
348098
- version: "1.0.52",
349142
+ version: "1.0.53",
348099
349143
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
348100
349144
  bin: {
348101
349145
  "validate-runner": "dist/cli.js"