@validate.qa/runner 1.0.52 → 1.0.54
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.
- package/dist/cli.js +1266 -132
- package/dist/cli.mjs +1266 -132
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -5609,6 +5609,91 @@ var init_env_flags = __esm({
|
|
|
5609
5609
|
}
|
|
5610
5610
|
});
|
|
5611
5611
|
|
|
5612
|
+
// ../shared/dist/analytics-url.js
|
|
5613
|
+
function isSensitiveQueryKey(key) {
|
|
5614
|
+
return SENSITIVE_QUERY_KEY_RE.test(key);
|
|
5615
|
+
}
|
|
5616
|
+
function scrubSearchParams(search) {
|
|
5617
|
+
if (!search)
|
|
5618
|
+
return "";
|
|
5619
|
+
const raw = search.startsWith("?") ? search.slice(1) : search;
|
|
5620
|
+
if (!raw)
|
|
5621
|
+
return "";
|
|
5622
|
+
try {
|
|
5623
|
+
const params = new URLSearchParams(raw);
|
|
5624
|
+
let changed = false;
|
|
5625
|
+
for (const key of [...params.keys()]) {
|
|
5626
|
+
if (isSensitiveQueryKey(key)) {
|
|
5627
|
+
params.set(key, "[redacted]");
|
|
5628
|
+
changed = true;
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
if (!changed) {
|
|
5632
|
+
return `?${raw}`;
|
|
5633
|
+
}
|
|
5634
|
+
const next = params.toString();
|
|
5635
|
+
return next ? `?${next}` : "";
|
|
5636
|
+
} catch {
|
|
5637
|
+
return "";
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5640
|
+
function scrubUrl(url) {
|
|
5641
|
+
if (!url)
|
|
5642
|
+
return url;
|
|
5643
|
+
try {
|
|
5644
|
+
const parsed = new URL(url, typeof window !== "undefined" ? window.location.origin : "https://local.invalid");
|
|
5645
|
+
parsed.search = scrubSearchParams(parsed.search);
|
|
5646
|
+
parsed.hash = "";
|
|
5647
|
+
return parsed.toString();
|
|
5648
|
+
} catch {
|
|
5649
|
+
const noHash = url.split("#")[0] ?? url;
|
|
5650
|
+
const q = noHash.indexOf("?");
|
|
5651
|
+
if (q === -1)
|
|
5652
|
+
return noHash;
|
|
5653
|
+
return `${noHash.slice(0, q)}${scrubSearchParams(noHash.slice(q))}`;
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
function safeCurrentUrl() {
|
|
5657
|
+
if (typeof window === "undefined")
|
|
5658
|
+
return "";
|
|
5659
|
+
try {
|
|
5660
|
+
return scrubUrl(window.location.href);
|
|
5661
|
+
} catch {
|
|
5662
|
+
return window.location.pathname;
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
function stripQuery(value) {
|
|
5666
|
+
const idx = value.indexOf("?");
|
|
5667
|
+
return idx === -1 ? value : value.slice(0, idx);
|
|
5668
|
+
}
|
|
5669
|
+
function scrubUrlPropertyBag(props) {
|
|
5670
|
+
if (!props)
|
|
5671
|
+
return;
|
|
5672
|
+
const urlKeys = ["$current_url", "$referrer", "$initial_current_url", "$initial_referrer"];
|
|
5673
|
+
for (const key of urlKeys) {
|
|
5674
|
+
const value = props[key];
|
|
5675
|
+
if (typeof value === "string") {
|
|
5676
|
+
props[key] = scrubUrl(value);
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
for (const key of ["$pathname", "$initial_pathname"]) {
|
|
5680
|
+
const value = props[key];
|
|
5681
|
+
if (typeof value === "string" && value.includes("?")) {
|
|
5682
|
+
props[key] = stripQuery(value);
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
if (typeof props.search === "string") {
|
|
5686
|
+
props.search = scrubSearchParams(props.search) || void 0;
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
var SENSITIVE_QUERY_KEY_RE;
|
|
5690
|
+
var init_analytics_url = __esm({
|
|
5691
|
+
"../shared/dist/analytics-url.js"() {
|
|
5692
|
+
"use strict";
|
|
5693
|
+
SENSITIVE_QUERY_KEY_RE = /^(access_token|refresh_token|id_token|token|code|state|password|secret|api[_-]?key|auth|authorization|session|jwt|otp|email|phone|magic[_-]?link)$|token|secret|password|credential|email|phone|(^|[_-])(otp|verification|auth)[_-]?code($|[_-])|(^|[_-])(state|session)$/i;
|
|
5694
|
+
}
|
|
5695
|
+
});
|
|
5696
|
+
|
|
5612
5697
|
// ../shared/dist/index.js
|
|
5613
5698
|
var dist_exports = {};
|
|
5614
5699
|
__export(dist_exports, {
|
|
@@ -5751,6 +5836,7 @@ __export(dist_exports, {
|
|
|
5751
5836
|
isRunnableStep: () => isRunnableStep,
|
|
5752
5837
|
isSafeDeepLink: () => isSafeDeepLink,
|
|
5753
5838
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
5839
|
+
isSensitiveQueryKey: () => isSensitiveQueryKey,
|
|
5754
5840
|
isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
|
|
5755
5841
|
isSttModelForProvider: () => isSttModelForProvider,
|
|
5756
5842
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
@@ -5784,9 +5870,13 @@ __export(dist_exports, {
|
|
|
5784
5870
|
resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
|
|
5785
5871
|
resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
|
|
5786
5872
|
resolveSttModelForProvider: () => resolveSttModelForProvider,
|
|
5873
|
+
safeCurrentUrl: () => safeCurrentUrl,
|
|
5787
5874
|
safeOrigin: () => safeOrigin,
|
|
5788
5875
|
sanitizeSttErrorMessage: () => sanitizeSttErrorMessage,
|
|
5789
5876
|
sanitizeTestVariables: () => sanitizeTestVariables,
|
|
5877
|
+
scrubSearchParams: () => scrubSearchParams,
|
|
5878
|
+
scrubUrl: () => scrubUrl,
|
|
5879
|
+
scrubUrlPropertyBag: () => scrubUrlPropertyBag,
|
|
5790
5880
|
selectorCandidatesForStep: () => selectorCandidatesForStep,
|
|
5791
5881
|
selectorForValue: () => selectorForValue,
|
|
5792
5882
|
stepAppliesToPlatform: () => stepAppliesToPlatform,
|
|
@@ -5830,6 +5920,7 @@ var init_dist = __esm({
|
|
|
5830
5920
|
init_voice_config();
|
|
5831
5921
|
init_feature_flags();
|
|
5832
5922
|
init_env_flags();
|
|
5923
|
+
init_analytics_url();
|
|
5833
5924
|
}
|
|
5834
5925
|
});
|
|
5835
5926
|
|
|
@@ -7707,6 +7798,7 @@ var require_credential_tools = __commonJS({
|
|
|
7707
7798
|
exports.buildCredentialPromptBlock = buildCredentialPromptBlock;
|
|
7708
7799
|
exports.isLikelyCredentialKey = isLikelyCredentialKey;
|
|
7709
7800
|
exports.partitionTestVariables = partitionTestVariables;
|
|
7801
|
+
exports.hasUsableLoginIdentity = hasUsableLoginIdentity;
|
|
7710
7802
|
exports.renderPublicVariablesBlock = renderPublicVariablesBlock;
|
|
7711
7803
|
exports.handleFillCredential = handleFillCredential;
|
|
7712
7804
|
exports.handleFillCredentialForm = handleFillCredentialForm;
|
|
@@ -7854,6 +7946,25 @@ CREDENTIAL ROTATION (rare \u2014 read this carefully):
|
|
|
7854
7946
|
}
|
|
7855
7947
|
return { secrets, publics };
|
|
7856
7948
|
}
|
|
7949
|
+
var IDENTITY_KEY_PATTERN = /^(TEST_|E2E_|QA_)?(USER_?EMAIL|USER_?NAME|USERNAME|LOGIN_?ID|LOGIN_?EMAIL|ACCOUNT_?EMAIL)$/i;
|
|
7950
|
+
var BOOLEAN_LIKE_VALUE = /^(true|false|1|0|yes|no|on|off|enabled|disabled)$/i;
|
|
7951
|
+
var EMAIL_SHAPED_VALUE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
7952
|
+
function hasUsableLoginIdentity(vars, opts) {
|
|
7953
|
+
if (!vars)
|
|
7954
|
+
return false;
|
|
7955
|
+
return Object.entries(vars).some(([k, rawValue]) => {
|
|
7956
|
+
const v = (rawValue ?? "").trim();
|
|
7957
|
+
if (!v)
|
|
7958
|
+
return false;
|
|
7959
|
+
if (!IDENTITY_KEY_PATTERN.test(k.trim()))
|
|
7960
|
+
return false;
|
|
7961
|
+
if (BOOLEAN_LIKE_VALUE.test(v))
|
|
7962
|
+
return false;
|
|
7963
|
+
if (/EMAIL/i.test(k) && !EMAIL_SHAPED_VALUE.test(v))
|
|
7964
|
+
return false;
|
|
7965
|
+
return !opts?.isPlaceholderIdentityValue?.(v);
|
|
7966
|
+
});
|
|
7967
|
+
}
|
|
7857
7968
|
function renderPublicVariablesBlock(publics) {
|
|
7858
7969
|
const entries = Object.entries(publics);
|
|
7859
7970
|
if (entries.length === 0)
|
|
@@ -72456,6 +72567,7 @@ var require_verification_inbox = __commonJS({
|
|
|
72456
72567
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
72457
72568
|
exports.VerificationInboxReader = void 0;
|
|
72458
72569
|
exports.isVerificationInboxConfigured = isVerificationInboxConfigured;
|
|
72570
|
+
exports.describeVerificationInboxConfigGap = describeVerificationInboxConfigGap;
|
|
72459
72571
|
exports.loadVerificationInboxConfig = loadVerificationInboxConfig;
|
|
72460
72572
|
exports.shouldUseUidWatermark = shouldUseUidWatermark;
|
|
72461
72573
|
exports.recipientMatchesAddress = recipientMatchesAddress;
|
|
@@ -72477,6 +72589,21 @@ var require_verification_inbox = __commonJS({
|
|
|
72477
72589
|
function isVerificationInboxConfigured(env = process.env) {
|
|
72478
72590
|
return !!(readEnv(env, "VERIFICATION_INBOX_HOST") && readEnv(env, "VERIFICATION_INBOX_USER") && readEnv(env, "VERIFICATION_INBOX_PASSWORD") && readEnv(env, "VERIFICATION_INBOX_ADDRESS"));
|
|
72479
72591
|
}
|
|
72592
|
+
var REQUIRED_INBOX_ENV_KEYS = [
|
|
72593
|
+
"VERIFICATION_INBOX_HOST",
|
|
72594
|
+
"VERIFICATION_INBOX_USER",
|
|
72595
|
+
"VERIFICATION_INBOX_PASSWORD",
|
|
72596
|
+
"VERIFICATION_INBOX_ADDRESS"
|
|
72597
|
+
];
|
|
72598
|
+
function describeVerificationInboxConfigGap(env = process.env) {
|
|
72599
|
+
const missing = REQUIRED_INBOX_ENV_KEYS.filter((key) => !readEnv(env, key));
|
|
72600
|
+
if (missing.length === 0)
|
|
72601
|
+
return null;
|
|
72602
|
+
if (missing.length === REQUIRED_INBOX_ENV_KEYS.length) {
|
|
72603
|
+
return "verification inbox not configured (no VERIFICATION_INBOX_* env)";
|
|
72604
|
+
}
|
|
72605
|
+
return `verification inbox PARTIALLY configured \u2014 missing: ${missing.join(", ")}`;
|
|
72606
|
+
}
|
|
72480
72607
|
function loadVerificationInboxConfig(env = process.env) {
|
|
72481
72608
|
if (!isVerificationInboxConfigured(env))
|
|
72482
72609
|
return null;
|
|
@@ -73085,14 +73212,16 @@ var require_inbox_tools = __commonJS({
|
|
|
73085
73212
|
"../runner-core/dist/services/inbox-tools.js"(exports) {
|
|
73086
73213
|
"use strict";
|
|
73087
73214
|
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;
|
|
73215
|
+
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
73216
|
exports.createInboxRunState = createInboxRunState;
|
|
73090
73217
|
exports.dispatchInboxTool = dispatchInboxTool;
|
|
73091
73218
|
exports.isPublicHttpUrl = isPublicHttpUrl;
|
|
73092
73219
|
exports.buildInboxPromptBlock = buildInboxPromptBlock;
|
|
73220
|
+
exports.guardVerificationCodeFabrication = guardVerificationCodeFabrication;
|
|
73093
73221
|
exports.hasRegisterIntent = hasRegisterIntent;
|
|
73094
73222
|
exports.resolveInboxRegistration = resolveInboxRegistration;
|
|
73095
73223
|
exports.applyInboxRegistrationIdentity = applyInboxRegistrationIdentity;
|
|
73224
|
+
exports.isThrowawayRegisterSeedEmail = isThrowawayRegisterSeedEmail;
|
|
73096
73225
|
exports.applyAndPersistInboxIdentity = applyAndPersistInboxIdentity;
|
|
73097
73226
|
exports.clearThrowawayRegisterSeed = clearThrowawayRegisterSeed;
|
|
73098
73227
|
exports.buildInboxAgentsGuidance = buildInboxAgentsGuidance;
|
|
@@ -73542,6 +73671,50 @@ Rules:
|
|
|
73542
73671
|
- 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
73672
|
`;
|
|
73544
73673
|
}
|
|
73674
|
+
exports.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = `## EMAIL VERIFICATION UNAVAILABLE \u2014 FINAL OVERRIDE (supersedes any instruction above)
|
|
73675
|
+
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:
|
|
73676
|
+
- 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.
|
|
73677
|
+
- 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.
|
|
73678
|
+
- 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.
|
|
73679
|
+
- 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.
|
|
73680
|
+
- 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.
|
|
73681
|
+
- 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).
|
|
73682
|
+
- 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).`;
|
|
73683
|
+
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;
|
|
73684
|
+
var GENERIC_CODE_OR_PIN = /\b(code|pin)\b/i;
|
|
73685
|
+
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;
|
|
73686
|
+
function guardVerificationCodeFabrication(toolName, toolArgs) {
|
|
73687
|
+
const isRawTool = toolName === "browser_type" || toolName === "browser_fill_form";
|
|
73688
|
+
const isCredentialTool = toolName === "fill_credential" || toolName === "fill_credential_form";
|
|
73689
|
+
if (!isRawTool && !isCredentialTool)
|
|
73690
|
+
return { blocked: false };
|
|
73691
|
+
const descriptors = [];
|
|
73692
|
+
if (toolName === "browser_type" || toolName === "fill_credential") {
|
|
73693
|
+
descriptors.push(String(toolArgs.element ?? ""));
|
|
73694
|
+
} else {
|
|
73695
|
+
const fields = Array.isArray(toolArgs.fields) ? toolArgs.fields : [];
|
|
73696
|
+
for (const f of fields) {
|
|
73697
|
+
if (!f || typeof f !== "object")
|
|
73698
|
+
continue;
|
|
73699
|
+
const entry = f;
|
|
73700
|
+
descriptors.push([entry.name, entry.element, entry.field, entry.label, entry.placeholder, entry.ariaLabel, entry.description].filter((part) => typeof part === "string").join(" "));
|
|
73701
|
+
}
|
|
73702
|
+
}
|
|
73703
|
+
for (const descriptor of descriptors) {
|
|
73704
|
+
if (!descriptor)
|
|
73705
|
+
continue;
|
|
73706
|
+
const otpSpecific = VERIFICATION_CODE_FIELD_PATTERN.test(descriptor);
|
|
73707
|
+
const genericCodeField = GENERIC_CODE_OR_PIN.test(descriptor) && !BENIGN_QUALIFIED_CODE.test(descriptor);
|
|
73708
|
+
if (otpSpecific || genericCodeField) {
|
|
73709
|
+
return {
|
|
73710
|
+
blocked: true,
|
|
73711
|
+
matchedField: descriptor.slice(0, 120),
|
|
73712
|
+
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."
|
|
73713
|
+
};
|
|
73714
|
+
}
|
|
73715
|
+
}
|
|
73716
|
+
return { blocked: false };
|
|
73717
|
+
}
|
|
73545
73718
|
function hasRegisterIntent(variableContext) {
|
|
73546
73719
|
const v = variableContext.allTestVariables?.DISCOVERY_SHOULD_REGISTER ?? variableContext.publicTestVariables?.DISCOVERY_SHOULD_REGISTER ?? "";
|
|
73547
73720
|
return String(v).trim().toLowerCase() === "true";
|
|
@@ -73579,7 +73752,11 @@ Rules:
|
|
|
73579
73752
|
vars[passwordKey] = derived;
|
|
73580
73753
|
}
|
|
73581
73754
|
}
|
|
73582
|
-
var THROWAWAY_REGISTER_EMAIL = /^testuser
|
|
73755
|
+
var THROWAWAY_REGISTER_EMAIL = /^testuser\+(\d{10,16}|[0-9a-f]{10})@validate\.qa$/i;
|
|
73756
|
+
function isThrowawayRegisterSeedEmail(value) {
|
|
73757
|
+
const v = (value ?? "").trim();
|
|
73758
|
+
return !!v && THROWAWAY_REGISTER_EMAIL.test(v);
|
|
73759
|
+
}
|
|
73583
73760
|
async function applyAndPersistInboxIdentity(opts) {
|
|
73584
73761
|
const { variableContext, registration, persist, log: log2 } = opts;
|
|
73585
73762
|
const vars = variableContext.allTestVariables ??= {};
|
|
@@ -318095,6 +318272,8 @@ var require_exploration_state = __commonJS({
|
|
|
318095
318272
|
existing.protected = true;
|
|
318096
318273
|
if (entry.label && !existing.label)
|
|
318097
318274
|
existing.label = entry.label;
|
|
318275
|
+
if (entry.provenance && !existing.provenance)
|
|
318276
|
+
existing.provenance = entry.provenance;
|
|
318098
318277
|
protectedCount++;
|
|
318099
318278
|
}
|
|
318100
318279
|
continue;
|
|
@@ -318104,10 +318283,10 @@ var require_exploration_state = __commonJS({
|
|
|
318104
318283
|
continue;
|
|
318105
318284
|
let evictKey = null;
|
|
318106
318285
|
let evictIter = -1;
|
|
318107
|
-
for (const [key,
|
|
318108
|
-
if (
|
|
318286
|
+
for (const [key, item2] of state2.frontier) {
|
|
318287
|
+
if (item2.source === "link-seen" && !item2.protected && item2.discoveredAtIteration >= evictIter) {
|
|
318109
318288
|
evictKey = key;
|
|
318110
|
-
evictIter =
|
|
318289
|
+
evictIter = item2.discoveredAtIteration;
|
|
318111
318290
|
}
|
|
318112
318291
|
}
|
|
318113
318292
|
if (evictKey === null)
|
|
@@ -318116,14 +318295,17 @@ var require_exploration_state = __commonJS({
|
|
|
318116
318295
|
linkSeenCount--;
|
|
318117
318296
|
}
|
|
318118
318297
|
const canProtect = entry.protected && protectedCount < exports.PROTECTED_FRONTIER_CAP;
|
|
318119
|
-
|
|
318298
|
+
const item = {
|
|
318120
318299
|
route: entry.route,
|
|
318121
318300
|
source: "link-seen",
|
|
318122
318301
|
discoveredAtIteration: state2.iteration,
|
|
318123
318302
|
label: entry.label,
|
|
318124
318303
|
protected: canProtect,
|
|
318125
318304
|
familyKey: entry.familyKey
|
|
318126
|
-
}
|
|
318305
|
+
};
|
|
318306
|
+
if (entry.provenance)
|
|
318307
|
+
item.provenance = entry.provenance;
|
|
318308
|
+
state2.frontier.set(entry.route, item);
|
|
318127
318309
|
linkSeenCount++;
|
|
318128
318310
|
if (canProtect)
|
|
318129
318311
|
protectedCount++;
|
|
@@ -318830,9 +319012,11 @@ var require_survey_link_frontier = __commonJS({
|
|
|
318830
319012
|
exports.isSurveyLinkFrontierEnabled = isSurveyLinkFrontierEnabled;
|
|
318831
319013
|
exports.extractSnapshotLinks = extractSnapshotLinks;
|
|
318832
319014
|
exports.deriveLinkScope = deriveLinkScope;
|
|
319015
|
+
exports.sameAppOrigin = sameAppOrigin;
|
|
318833
319016
|
exports.computeFamilyKey = computeFamilyKey;
|
|
318834
319017
|
exports.basePathDepthOf = basePathDepthOf;
|
|
318835
319018
|
exports.ingestSnapshotLinks = ingestSnapshotLinks;
|
|
319019
|
+
exports.ingestSeenLinks = ingestSeenLinks;
|
|
318836
319020
|
exports.visitedSatisfies = visitedSatisfies;
|
|
318837
319021
|
exports.markLinkNavigationAttempt = markLinkNavigationAttempt;
|
|
318838
319022
|
exports.getFinishBlockingFrontier = getFinishBlockingFrontier;
|
|
@@ -318891,6 +319075,20 @@ var require_survey_link_frontier = __commonJS({
|
|
|
318891
319075
|
}
|
|
318892
319076
|
}
|
|
318893
319077
|
var NON_HTTP_SCHEME_RE = /^(mailto|tel|sms|javascript|data|blob|ftp|file|about|chrome|intent):/i;
|
|
319078
|
+
function sameAppOrigin(a, b) {
|
|
319079
|
+
if (!a || !b)
|
|
319080
|
+
return false;
|
|
319081
|
+
if (a === b)
|
|
319082
|
+
return true;
|
|
319083
|
+
try {
|
|
319084
|
+
const ua = new URL(a);
|
|
319085
|
+
const ub = new URL(b);
|
|
319086
|
+
const host = (u) => u.host.replace(/^www\./i, "");
|
|
319087
|
+
return ua.protocol === ub.protocol && host(ua) === host(ub);
|
|
319088
|
+
} catch {
|
|
319089
|
+
return false;
|
|
319090
|
+
}
|
|
319091
|
+
}
|
|
318894
319092
|
var GENERATOR_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
318895
319093
|
"page",
|
|
318896
319094
|
"p",
|
|
@@ -318959,9 +319157,15 @@ var require_survey_link_frontier = __commonJS({
|
|
|
318959
319157
|
return (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).split("/").filter(Boolean).length;
|
|
318960
319158
|
}
|
|
318961
319159
|
function ingestSnapshotLinks(state2, params) {
|
|
318962
|
-
const { rawSnapshot,
|
|
319160
|
+
const { rawSnapshot, ...rest } = params;
|
|
319161
|
+
ingestSeenLinks(state2, { ...rest, links: extractSnapshotLinks(rawSnapshot), provenance: "snapshot" });
|
|
319162
|
+
}
|
|
319163
|
+
var SESSION_KILLER_PATH_RE = /(^|\/)(log[-_]?out|sign[-_]?out|sign[-_]?off|sessions?\/(destroy|delete))([/?#.]|$)/i;
|
|
319164
|
+
function ingestSeenLinks(state2, params) {
|
|
319165
|
+
const { links, scope, isEntryOrHub, isErrorContext, pageUrl, log: log2, provenance = "snapshot" } = params;
|
|
318963
319166
|
if (isErrorContext)
|
|
318964
319167
|
return;
|
|
319168
|
+
const originsMatch = (a, b) => provenance === "dom" ? sameAppOrigin(a, b) : a === b;
|
|
318965
319169
|
let pageBase = null;
|
|
318966
319170
|
if (pageUrl) {
|
|
318967
319171
|
try {
|
|
@@ -318970,11 +319174,11 @@ var require_survey_link_frontier = __commonJS({
|
|
|
318970
319174
|
pageBase = null;
|
|
318971
319175
|
}
|
|
318972
319176
|
}
|
|
318973
|
-
if (pageBase && scope.origin && pageBase.origin
|
|
319177
|
+
if (pageBase && scope.origin && !originsMatch(pageBase.origin, scope.origin)) {
|
|
318974
319178
|
log2?.(` [link-frontier] skipped off-origin page ${pageBase.origin}`);
|
|
318975
319179
|
return;
|
|
318976
319180
|
}
|
|
318977
|
-
const seen =
|
|
319181
|
+
const seen = links;
|
|
318978
319182
|
if (seen.length === 0)
|
|
318979
319183
|
return;
|
|
318980
319184
|
const suppressed = [];
|
|
@@ -319020,14 +319224,14 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319020
319224
|
suppressed.push(`${raw} (unparseable)`);
|
|
319021
319225
|
continue;
|
|
319022
319226
|
}
|
|
319023
|
-
if (scope.origin && abs.origin
|
|
319227
|
+
if (scope.origin && !originsMatch(abs.origin, scope.origin)) {
|
|
319024
319228
|
suppressed.push(`${raw} (external)`);
|
|
319025
319229
|
continue;
|
|
319026
319230
|
}
|
|
319027
319231
|
if (abs.hash && !abs.hash.startsWith("#/"))
|
|
319028
319232
|
abs.hash = "";
|
|
319029
319233
|
const route2 = (0, exploration_parser_js_1.canonicalizeRoute)(abs.pathname + abs.search + abs.hash);
|
|
319030
|
-
candidates.push({ route: route2, label: link.label });
|
|
319234
|
+
candidates.push({ route: route2, label: link.label, hidden: link.hidden, inNav: link.inNav, inFooter: link.inFooter });
|
|
319031
319235
|
}
|
|
319032
319236
|
candidates.sort((a, b) => {
|
|
319033
319237
|
const aVariant = a.route === (0, exploration_parser_js_1.normalizeRouteForFocusScope)(a.route) ? 0 : 1;
|
|
@@ -319041,6 +319245,10 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319041
319245
|
if (stripped)
|
|
319042
319246
|
suppressed.push(`${candidate.route} (generator-stripped -> ${route2})`);
|
|
319043
319247
|
const scopePath = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2);
|
|
319248
|
+
if (provenance === "dom" && SESSION_KILLER_PATH_RE.test(scopePath)) {
|
|
319249
|
+
suppressed.push(`${route2} (session-killer)`);
|
|
319250
|
+
continue;
|
|
319251
|
+
}
|
|
319044
319252
|
if (scope.basePath !== "/" && !(0, exploration_parser_js_1.routeIsWithinFocusAreas)(route2, [scope.basePath])) {
|
|
319045
319253
|
suppressed.push(`${route2} (out-of-scope)`);
|
|
319046
319254
|
continue;
|
|
@@ -319049,9 +319257,10 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319049
319257
|
suppressed.push(`${route2} (root)`);
|
|
319050
319258
|
continue;
|
|
319051
319259
|
}
|
|
319260
|
+
const doorLike = provenance === "dom" && candidate.inNav === true && candidate.hidden === true;
|
|
319052
319261
|
const existing = state2.frontier.get(route2);
|
|
319053
319262
|
if (existing) {
|
|
319054
|
-
const upgradeCandidate = isEntryOrHub && existing.source === "link-seen" && !existing.protected;
|
|
319263
|
+
const upgradeCandidate = (isEntryOrHub || doorLike) && existing.source === "link-seen" && !existing.protected;
|
|
319055
319264
|
if (!upgradeCandidate)
|
|
319056
319265
|
continue;
|
|
319057
319266
|
}
|
|
@@ -319073,9 +319282,17 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319073
319282
|
familiesInBatch.add(familyKey);
|
|
319074
319283
|
seenScopedPaths.add(scopePath);
|
|
319075
319284
|
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({
|
|
319285
|
+
const isFooterLegal = FOOTER_LEGAL_PATH_RE.test(relPath) || provenance === "dom" && candidate.inFooter === true;
|
|
319286
|
+
const isProtected = (isEntryOrHub || doorLike) && !isTemplate && !isFooterLegal;
|
|
319287
|
+
entries.push({
|
|
319288
|
+
route: route2,
|
|
319289
|
+
label,
|
|
319290
|
+
protected: isProtected,
|
|
319291
|
+
familyKey,
|
|
319292
|
+
// Snapshot entries stay provenance-free — the legacy state shape must be
|
|
319293
|
+
// byte-identical when the DOM-harvest flag is off (flag-off invariance).
|
|
319294
|
+
provenance: provenance === "dom" ? candidate.hidden ? "dom-hidden" : "dom" : void 0
|
|
319295
|
+
});
|
|
319079
319296
|
}
|
|
319080
319297
|
if (suppressed.length > 0 && log2) {
|
|
319081
319298
|
log2(` [link-frontier] suppressed ${suppressed.length}: ${suppressed.slice(0, 8).join(", ")}${suppressed.length > 8 ? ` \u2026+${suppressed.length - 8}` : ""}`);
|
|
@@ -319087,7 +319304,10 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319087
319304
|
(0, exploration_state_js_1.reduceEvent)(state2, { type: "LINKS_DISCOVERED", entries });
|
|
319088
319305
|
const admitted = countLinkSeen(state2) - before;
|
|
319089
319306
|
if (admitted > 0 && log2) {
|
|
319090
|
-
|
|
319307
|
+
const tag = provenance === "dom" ? " [dom-harvest]" : "";
|
|
319308
|
+
const hiddenCount = entries.filter((e) => e.provenance === "dom-hidden").length;
|
|
319309
|
+
const hiddenNote = hiddenCount > 0 ? ` (${hiddenCount} hidden-menu door(s))` : "";
|
|
319310
|
+
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
319311
|
}
|
|
319092
319312
|
const deniedProtected = entries.filter((e) => e.protected && state2.frontier.get(e.route) && !state2.frontier.get(e.route)?.protected);
|
|
319093
319313
|
if (deniedProtected.length > 0 && log2) {
|
|
@@ -319214,10 +319434,10 @@ var require_exploration_prompt_builder = __commonJS({
|
|
|
319214
319434
|
sections.push(`Mode: ${config.mode} | Budget remaining: ${config.maxIterations - state2.iteration} iterations`);
|
|
319215
319435
|
const budgetPct = state2.iteration / config.maxIterations;
|
|
319216
319436
|
const budgetBlocking = config.linkFrontierEnabled ? (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2) : [];
|
|
319217
|
-
if (budgetPct >= 0.95) {
|
|
319437
|
+
if (!config.adaptiveBudget && budgetPct >= 0.95) {
|
|
319218
319438
|
sections.push("");
|
|
319219
319439
|
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) {
|
|
319440
|
+
} else if (!config.adaptiveBudget && budgetPct >= 0.9) {
|
|
319221
319441
|
sections.push("");
|
|
319222
319442
|
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
319443
|
}
|
|
@@ -319603,9 +319823,9 @@ var require_exploration_prompt_builder = __commonJS({
|
|
|
319603
319823
|
sections.push("## Control Brief");
|
|
319604
319824
|
sections.push(`Current route: ${state2.currentRoute} | Iteration ${state2.iteration}/${config.maxIterations} | Budget remaining: ${config.maxIterations - state2.iteration}`);
|
|
319605
319825
|
const budgetPct = state2.iteration / config.maxIterations;
|
|
319606
|
-
if (budgetPct >= 0.95) {
|
|
319826
|
+
if (!config.adaptiveBudget && budgetPct >= 0.95) {
|
|
319607
319827
|
sections.push("**CRITICAL: FINAL ITERATIONS. Call finish_exploration NOW.**");
|
|
319608
|
-
} else if (budgetPct >= 0.9) {
|
|
319828
|
+
} else if (!config.adaptiveBudget && budgetPct >= 0.9) {
|
|
319609
319829
|
sections.push("**CRITICAL: 90%+ budget used. Wrap up and call finish_exploration.**");
|
|
319610
319830
|
}
|
|
319611
319831
|
if (state2.routeState.size > 0) {
|
|
@@ -321573,6 +321793,332 @@ var require_evidence_harvester = __commonJS({
|
|
|
321573
321793
|
}
|
|
321574
321794
|
});
|
|
321575
321795
|
|
|
321796
|
+
// ../runner-core/dist/services/survey-door-harvester.js
|
|
321797
|
+
var require_survey_door_harvester = __commonJS({
|
|
321798
|
+
"../runner-core/dist/services/survey-door-harvester.js"(exports) {
|
|
321799
|
+
"use strict";
|
|
321800
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
321801
|
+
exports.PAGE_URL_PROBE_FN = exports.MENU_EXPAND_COLLECT_FN = exports.DOM_LINK_COLLECT_FN = void 0;
|
|
321802
|
+
exports.isDomHarvestEnabled = isDomHarvestEnabled;
|
|
321803
|
+
exports.isMenuExpandEnabled = isMenuExpandEnabled;
|
|
321804
|
+
exports.parseEvaluateResult = parseEvaluateResult;
|
|
321805
|
+
exports.sortDoorsFirst = sortDoorsFirst;
|
|
321806
|
+
exports.harvestDomLinks = harvestDomLinks;
|
|
321807
|
+
exports.toSeenLinks = toSeenLinks;
|
|
321808
|
+
function isDomHarvestEnabled() {
|
|
321809
|
+
const v = process.env.DISCOVERY_SURVEY_DOM_HARVEST;
|
|
321810
|
+
return v === "true" || v === "1";
|
|
321811
|
+
}
|
|
321812
|
+
function isMenuExpandEnabled() {
|
|
321813
|
+
const v = process.env.DISCOVERY_SURVEY_MENU_EXPAND;
|
|
321814
|
+
return (v === "true" || v === "1") && isDomHarvestEnabled();
|
|
321815
|
+
}
|
|
321816
|
+
var MAX_DOM_LINKS = 150;
|
|
321817
|
+
var MAX_EXPAND_TRIGGERS = 4;
|
|
321818
|
+
var EVALUATE_TIMEOUT_MS = 1e4;
|
|
321819
|
+
exports.DOM_LINK_COLLECT_FN = `() => {
|
|
321820
|
+
const doors = [];
|
|
321821
|
+
const rest = [];
|
|
321822
|
+
const seen = new Set();
|
|
321823
|
+
const styleHidden = (el) => {
|
|
321824
|
+
try {
|
|
321825
|
+
const s = window.getComputedStyle(el);
|
|
321826
|
+
return s.display === 'none' || s.visibility === 'hidden';
|
|
321827
|
+
} catch { return false; }
|
|
321828
|
+
};
|
|
321829
|
+
const anchors = document.querySelectorAll('a[href], area[href]');
|
|
321830
|
+
for (const a of anchors) {
|
|
321831
|
+
const raw = a.getAttribute('href') || '';
|
|
321832
|
+
if (!raw || raw === '#') continue;
|
|
321833
|
+
if (/^(javascript|mailto|tel|sms|data|blob|about):/i.test(raw)) continue;
|
|
321834
|
+
if (a.hasAttribute('download')) continue;
|
|
321835
|
+
let resolved = '';
|
|
321836
|
+
try { resolved = a.href || ''; } catch {}
|
|
321837
|
+
if (typeof resolved !== 'string') resolved = String(resolved.baseVal || '');
|
|
321838
|
+
const key = raw + '|' + resolved;
|
|
321839
|
+
if (seen.has(key)) continue;
|
|
321840
|
+
seen.add(key);
|
|
321841
|
+
const label = (a.getAttribute('aria-label') || a.textContent || '')
|
|
321842
|
+
.replace(/\\s+/g, ' ').trim().slice(0, 80);
|
|
321843
|
+
let hidden = false;
|
|
321844
|
+
let node = a;
|
|
321845
|
+
while (node && node !== document.documentElement) {
|
|
321846
|
+
if (styleHidden(node)) { hidden = true; break; }
|
|
321847
|
+
node = node.parentElement;
|
|
321848
|
+
}
|
|
321849
|
+
// Footer context WINS over nav: <footer><nav> link farms are boilerplate,
|
|
321850
|
+
// not doors (sol implementation-review P1).
|
|
321851
|
+
const inFooter = !!a.closest('footer,[role="contentinfo"]');
|
|
321852
|
+
const inNav = !inFooter && !!a.closest('nav,[role="navigation"],[role="menubar"],[role="menu"],header,[role="banner"]');
|
|
321853
|
+
const entry = { href: raw.slice(0, 500), resolved: resolved.slice(0, 500), label, hidden, inNav, inFooter };
|
|
321854
|
+
if (inNav || hidden) doors.push(entry); else rest.push(entry);
|
|
321855
|
+
if (doors.length >= ${MAX_DOM_LINKS}) break;
|
|
321856
|
+
}
|
|
321857
|
+
const links = doors.concat(rest).slice(0, ${MAX_DOM_LINKS});
|
|
321858
|
+
return { pageUrl: location.href, links };
|
|
321859
|
+
}`;
|
|
321860
|
+
exports.MENU_EXPAND_COLLECT_FN = `async () => {
|
|
321861
|
+
const startUrl = location.href;
|
|
321862
|
+
// Baseline pass records KEYS ONLY (no output budget consumed) \u2014 a page with
|
|
321863
|
+
// 150+ static anchors must not crowd freshly-mounted menu links out of the
|
|
321864
|
+
// result. Only post-click NEW anchors are returned.
|
|
321865
|
+
const seen = new Set();
|
|
321866
|
+
const out = [];
|
|
321867
|
+
const keyOf = (a) => {
|
|
321868
|
+
const raw = a.getAttribute('href') || '';
|
|
321869
|
+
if (!raw || raw === '#') return null;
|
|
321870
|
+
if (/^(javascript|mailto|tel|sms|data|blob|about):/i.test(raw)) return null;
|
|
321871
|
+
if (a.hasAttribute('download')) return null;
|
|
321872
|
+
let resolved = '';
|
|
321873
|
+
try { resolved = a.href || ''; } catch {}
|
|
321874
|
+
if (typeof resolved !== 'string') resolved = String(resolved.baseVal || '');
|
|
321875
|
+
return { raw, resolved, key: raw + '|' + resolved };
|
|
321876
|
+
};
|
|
321877
|
+
for (const a of document.querySelectorAll('a[href], area[href]')) {
|
|
321878
|
+
const k = keyOf(a);
|
|
321879
|
+
if (k) seen.add(k.key);
|
|
321880
|
+
}
|
|
321881
|
+
const collectNew = () => {
|
|
321882
|
+
for (const a of document.querySelectorAll('a[href], area[href]')) {
|
|
321883
|
+
if (out.length >= ${MAX_DOM_LINKS}) return;
|
|
321884
|
+
const k = keyOf(a);
|
|
321885
|
+
if (!k || seen.has(k.key)) continue;
|
|
321886
|
+
seen.add(k.key);
|
|
321887
|
+
const label = (a.getAttribute('aria-label') || a.textContent || '')
|
|
321888
|
+
.replace(/\\s+/g, ' ').trim().slice(0, 80);
|
|
321889
|
+
// hidden:true \u2014 mounted-on-demand links are absent from a resting
|
|
321890
|
+
// accessibility snapshot, which is exactly what "hidden door" means.
|
|
321891
|
+
out.push({ href: k.raw.slice(0, 500), resolved: k.resolved.slice(0, 500), label, hidden: true, inNav: true, inFooter: false });
|
|
321892
|
+
}
|
|
321893
|
+
};
|
|
321894
|
+
const chrome = 'nav,[role="navigation"],[role="menubar"],header,[role="banner"]';
|
|
321895
|
+
const destructive = /(delete|remove|archive|reset|disconnect|deactivate|destroy|purge|log\\s*out|sign\\s*out)/i;
|
|
321896
|
+
const styleVisible = (el) => {
|
|
321897
|
+
let node = el;
|
|
321898
|
+
while (node && node !== document.documentElement) {
|
|
321899
|
+
try {
|
|
321900
|
+
const s = window.getComputedStyle(node);
|
|
321901
|
+
if (s.display === 'none' || s.visibility === 'hidden') return false;
|
|
321902
|
+
} catch { return false; }
|
|
321903
|
+
node = node.parentElement;
|
|
321904
|
+
}
|
|
321905
|
+
return true;
|
|
321906
|
+
};
|
|
321907
|
+
const candidates = [];
|
|
321908
|
+
for (const el of document.querySelectorAll('button[aria-haspopup], button[aria-expanded="false"], [role="button"][aria-haspopup], [role="button"][aria-expanded="false"]')) {
|
|
321909
|
+
if (candidates.length >= ${MAX_EXPAND_TRIGGERS}) break;
|
|
321910
|
+
if (el.tagName === 'A' || el.closest('a[href]')) continue;
|
|
321911
|
+
const haspopup = el.getAttribute('aria-haspopup');
|
|
321912
|
+
const expanded = el.getAttribute('aria-expanded');
|
|
321913
|
+
if (haspopup === 'false') continue; // explicit no-popup
|
|
321914
|
+
if (expanded === 'true') continue; // already open \u2014 not ours to toggle
|
|
321915
|
+
if (el.form || el.closest('form')) continue; // form-associated (incl. form="" attr)
|
|
321916
|
+
const type = (el.getAttribute('type') || '').toLowerCase();
|
|
321917
|
+
if (type === 'submit' || type === 'reset') continue;
|
|
321918
|
+
if (!el.closest(chrome)) continue;
|
|
321919
|
+
if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') continue;
|
|
321920
|
+
if (!styleVisible(el)) continue; // hidden triggers keep bogus geometry
|
|
321921
|
+
const name = (el.getAttribute('aria-label') || el.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
321922
|
+
if (destructive.test(name)) continue;
|
|
321923
|
+
const rect = el.getBoundingClientRect();
|
|
321924
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
321925
|
+
candidates.push(el);
|
|
321926
|
+
}
|
|
321927
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
321928
|
+
let expanded = 0;
|
|
321929
|
+
for (const el of candidates) {
|
|
321930
|
+
let opened = false;
|
|
321931
|
+
try {
|
|
321932
|
+
try { el.click(); opened = true; } catch { continue; }
|
|
321933
|
+
expanded++;
|
|
321934
|
+
await sleep(350);
|
|
321935
|
+
if (location.href !== startUrl) {
|
|
321936
|
+
return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: true, originalUrl: startUrl };
|
|
321937
|
+
}
|
|
321938
|
+
collectNew();
|
|
321939
|
+
} finally {
|
|
321940
|
+
// Restore the exact trigger we opened: aria-expanded still "true" needs a
|
|
321941
|
+
// closing click; an attribute-less toggle gets clicked back once too.
|
|
321942
|
+
if (opened) {
|
|
321943
|
+
try {
|
|
321944
|
+
const nowExpanded = el.getAttribute('aria-expanded');
|
|
321945
|
+
if (nowExpanded !== 'false') el.click();
|
|
321946
|
+
} catch {}
|
|
321947
|
+
}
|
|
321948
|
+
}
|
|
321949
|
+
await sleep(100);
|
|
321950
|
+
if (location.href !== startUrl) {
|
|
321951
|
+
return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: true, originalUrl: startUrl };
|
|
321952
|
+
}
|
|
321953
|
+
}
|
|
321954
|
+
return { pageUrl: location.href, links: out, expandedTriggers: expanded, navigated: false, originalUrl: startUrl };
|
|
321955
|
+
}`;
|
|
321956
|
+
exports.PAGE_URL_PROBE_FN = `() => location.href`;
|
|
321957
|
+
function parseEvaluateResult(mcpResult) {
|
|
321958
|
+
if (!mcpResult || typeof mcpResult !== "object")
|
|
321959
|
+
return null;
|
|
321960
|
+
if (mcpResult.isError === true)
|
|
321961
|
+
return null;
|
|
321962
|
+
const content = mcpResult.content;
|
|
321963
|
+
if (!Array.isArray(content))
|
|
321964
|
+
return null;
|
|
321965
|
+
for (const item of content) {
|
|
321966
|
+
if (!item || typeof item !== "object")
|
|
321967
|
+
continue;
|
|
321968
|
+
const { type, text } = item;
|
|
321969
|
+
if (type !== "text" || typeof text !== "string" || !text.trim())
|
|
321970
|
+
continue;
|
|
321971
|
+
let payload = null;
|
|
321972
|
+
const section = /(?:^|\n)### Result\s*\n([\s\S]*?)(?=\n### |$)/.exec(text);
|
|
321973
|
+
if (section) {
|
|
321974
|
+
payload = section[1];
|
|
321975
|
+
} else if (!text.trimStart().startsWith("###")) {
|
|
321976
|
+
payload = text;
|
|
321977
|
+
}
|
|
321978
|
+
if (!payload)
|
|
321979
|
+
continue;
|
|
321980
|
+
const cleaned = payload.replace(/^\s*```(json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
|
|
321981
|
+
if (!cleaned)
|
|
321982
|
+
continue;
|
|
321983
|
+
try {
|
|
321984
|
+
return JSON.parse(cleaned);
|
|
321985
|
+
} catch {
|
|
321986
|
+
continue;
|
|
321987
|
+
}
|
|
321988
|
+
}
|
|
321989
|
+
return null;
|
|
321990
|
+
}
|
|
321991
|
+
function normalizeLinks(value) {
|
|
321992
|
+
if (!value || typeof value !== "object")
|
|
321993
|
+
return [];
|
|
321994
|
+
const links = value.links;
|
|
321995
|
+
if (!Array.isArray(links))
|
|
321996
|
+
return [];
|
|
321997
|
+
const out = [];
|
|
321998
|
+
for (const raw of links) {
|
|
321999
|
+
if (!raw || typeof raw !== "object")
|
|
322000
|
+
continue;
|
|
322001
|
+
const l = raw;
|
|
322002
|
+
if (typeof l.href !== "string" || !l.href)
|
|
322003
|
+
continue;
|
|
322004
|
+
out.push({
|
|
322005
|
+
href: l.href,
|
|
322006
|
+
resolved: typeof l.resolved === "string" ? l.resolved : "",
|
|
322007
|
+
label: typeof l.label === "string" && l.label ? l.label : void 0,
|
|
322008
|
+
hidden: l.hidden === true,
|
|
322009
|
+
inNav: l.inNav === true,
|
|
322010
|
+
inFooter: l.inFooter === true
|
|
322011
|
+
});
|
|
322012
|
+
}
|
|
322013
|
+
return out;
|
|
322014
|
+
}
|
|
322015
|
+
function sortDoorsFirst(links) {
|
|
322016
|
+
const tier = (l) => {
|
|
322017
|
+
if (l.inNav && l.hidden)
|
|
322018
|
+
return 0;
|
|
322019
|
+
if (l.inNav)
|
|
322020
|
+
return 1;
|
|
322021
|
+
if (!l.inFooter)
|
|
322022
|
+
return 2;
|
|
322023
|
+
return 3;
|
|
322024
|
+
};
|
|
322025
|
+
return [...links].sort((a, b) => tier(a) - tier(b));
|
|
322026
|
+
}
|
|
322027
|
+
async function callWithTimeout(client, fn, timeoutMs) {
|
|
322028
|
+
let timer;
|
|
322029
|
+
const timeout = new Promise((_, reject) => {
|
|
322030
|
+
timer = setTimeout(() => reject(new Error(`browser_evaluate timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
322031
|
+
});
|
|
322032
|
+
try {
|
|
322033
|
+
return await Promise.race([
|
|
322034
|
+
client.callTool({ name: "browser_evaluate", arguments: { function: fn } }, void 0, { timeout: timeoutMs }),
|
|
322035
|
+
timeout
|
|
322036
|
+
]);
|
|
322037
|
+
} finally {
|
|
322038
|
+
if (timer)
|
|
322039
|
+
clearTimeout(timer);
|
|
322040
|
+
}
|
|
322041
|
+
}
|
|
322042
|
+
async function harvestDomLinks(client, options = {}) {
|
|
322043
|
+
const { expandMenus = false, log: log2 } = options;
|
|
322044
|
+
const empty = { pageUrl: null, links: [], expandedTriggers: 0, navigated: false, originalUrl: null };
|
|
322045
|
+
let staticResult = empty;
|
|
322046
|
+
try {
|
|
322047
|
+
const parsed = parseEvaluateResult(await callWithTimeout(client, exports.DOM_LINK_COLLECT_FN, EVALUATE_TIMEOUT_MS));
|
|
322048
|
+
const links = normalizeLinks(parsed);
|
|
322049
|
+
const pageUrl = parsed && typeof parsed === "object" && typeof parsed.pageUrl === "string" ? parsed.pageUrl : null;
|
|
322050
|
+
staticResult = { pageUrl, links, expandedTriggers: 0, navigated: false, originalUrl: pageUrl };
|
|
322051
|
+
} catch (err) {
|
|
322052
|
+
log2?.(` [door-harvest] dom read skipped \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
322053
|
+
return empty;
|
|
322054
|
+
}
|
|
322055
|
+
if (!expandMenus) {
|
|
322056
|
+
return { ...staticResult, links: sortDoorsFirst(staticResult.links) };
|
|
322057
|
+
}
|
|
322058
|
+
if (!staticResult.pageUrl) {
|
|
322059
|
+
log2?.(" [door-harvest] skipping menu expansion \u2014 no verifiable page URL from the static pass");
|
|
322060
|
+
return { ...staticResult, links: sortDoorsFirst(staticResult.links) };
|
|
322061
|
+
}
|
|
322062
|
+
let extra = [];
|
|
322063
|
+
let expandedTriggers = 0;
|
|
322064
|
+
let expansionFailed = false;
|
|
322065
|
+
try {
|
|
322066
|
+
const parsed = parseEvaluateResult(await callWithTimeout(client, exports.MENU_EXPAND_COLLECT_FN, EVALUATE_TIMEOUT_MS + 4e3));
|
|
322067
|
+
if (parsed === null) {
|
|
322068
|
+
expansionFailed = true;
|
|
322069
|
+
} else {
|
|
322070
|
+
extra = normalizeLinks(parsed);
|
|
322071
|
+
const obj = typeof parsed === "object" ? parsed : {};
|
|
322072
|
+
expandedTriggers = typeof obj.expandedTriggers === "number" ? obj.expandedTriggers : 0;
|
|
322073
|
+
}
|
|
322074
|
+
} catch (err) {
|
|
322075
|
+
expansionFailed = true;
|
|
322076
|
+
log2?.(` [door-harvest] menu expansion skipped \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
322077
|
+
}
|
|
322078
|
+
let navigated = false;
|
|
322079
|
+
try {
|
|
322080
|
+
const probe = parseEvaluateResult(await callWithTimeout(client, exports.PAGE_URL_PROBE_FN, 5e3));
|
|
322081
|
+
if (typeof probe === "string" && probe) {
|
|
322082
|
+
navigated = probe !== staticResult.pageUrl;
|
|
322083
|
+
} else {
|
|
322084
|
+
navigated = expansionFailed;
|
|
322085
|
+
}
|
|
322086
|
+
} catch {
|
|
322087
|
+
navigated = expansionFailed;
|
|
322088
|
+
}
|
|
322089
|
+
const known = new Set(staticResult.links.map((l) => l.href));
|
|
322090
|
+
const merged = [...staticResult.links];
|
|
322091
|
+
for (const link of extra) {
|
|
322092
|
+
if (!known.has(link.href)) {
|
|
322093
|
+
known.add(link.href);
|
|
322094
|
+
merged.push(link);
|
|
322095
|
+
}
|
|
322096
|
+
}
|
|
322097
|
+
if (expandedTriggers > 0) {
|
|
322098
|
+
log2?.(` [door-harvest] expanded ${expandedTriggers} menu trigger(s), +${merged.length - staticResult.links.length} link(s)${navigated ? " \u2014 PAGE NAVIGATED, caller must restore" : ""}`);
|
|
322099
|
+
} else if (navigated) {
|
|
322100
|
+
log2?.(" [door-harvest] page moved during expansion attempt \u2014 caller must restore");
|
|
322101
|
+
}
|
|
322102
|
+
return {
|
|
322103
|
+
pageUrl: staticResult.pageUrl,
|
|
322104
|
+
links: sortDoorsFirst(merged),
|
|
322105
|
+
expandedTriggers,
|
|
322106
|
+
navigated,
|
|
322107
|
+
originalUrl: staticResult.pageUrl
|
|
322108
|
+
};
|
|
322109
|
+
}
|
|
322110
|
+
function toSeenLinks(links) {
|
|
322111
|
+
return links.map((l) => ({
|
|
322112
|
+
url: l.resolved || l.href,
|
|
322113
|
+
label: l.label,
|
|
322114
|
+
hidden: l.hidden,
|
|
322115
|
+
inNav: l.inNav,
|
|
322116
|
+
inFooter: l.inFooter
|
|
322117
|
+
}));
|
|
322118
|
+
}
|
|
322119
|
+
}
|
|
322120
|
+
});
|
|
322121
|
+
|
|
321576
322122
|
// ../runner-core/dist/services/survey-scope.js
|
|
321577
322123
|
var require_survey_scope = __commonJS({
|
|
321578
322124
|
"../runner-core/dist/services/survey-scope.js"(exports) {
|
|
@@ -321580,7 +322126,9 @@ var require_survey_scope = __commonJS({
|
|
|
321580
322126
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
321581
322127
|
exports.deriveSurveyPathScope = deriveSurveyPathScope;
|
|
321582
322128
|
exports.isAuthFlowRoute = isAuthFlowRoute;
|
|
322129
|
+
exports.hasAuthFlowSegment = hasAuthFlowSegment;
|
|
321583
322130
|
exports.buildSurveyScopeBoundary = buildSurveyScopeBoundary;
|
|
322131
|
+
exports.routeWithinSurveyBasePath = routeWithinSurveyBasePath;
|
|
321584
322132
|
exports.filterFeatureGroupsToPathScope = filterFeatureGroupsToPathScope;
|
|
321585
322133
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
321586
322134
|
function deriveSurveyPathScope(baseUrl) {
|
|
@@ -321608,22 +322156,35 @@ var require_survey_scope = __commonJS({
|
|
|
321608
322156
|
"callback",
|
|
321609
322157
|
"login",
|
|
321610
322158
|
"log-in",
|
|
322159
|
+
"log_in",
|
|
321611
322160
|
"signin",
|
|
321612
322161
|
"sign-in",
|
|
322162
|
+
"sign_in",
|
|
321613
322163
|
"signup",
|
|
321614
322164
|
"sign-up",
|
|
322165
|
+
"sign_up",
|
|
321615
322166
|
"register",
|
|
321616
322167
|
"logout",
|
|
321617
322168
|
"log-out",
|
|
322169
|
+
"log_out",
|
|
321618
322170
|
"signout",
|
|
321619
322171
|
"sign-out",
|
|
322172
|
+
"sign_out",
|
|
321620
322173
|
"sso",
|
|
321621
322174
|
"saml",
|
|
321622
322175
|
"oidc"
|
|
321623
322176
|
]);
|
|
322177
|
+
var AUTH_FLOW_PARENT_SEGMENTS = /* @__PURE__ */ new Set(["api", "auth", "users", "user", "account", "accounts", "identity", "id"]);
|
|
321624
322178
|
function isAuthFlowRoute(route2) {
|
|
321625
|
-
const
|
|
321626
|
-
|
|
322179
|
+
const segments = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).toLowerCase().split(/[/#]/).filter(Boolean);
|
|
322180
|
+
if (segments.length === 0)
|
|
322181
|
+
return false;
|
|
322182
|
+
if (AUTH_FLOW_SEGMENTS.has(segments[0]))
|
|
322183
|
+
return true;
|
|
322184
|
+
return segments.length > 1 && AUTH_FLOW_PARENT_SEGMENTS.has(segments[0]) && AUTH_FLOW_SEGMENTS.has(segments[1]);
|
|
322185
|
+
}
|
|
322186
|
+
function hasAuthFlowSegment(route2) {
|
|
322187
|
+
return (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2).toLowerCase().split(/[/#]/).filter(Boolean).some((segment) => AUTH_FLOW_SEGMENTS.has(segment));
|
|
321627
322188
|
}
|
|
321628
322189
|
function routePathForScope(route2, origin) {
|
|
321629
322190
|
const trimmed = (route2 ?? "").trim();
|
|
@@ -321647,7 +322208,7 @@ var require_survey_scope = __commonJS({
|
|
|
321647
322208
|
return trimmed;
|
|
321648
322209
|
}
|
|
321649
322210
|
}
|
|
321650
|
-
if (trimmed.startsWith("/") && trimmed.includes("..")) {
|
|
322211
|
+
if (trimmed.startsWith("/") && (trimmed.includes("..") || /%2e/i.test(trimmed))) {
|
|
321651
322212
|
try {
|
|
321652
322213
|
const resolved = new URL(trimmed, "https://scope.invalid");
|
|
321653
322214
|
return `${resolved.pathname}${resolved.search}${resolved.hash}`;
|
|
@@ -321667,20 +322228,21 @@ var require_survey_scope = __commonJS({
|
|
|
321667
322228
|
}
|
|
321668
322229
|
return lines.join("\n");
|
|
321669
322230
|
}
|
|
322231
|
+
function routeWithinSurveyBasePath(route2, scope) {
|
|
322232
|
+
const path = routePathForScope(route2, scope.origin);
|
|
322233
|
+
if (path === null)
|
|
322234
|
+
return false;
|
|
322235
|
+
if ((0, exploration_parser_js_1.routeIsWithinFocusAreas)(path, [scope.basePath]))
|
|
322236
|
+
return true;
|
|
322237
|
+
const norm = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(path);
|
|
322238
|
+
return norm === `${scope.basePath}#` || norm.startsWith(`${scope.basePath}#/`);
|
|
322239
|
+
}
|
|
321670
322240
|
function filterFeatureGroupsToPathScope(featureGroups, baseUrl) {
|
|
321671
322241
|
const scope = deriveSurveyPathScope(baseUrl);
|
|
321672
322242
|
if (!scope) {
|
|
321673
|
-
return { kept: featureGroups, droppedGroups: [], trimmedRoutes: [], trimmedPages: [], basePath: null };
|
|
322243
|
+
return { kept: featureGroups, droppedGroups: [], trimmedRoutes: [], trimmedPages: [], basePath: null, failedOpen: false };
|
|
321674
322244
|
}
|
|
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
|
-
};
|
|
322245
|
+
const withinBase = (route2) => routeWithinSurveyBasePath(route2, scope);
|
|
321684
322246
|
const allowedAuthFlow = (route2) => {
|
|
321685
322247
|
const path = routePathForScope(route2, scope.origin);
|
|
321686
322248
|
return path !== null && isAuthFlowRoute(path);
|
|
@@ -321730,7 +322292,23 @@ var require_survey_scope = __commonJS({
|
|
|
321730
322292
|
trimmedPages.push({ name: group.name, routes: removedPages });
|
|
321731
322293
|
}
|
|
321732
322294
|
}
|
|
321733
|
-
|
|
322295
|
+
if (kept.length === 0 && droppedGroups.length > 0) {
|
|
322296
|
+
const redirectSignature = featureGroups.some((group) => (Array.isArray(group.routes) ? group.routes : []).some((route2) => {
|
|
322297
|
+
const path = routePathForScope(route2, scope.origin);
|
|
322298
|
+
return path !== null && !isAuthFlowRoute(path);
|
|
322299
|
+
}));
|
|
322300
|
+
if (redirectSignature) {
|
|
322301
|
+
return {
|
|
322302
|
+
kept: featureGroups,
|
|
322303
|
+
droppedGroups: [],
|
|
322304
|
+
trimmedRoutes: [],
|
|
322305
|
+
trimmedPages: [],
|
|
322306
|
+
basePath: scope.basePath,
|
|
322307
|
+
failedOpen: true
|
|
322308
|
+
};
|
|
322309
|
+
}
|
|
322310
|
+
}
|
|
322311
|
+
return { kept, droppedGroups, trimmedRoutes, trimmedPages, basePath: scope.basePath, failedOpen: false };
|
|
321734
322312
|
}
|
|
321735
322313
|
}
|
|
321736
322314
|
});
|
|
@@ -321751,8 +322329,10 @@ var require_discover_explorer = __commonJS({
|
|
|
321751
322329
|
exports.shouldSurfaceSurveyModalTrigger = shouldSurfaceSurveyModalTrigger;
|
|
321752
322330
|
exports.normalizeAssistantToolCallArgs = normalizeAssistantToolCallArgs;
|
|
321753
322331
|
exports.getMaxIterations = getMaxIterations;
|
|
322332
|
+
exports.computeBudgetDecision = computeBudgetDecision;
|
|
321754
322333
|
exports.updatePendingRouteScreenshot = updatePendingRouteScreenshot;
|
|
321755
322334
|
exports.consumePendingRouteScreenshot = consumePendingRouteScreenshot;
|
|
322335
|
+
exports.buildSystemPrompt = buildSystemPrompt;
|
|
321756
322336
|
exports.runExplorePhase = runExplorePhase;
|
|
321757
322337
|
var crypto_1 = __require("crypto");
|
|
321758
322338
|
var ai_provider_js_1 = require_ai_provider();
|
|
@@ -321776,6 +322356,7 @@ var require_discover_explorer = __commonJS({
|
|
|
321776
322356
|
var evidence_harvester_js_1 = require_evidence_harvester();
|
|
321777
322357
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
321778
322358
|
var survey_link_frontier_js_1 = require_survey_link_frontier();
|
|
322359
|
+
var survey_door_harvester_js_1 = require_survey_door_harvester();
|
|
321779
322360
|
var survey_scope_js_1 = require_survey_scope();
|
|
321780
322361
|
var snapshot_context_compactor_js_2 = require_snapshot_context_compactor();
|
|
321781
322362
|
Object.defineProperty(exports, "formatMCPToolResult", { enumerable: true, get: function() {
|
|
@@ -321929,6 +322510,46 @@ var require_discover_explorer = __commonJS({
|
|
|
321929
322510
|
}
|
|
321930
322511
|
return readPositiveIntEnv2(env, "DISCOVERY_EXPLORE_MAX_ITERATIONS_DEFAULT", DEFAULT_MAX_EXPLORE_ITERATIONS_DEFAULT);
|
|
321931
322512
|
}
|
|
322513
|
+
var DEFAULT_ADAPTIVE_BUDGET_HARD_CAP = 200;
|
|
322514
|
+
var ADAPTIVE_BUDGET_FINALIZE_RESERVE = 5;
|
|
322515
|
+
var ADAPTIVE_BUDGET_ITERS_PER_ROUTE = 4;
|
|
322516
|
+
var ADAPTIVE_BUDGET_MIN_LEASE = 10;
|
|
322517
|
+
var ADAPTIVE_BUDGET_MAX_LEASE = 30;
|
|
322518
|
+
var ADAPTIVE_BUDGET_DISCOVERY_LEASE = 20;
|
|
322519
|
+
var ADAPTIVE_BUDGET_RESCUE_WINDOW = 10;
|
|
322520
|
+
var ADAPTIVE_BUDGET_MIN_GAP = 10;
|
|
322521
|
+
var ADAPTIVE_BUDGET_DECISION_PCT = 0.9;
|
|
322522
|
+
var ADAPTIVE_BUDGET_PROGRESS_WINDOW = 15;
|
|
322523
|
+
function computeBudgetDecision(input) {
|
|
322524
|
+
const { iteration, currentCap, hardCap, remainingRoutes, capturing, freshDiscovery, discoveryLeaseUsed, rescueUsed, allowExtension } = input;
|
|
322525
|
+
if (remainingRoutes <= 0)
|
|
322526
|
+
return { action: "finish", reason: "drained" };
|
|
322527
|
+
if (iteration >= hardCap)
|
|
322528
|
+
return { action: "finish", reason: "hard-cap" };
|
|
322529
|
+
const tail = Math.max(0, currentCap - iteration);
|
|
322530
|
+
const needed = ADAPTIVE_BUDGET_FINALIZE_RESERVE + ADAPTIVE_BUDGET_ITERS_PER_ROUTE * remainingRoutes;
|
|
322531
|
+
if (allowExtension && capturing) {
|
|
322532
|
+
if (currentCap >= hardCap)
|
|
322533
|
+
return { action: "none" };
|
|
322534
|
+
const shortfall = needed - tail;
|
|
322535
|
+
if (shortfall <= 0)
|
|
322536
|
+
return { action: "none" };
|
|
322537
|
+
const clamped = Math.min(Math.max(shortfall, ADAPTIVE_BUDGET_MIN_LEASE), ADAPTIVE_BUDGET_MAX_LEASE);
|
|
322538
|
+
const delta = Math.min(hardCap - currentCap, clamped);
|
|
322539
|
+
return { action: "extend", delta, newCap: currentCap + delta, lease: "capture" };
|
|
322540
|
+
}
|
|
322541
|
+
if (allowExtension && freshDiscovery && !discoveryLeaseUsed) {
|
|
322542
|
+
if (currentCap >= hardCap)
|
|
322543
|
+
return { action: "none" };
|
|
322544
|
+
if (tail >= needed)
|
|
322545
|
+
return { action: "none" };
|
|
322546
|
+
const delta = Math.min(hardCap - currentCap, ADAPTIVE_BUDGET_DISCOVERY_LEASE);
|
|
322547
|
+
return { action: "extend", delta, newCap: currentCap + delta, lease: "discovery" };
|
|
322548
|
+
}
|
|
322549
|
+
if (!rescueUsed)
|
|
322550
|
+
return { action: "rescue" };
|
|
322551
|
+
return { action: "finish", reason: "stalled" };
|
|
322552
|
+
}
|
|
321932
322553
|
function updatePendingRouteScreenshot(pending, { imageData, detectedRoute, fallbackRoute }) {
|
|
321933
322554
|
if (imageData) {
|
|
321934
322555
|
return {
|
|
@@ -322125,8 +322746,9 @@ NEVER assert: user-specific text (names, emails), counts/numbers, timestamps, ma
|
|
|
322125
322746
|
};
|
|
322126
322747
|
function buildSystemPrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, options) {
|
|
322127
322748
|
const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
322128
|
-
const { secrets: explorerSecretVars, publics:
|
|
322749
|
+
const { secrets: explorerSecretVars, publics: explorerPublicVarsRaw } = (0, credential_tools_js_1.partitionTestVariables)(browserCredentials);
|
|
322129
322750
|
const hasCredentials = Object.keys(explorerSecretVars).length > 0;
|
|
322751
|
+
const { DISCOVERY_SHOULD_REGISTER: _omitRegisterFlag, ...explorerPublicVars } = explorerPublicVarsRaw;
|
|
322130
322752
|
const explorerPublicLines = (0, credential_tools_js_1.renderPublicVariablesBlock)(explorerPublicVars);
|
|
322131
322753
|
const credentialBlock = `${explorerPublicLines ? `
|
|
322132
322754
|
## Test Variables (public)
|
|
@@ -322136,6 +322758,7 @@ ${explorerPublicLines}
|
|
|
322136
322758
|
const inboxBlock = options?.inboxPrompt ? `
|
|
322137
322759
|
${options.inboxPrompt}` : "";
|
|
322138
322760
|
const isRegisterMode = !!options?.inboxPrompt;
|
|
322761
|
+
const emailVerificationUnavailable = !isRegisterMode && !!options?.emailVerificationUnavailable;
|
|
322139
322762
|
const focusAreas = options?.focusAreas;
|
|
322140
322763
|
let alreadyDiscoveredPages = "";
|
|
322141
322764
|
if (existingSiteMap && existingSiteMap.pages.length > 0) {
|
|
@@ -322157,21 +322780,27 @@ ${options.inboxPrompt}` : "";
|
|
|
322157
322780
|
const isIncremental = !!(options?.incremental && mode === "survey");
|
|
322158
322781
|
const MAX_EXPLORE_ITERATIONS = getMaxIterations(mode, options?.incremental);
|
|
322159
322782
|
const isSurvey = mode === "survey";
|
|
322783
|
+
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)();
|
|
322784
|
+
const surveyPathScope = isSurvey ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
|
|
322160
322785
|
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.
|
|
322786
|
+
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
322787
|
|
|
322163
322788
|
## Strategy
|
|
322164
322789
|
1. ${isRegisterMode ? `Navigate to the base URL. BEFORE registering, explore the unauthenticated experience first:
|
|
322165
322790
|
- 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
322791
|
- For each: browser_snapshot \u2192 capture_page \u2192 record_transition
|
|
322167
322792
|
- 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` :
|
|
322793
|
+
- 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:
|
|
322794
|
+
- 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
|
|
322795
|
+
- 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
|
|
322796
|
+
- If you end up signed in WITHOUT an email round-trip, continue mapping the authenticated app normally
|
|
322797
|
+
- 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
322798
|
- 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
322799
|
- For each: browser_snapshot \u2192 capture_page \u2192 record_transition
|
|
322171
322800
|
- Do NOT submit registration or password-reset forms - only catalog the page structure (survey rule: map, do not mutate)
|
|
322172
322801
|
- 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
322802
|
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
|
|
322803
|
+
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
322804
|
- **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
322805
|
4. Visit each navigation target systematically. On each page:
|
|
322177
322806
|
- \`browser_snapshot\` to see the page
|
|
@@ -322182,8 +322811,8 @@ ${options.inboxPrompt}` : "";
|
|
|
322182
322811
|
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
322812
|
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
322813
|
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**:
|
|
322186
|
-
9. After visiting ALL reachable pages, call \`finish_exploration\`
|
|
322814
|
+
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)
|
|
322815
|
+
9. After visiting ALL reachable ${surveyPathScope ? "in-scope " : ""}pages, call \`finish_exploration\`
|
|
322187
322816
|
You do NOT have a \`record_journey\` tool - your job is mapping, not testing. Downstream agents will create tests from your site map.
|
|
322188
322817
|
|
|
322189
322818
|
## What to catalog (capture_page)
|
|
@@ -322208,7 +322837,7 @@ ${(0, survey_scope_js_1.buildSurveyScopeBoundary)(baseUrl)}
|
|
|
322208
322837
|
- **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
322838
|
- **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
322839
|
- **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
|
|
322840
|
+
- ${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
322841
|
|
|
322213
322842
|
${appBrief ? `## Application Brief
|
|
322214
322843
|
The user provided this description of their application:
|
|
@@ -322230,7 +322859,7 @@ If the user mentioned a specific page that changed (e.g. "Settings was redesigne
|
|
|
322230
322859
|
- Check for Login, Register/Sign Up, Forgot Password - including behind menus, drawers, and modal auth switches
|
|
322231
322860
|
- For each NEW auth page: browser_snapshot \u2192 capture_page \u2192 record_transition
|
|
322232
322861
|
- 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:
|
|
322862
|
+
- 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
322863
|
- Check for Login, Register/Sign Up, Forgot Password - including behind menus, drawers, and modal auth switches
|
|
322235
322864
|
- For each NEW auth page: browser_snapshot \u2192 capture_page \u2192 record_transition
|
|
322236
322865
|
- Do NOT submit registration or password-reset forms (survey rule: map, do not mutate)
|
|
@@ -322248,7 +322877,9 @@ ${surfaceIntelligence ? "## Prior Recordings\n" + surfaceIntelligence + "\n" : "
|
|
|
322248
322877
|
|
|
322249
322878
|
## App Details
|
|
322250
322879
|
- Base URL: ${baseUrl}
|
|
322251
|
-
${credentialBlock}${redactedBlock}${inboxBlock}
|
|
322880
|
+
${credentialBlock}${redactedBlock}${inboxBlock}${emailVerificationUnavailable ? `
|
|
322881
|
+
|
|
322882
|
+
${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
322252
322883
|
}
|
|
322253
322884
|
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
322885
|
|
|
@@ -322275,7 +322906,7 @@ Every feature you discover should be USED, not just cataloged. The test generato
|
|
|
322275
322906
|
|
|
322276
322907
|
## Strategy
|
|
322277
322908
|
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." : ""}
|
|
322909
|
+
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
322910
|
3. On each page: take a \`browser_snapshot\`, identify what features/actions are available
|
|
322280
322911
|
4. **USE the features on the page:**
|
|
322281
322912
|
- Forms: fill every field with realistic data, submit, observe the result (success page, validation errors, redirect, toast messages)
|
|
@@ -322293,7 +322924,11 @@ Every feature you discover should be USED, not just cataloged. The test generato
|
|
|
322293
322924
|
- Do NOT split validation, invalid input, and recovery into separate tests - combine them into ONE comprehensive test
|
|
322294
322925
|
c) For multi-section pages (profile with tabs, settings with categories): test each DISTINCT section as a separate happy path test
|
|
322295
322926
|
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) -
|
|
322927
|
+
${emailVerificationUnavailable ? ` **Auth pages (login/register) - registration requested but this runner CANNOT receive email:**
|
|
322928
|
+
- 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
|
|
322929
|
+
- 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
|
|
322930
|
+
- 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
|
|
322931
|
+
- 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
322932
|
- You arrive at the login/register page NOT logged in - use this state to test errors FIRST
|
|
322298
322933
|
- Step 1: Submit with empty fields \u2192 observe validation errors
|
|
322299
322934
|
- Step 2: Submit with invalid credentials (wrong password, bad email) \u2192 observe error messages
|
|
@@ -322301,7 +322936,7 @@ Every feature you discover should be USED, not just cataloged. The test generato
|
|
|
322301
322936
|
- Step 4: NOW do the happy path - log in with valid credentials \u2192 observe redirect to dashboard
|
|
322302
322937
|
- Step 5: Call \`record_journey\` for the happy path
|
|
322303
322938
|
- 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
|
|
322939
|
+
- **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
322940
|
**Logging out (when needed for other reasons):**
|
|
322306
322941
|
- 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
322942
|
- 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 +322987,38 @@ Every feature you discover should be USED, not just cataloged. The test generato
|
|
|
322352
322987
|
|
|
322353
322988
|
GOOD (specific, with routes, stable assertions):
|
|
322354
322989
|
\`\`\`
|
|
322355
|
-
record_journey({
|
|
322990
|
+
${emailVerificationUnavailable ? `record_journey({
|
|
322991
|
+
name: "Contact Form - Validation",
|
|
322992
|
+
route: "/contact",
|
|
322993
|
+
routes: ["/", "/contact"],
|
|
322994
|
+
steps: ["Navigate to /", "Click 'Contact' link", "Submit the empty form", "Observe validation errors"],
|
|
322995
|
+
expectedOutcome: "URL matches /contact. Validation error 'Email is required' is visible.",
|
|
322996
|
+
assertions: [{ type: "url", value: "/contact" }, { type: "error-text", value: "Email is required" }]
|
|
322997
|
+
})` : `record_journey({
|
|
322356
322998
|
name: "Login - Happy Path",
|
|
322357
322999
|
route: "/auth/login",
|
|
322358
323000
|
routes: ["/", "/auth/login", "/dashboard"],
|
|
322359
323001
|
steps: ["Navigate to /", "Click 'Sign In' link", "Fill email and password with test credentials", "Click 'Log In' button", "Observe redirect to /dashboard"],
|
|
322360
323002
|
expectedOutcome: "URL matches /dashboard. Sidebar link 'Overview' is visible.",
|
|
322361
323003
|
assertions: [{ type: "url", value: "/dashboard" }, { type: "nav-link", locator: "getByRole('link', { name: 'Overview' })" }]
|
|
322362
|
-
})
|
|
323004
|
+
})`}
|
|
322363
323005
|
\`\`\`
|
|
322364
323006
|
|
|
322365
323007
|
BAD (vague, missing routes, marketing assertions):
|
|
322366
323008
|
\`\`\`
|
|
322367
|
-
record_journey({
|
|
323009
|
+
${emailVerificationUnavailable ? `record_journey({
|
|
323010
|
+
name: "Contact test",
|
|
323011
|
+
route: "/contact",
|
|
323012
|
+
routes: ["/contact"],
|
|
323013
|
+
steps: ["Try the contact form"],
|
|
323014
|
+
expectedOutcome: "User sees 'The Best Platform Ever' heading"
|
|
323015
|
+
})` : `record_journey({
|
|
322368
323016
|
name: "Login test",
|
|
322369
323017
|
route: "/auth/login",
|
|
322370
323018
|
routes: ["/auth/login"],
|
|
322371
323019
|
steps: ["Login to the app"],
|
|
322372
323020
|
expectedOutcome: "User sees dashboard with 'Welcome to the Best Platform Ever' heading"
|
|
322373
|
-
})
|
|
323021
|
+
})`}
|
|
322374
323022
|
\`\`\`
|
|
322375
323023
|
|
|
322376
323024
|
## Bug Detection - CRITICAL
|
|
@@ -322415,7 +323063,7 @@ Exception: if you need to navigate THROUGH other pages to reach a focus area (e.
|
|
|
322415
323063
|
2. Take a \`browser_snapshot\` to understand what's on the page
|
|
322416
323064
|
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
323065
|
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
|
|
323066
|
+
- **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
323067
|
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
323068
|
6. After each interaction, call \`capture_page\` with detailed observations
|
|
322421
323069
|
7. **ONLY after completing steps 1-6 for ALL focus areas**, call \`finish_exploration\`
|
|
@@ -322517,7 +323165,9 @@ ${surfaceIntelligence}
|
|
|
322517
323165
|
|
|
322518
323166
|
## App Details
|
|
322519
323167
|
- Base URL: ${baseUrl}
|
|
322520
|
-
${credentialBlock}${redactedBlock}${inboxBlock}
|
|
323168
|
+
${credentialBlock}${redactedBlock}${inboxBlock}${emailVerificationUnavailable ? `
|
|
323169
|
+
|
|
323170
|
+
${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
322521
323171
|
}
|
|
322522
323172
|
async function runExplorePhase(mcpClient, baseUrl, variableContext, existingSiteMap, projectId, existingTestNames, options, config) {
|
|
322523
323173
|
const logs2 = [];
|
|
@@ -322561,6 +323211,7 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
|
|
|
322561
323211
|
const perceptionEnricherEnabled = ((0, shared_1.parseBoolFlag)(process.env.DISCOVERY_PERCEPTION_ENRICHER) || process.env.DISCOVERY_PERCEPTION_ENRICHER === "1") && typeof options?.getPerceptionPage === "function";
|
|
322562
323212
|
const linkFrontierEnabled = (0, survey_link_frontier_js_1.isSurveyLinkFrontierEnabled)() && options?.mode === "survey";
|
|
322563
323213
|
const linkScope = (0, survey_link_frontier_js_1.deriveLinkScope)(baseUrl);
|
|
323214
|
+
const surveyNavScope = options?.mode === "survey" ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
|
|
322564
323215
|
let finishRedirects = 0;
|
|
322565
323216
|
let lastLandedPageUrl = null;
|
|
322566
323217
|
const state2 = (0, exploration_state_js_1.createExplorationState)();
|
|
@@ -322589,6 +323240,50 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
|
|
|
322589
323240
|
logs2.push(` [link-frontier] ingestion skipped - ${err instanceof Error ? err.message : String(err)}`);
|
|
322590
323241
|
}
|
|
322591
323242
|
}
|
|
323243
|
+
const domHarvestEnabled = linkFrontierEnabled && (0, survey_door_harvester_js_1.isDomHarvestEnabled)();
|
|
323244
|
+
const menuExpandEnabled = domHarvestEnabled && (0, survey_door_harvester_js_1.isMenuExpandEnabled)();
|
|
323245
|
+
const menuExpandAttemptedRoutes = /* @__PURE__ */ new Set();
|
|
323246
|
+
async function harvestDomLinksIntoFrontier() {
|
|
323247
|
+
if (!domHarvestEnabled)
|
|
323248
|
+
return;
|
|
323249
|
+
const currentScoped = (0, exploration_parser_js_1.normalizeRouteForFocusScope)(state2.currentRoute);
|
|
323250
|
+
const currentRs = state2.routeState.get(state2.currentRoute);
|
|
323251
|
+
try {
|
|
323252
|
+
const expandHere = menuExpandEnabled && !menuExpandAttemptedRoutes.has(currentScoped);
|
|
323253
|
+
if (expandHere)
|
|
323254
|
+
menuExpandAttemptedRoutes.add(currentScoped);
|
|
323255
|
+
const harvest = await (0, survey_door_harvester_js_1.harvestDomLinks)(mcpClient, {
|
|
323256
|
+
expandMenus: expandHere,
|
|
323257
|
+
log: (m) => logs2.push(m)
|
|
323258
|
+
});
|
|
323259
|
+
if (harvest.pageUrl)
|
|
323260
|
+
lastLandedPageUrl = harvest.pageUrl;
|
|
323261
|
+
if (harvest.links.length > 0) {
|
|
323262
|
+
(0, survey_link_frontier_js_1.ingestSeenLinks)(state2, {
|
|
323263
|
+
links: (0, survey_door_harvester_js_1.toSeenLinks)(harvest.links),
|
|
323264
|
+
scope: linkScope,
|
|
323265
|
+
isEntryOrHub: currentScoped === linkScope.basePath || state2.iteration <= 2,
|
|
323266
|
+
isErrorContext: currentRs?.pageType === "ERROR",
|
|
323267
|
+
pageUrl: harvest.pageUrl ?? lastLandedPageUrl,
|
|
323268
|
+
provenance: "dom",
|
|
323269
|
+
log: (m) => logs2.push(m)
|
|
323270
|
+
});
|
|
323271
|
+
}
|
|
323272
|
+
if (harvest.navigated && harvest.originalUrl) {
|
|
323273
|
+
logs2.push(` [door-harvest] page moved during menu expansion \u2014 restoring ${harvest.originalUrl}`);
|
|
323274
|
+
try {
|
|
323275
|
+
const navResult = await mcpClient.callTool({ name: "browser_navigate", arguments: { url: harvest.originalUrl } });
|
|
323276
|
+
if (navResult && typeof navResult === "object" && navResult.isError === true) {
|
|
323277
|
+
logs2.push(" [door-harvest] restore navigate returned an error \u2014 model may be on a different page");
|
|
323278
|
+
}
|
|
323279
|
+
} catch (navErr) {
|
|
323280
|
+
logs2.push(` [door-harvest] restore failed - ${navErr instanceof Error ? navErr.message : String(navErr)}`);
|
|
323281
|
+
}
|
|
323282
|
+
}
|
|
323283
|
+
} catch (err) {
|
|
323284
|
+
logs2.push(` [door-harvest] skipped - ${err instanceof Error ? err.message : String(err)}`);
|
|
323285
|
+
}
|
|
323286
|
+
}
|
|
322592
323287
|
const recentExchanges = [];
|
|
322593
323288
|
const transientDirectives = [];
|
|
322594
323289
|
const MAX_RECENT_EXCHANGES = 28;
|
|
@@ -322617,7 +323312,8 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
|
|
|
322617
323312
|
let inboxState = null;
|
|
322618
323313
|
let inboxReader = null;
|
|
322619
323314
|
const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
|
|
322620
|
-
|
|
323315
|
+
const registerIntent = (0, inbox_tools_js_1.hasRegisterIntent)(variableContext);
|
|
323316
|
+
if (inboxConfig && registerIntent) {
|
|
322621
323317
|
inboxState = (0, inbox_tools_js_1.createInboxRunState)(inboxConfig, {
|
|
322622
323318
|
tagSeed: baseUrl,
|
|
322623
323319
|
projectScope: projectId,
|
|
@@ -322635,8 +323331,19 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
|
|
|
322635
323331
|
log: (line) => logs2.push(line)
|
|
322636
323332
|
});
|
|
322637
323333
|
logs2.push(`Verification inbox ENABLED for registration \u2014 email ${inboxState.email}`);
|
|
322638
|
-
} else
|
|
322639
|
-
|
|
323334
|
+
} else {
|
|
323335
|
+
if (registerIntent && !inboxConfig) {
|
|
323336
|
+
const gap = (0, verification_inbox_js_1.describeVerificationInboxConfigGap)() ?? "verification inbox unavailable";
|
|
323337
|
+
logs2.push(`Register intent set but the ${gap} on this runner \u2014 keeping the seeded register identity for a one-shot registration attempt.`);
|
|
323338
|
+
} else if ((0, inbox_tools_js_1.clearThrowawayRegisterSeed)(variableContext)) {
|
|
323339
|
+
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.");
|
|
323340
|
+
}
|
|
323341
|
+
}
|
|
323342
|
+
const emailVerificationUnavailable = registerIntent && !inboxConfig && !(0, credential_tools_js_1.hasUsableLoginIdentity)(variableContext.allTestVariables ?? variableContext.publicTestVariables, {
|
|
323343
|
+
isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail
|
|
323344
|
+
});
|
|
323345
|
+
if (emailVerificationUnavailable) {
|
|
323346
|
+
logs2.push("Email verification unavailable \u2014 the agent will ATTEMPT registration once; any flow requiring an emailed code/link will be reported BLOCKED.");
|
|
322640
323347
|
}
|
|
322641
323348
|
const inboxPromptBlock = inboxState ? (0, inbox_tools_js_1.buildInboxPromptBlock)({
|
|
322642
323349
|
email: inboxState.email,
|
|
@@ -322674,7 +323381,8 @@ ${credentialBlock}${redactedBlock}${inboxBlock}`;
|
|
|
322674
323381
|
let systemPrompt = config?.systemPrompt ?? buildSystemPrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, {
|
|
322675
323382
|
...options,
|
|
322676
323383
|
canRotateCredentials: !!options?.onCredentialUpdate,
|
|
322677
|
-
inboxPrompt: inboxPromptBlock
|
|
323384
|
+
inboxPrompt: inboxPromptBlock,
|
|
323385
|
+
emailVerificationUnavailable
|
|
322678
323386
|
});
|
|
322679
323387
|
if (config?.systemPrompt && inboxPromptBlock) {
|
|
322680
323388
|
systemPrompt += `
|
|
@@ -322685,7 +323393,14 @@ ${inboxPromptBlock}`;
|
|
|
322685
323393
|
const hasLoginCredentials = Object.keys(browserSecretsForHint).length > 0;
|
|
322686
323394
|
const aiClient = options?.modelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getRunnerAIClient)();
|
|
322687
323395
|
const isDeepMode = options?.mode === "deep";
|
|
322688
|
-
const
|
|
323396
|
+
const baseMaxIterations = config?.maxIterationsOverride && config.maxIterationsOverride > 0 ? config.maxIterationsOverride : getMaxIterations(options?.mode ?? "full", options?.incremental);
|
|
323397
|
+
const adaptiveBudgetEnabled = process.env.DISCOVERY_ADAPTIVE_BUDGET === "1" || (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_ADAPTIVE_BUDGET);
|
|
323398
|
+
const adaptiveActive = adaptiveBudgetEnabled && isSurveyMode && linkFrontierEnabled && !options?.incremental && !(config?.maxIterationsOverride && config.maxIterationsOverride > 0);
|
|
323399
|
+
const adaptiveHardCap = adaptiveActive ? Math.max(baseMaxIterations, readPositiveIntEnv2(process.env, "DISCOVERY_ADAPTIVE_BUDGET_HARD_CAP", DEFAULT_ADAPTIVE_BUDGET_HARD_CAP)) : baseMaxIterations;
|
|
323400
|
+
let maxIterations = baseMaxIterations;
|
|
323401
|
+
if (adaptiveActive) {
|
|
323402
|
+
logs2.push(`Adaptive budget: ON (floor ${baseMaxIterations}, hard cap ${adaptiveHardCap})`);
|
|
323403
|
+
}
|
|
322689
323404
|
const configKeepTail = isDeepMode ? Math.ceil(ctxConfig.keepTail * 1.5) : ctxConfig.keepTail;
|
|
322690
323405
|
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
323406
|
logs2.push(`Discovery mode: ${useV3 ? "V3 (per-turn rebuild)" : "V2.5 (full transcript + control brief)"}${v3Env ? ` [override: V3=${v3Env}]` : " [auto]"}`);
|
|
@@ -322694,7 +323409,8 @@ ${inboxPromptBlock}`;
|
|
|
322694
323409
|
const inboxPasswordKey = inboxState?.passwordKey ?? "TEST_USER_PASSWORD";
|
|
322695
323410
|
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
323411
|
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
|
|
323412
|
+
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).';
|
|
323413
|
+
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
323414
|
let messages = [
|
|
322699
323415
|
{ role: "system", content: systemPrompt },
|
|
322700
323416
|
{ role: "user", content: userMessage }
|
|
@@ -322703,6 +323419,7 @@ ${inboxPromptBlock}`;
|
|
|
322703
323419
|
let lastVisitedSize = 0;
|
|
322704
323420
|
let lastSurveyProgressMark = 0;
|
|
322705
323421
|
let staleIterations = 0;
|
|
323422
|
+
let staleKnownKeysMark = 0;
|
|
322706
323423
|
let lastCaptureIteration = 0;
|
|
322707
323424
|
let lastJourneyProgress = 0;
|
|
322708
323425
|
let lastDeepProgressMark = 0;
|
|
@@ -322715,9 +323432,151 @@ ${inboxPromptBlock}`;
|
|
|
322715
323432
|
const BRIEF_INTERVAL = 5;
|
|
322716
323433
|
const stuckDetectorEnabled = process.env.DISCOVERY_STUCK_DETECTOR === "1" || (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_STUCK_DETECTOR);
|
|
322717
323434
|
const stuckDetector = stuckDetectorEnabled ? new stuck_detector_js_1.StuckDetector({ repetitionReachesRecovery: !isDeepMode }) : null;
|
|
322718
|
-
|
|
323435
|
+
let adaptiveDecisionArmedAt = Math.ceil(maxIterations * ADAPTIVE_BUDGET_DECISION_PCT);
|
|
323436
|
+
let adaptiveRescueUsed = false;
|
|
323437
|
+
let adaptiveComplete = false;
|
|
323438
|
+
let adaptivePendingDiscoveryRoutes = [];
|
|
323439
|
+
let adaptiveLastNewRouteIteration = Number.NEGATIVE_INFINITY;
|
|
323440
|
+
let adaptivePrevVisitedSize = 0;
|
|
323441
|
+
let captureRevision = 0;
|
|
323442
|
+
let captureCreditedThrough = 0;
|
|
323443
|
+
const adaptiveKnownKeys = /* @__PURE__ */ new Set();
|
|
323444
|
+
let discoveryRevision = 0;
|
|
323445
|
+
let discoveryCreditedThrough = 0;
|
|
323446
|
+
let adaptiveLastDiscoveryIteration = Number.NEGATIVE_INFINITY;
|
|
323447
|
+
let adaptiveDiscoveryLeaseUsed = false;
|
|
323448
|
+
const adaptiveDebtRoutes = () => {
|
|
323449
|
+
const isAttemptedLinkEntry = (r) => {
|
|
323450
|
+
const item = state2.frontier.get(r);
|
|
323451
|
+
return item?.source === "link-seen" && item.attempted === true;
|
|
323452
|
+
};
|
|
323453
|
+
const ranked = (0, exploration_state_js_1.getRankedFrontier)(state2, { preferShallow: true }).filter((r) => r !== "/" && !isAttemptedLinkEntry(r));
|
|
323454
|
+
const rankedSet = new Set(ranked);
|
|
323455
|
+
const navigatedUncaptured = [];
|
|
323456
|
+
for (const r of state2.frontier.keys()) {
|
|
323457
|
+
if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r))
|
|
323458
|
+
navigatedUncaptured.push(r);
|
|
323459
|
+
}
|
|
323460
|
+
return ranked.concat(navigatedUncaptured);
|
|
323461
|
+
};
|
|
323462
|
+
const describeRankedRoutes = (routes, max) => routes.slice(0, max).map((r) => {
|
|
323463
|
+
const label = state2.frontier.get(r)?.label;
|
|
323464
|
+
return label ? `${label} (${r})` : r;
|
|
323465
|
+
}).join(", ");
|
|
323466
|
+
const refreshAdaptiveDiscovery = () => {
|
|
323467
|
+
if (state2.visitedRoutes.size > adaptivePrevVisitedSize) {
|
|
323468
|
+
adaptivePrevVisitedSize = state2.visitedRoutes.size;
|
|
323469
|
+
adaptiveLastNewRouteIteration = iteration;
|
|
323470
|
+
captureRevision++;
|
|
323471
|
+
}
|
|
323472
|
+
const debt = adaptiveDebtRoutes();
|
|
323473
|
+
let grew = false;
|
|
323474
|
+
for (const route2 of debt) {
|
|
323475
|
+
const key = state2.frontier.get(route2)?.familyKey ?? (0, exploration_parser_js_1.normalizeRouteForFocusScope)(route2);
|
|
323476
|
+
if (!adaptiveKnownKeys.has(key)) {
|
|
323477
|
+
adaptiveKnownKeys.add(key);
|
|
323478
|
+
adaptivePendingDiscoveryRoutes.push(route2);
|
|
323479
|
+
grew = true;
|
|
323480
|
+
}
|
|
323481
|
+
}
|
|
323482
|
+
if (grew) {
|
|
323483
|
+
discoveryRevision++;
|
|
323484
|
+
adaptiveLastDiscoveryIteration = iteration;
|
|
323485
|
+
}
|
|
323486
|
+
if (!adaptiveComplete && adaptiveDecisionArmedAt === Number.POSITIVE_INFINITY && debt.length > 0) {
|
|
323487
|
+
adaptiveDecisionArmedAt = iteration;
|
|
323488
|
+
}
|
|
323489
|
+
};
|
|
323490
|
+
const runBudgetController = (opts = {}) => {
|
|
323491
|
+
const allowExtension = opts.allowExtension ?? true;
|
|
323492
|
+
const remainingList = adaptiveDebtRoutes();
|
|
323493
|
+
const capturing = captureRevision > captureCreditedThrough && iteration - adaptiveLastNewRouteIteration <= ADAPTIVE_BUDGET_PROGRESS_WINDOW;
|
|
323494
|
+
const freshDiscovery = discoveryRevision > discoveryCreditedThrough && iteration - adaptiveLastDiscoveryIteration <= ADAPTIVE_BUDGET_PROGRESS_WINDOW;
|
|
323495
|
+
const decision = computeBudgetDecision({
|
|
323496
|
+
iteration,
|
|
323497
|
+
currentCap: maxIterations,
|
|
323498
|
+
hardCap: adaptiveHardCap,
|
|
323499
|
+
remainingRoutes: remainingList.length,
|
|
323500
|
+
capturing,
|
|
323501
|
+
freshDiscovery,
|
|
323502
|
+
discoveryLeaseUsed: adaptiveDiscoveryLeaseUsed,
|
|
323503
|
+
rescueUsed: adaptiveRescueUsed,
|
|
323504
|
+
allowExtension
|
|
323505
|
+
});
|
|
323506
|
+
if (decision.action === "extend") {
|
|
323507
|
+
maxIterations = decision.newCap;
|
|
323508
|
+
adaptiveRescueUsed = false;
|
|
323509
|
+
discoveryCreditedThrough = discoveryRevision;
|
|
323510
|
+
adaptiveDecisionArmedAt = Math.min(maxIterations, Math.max(iteration + ADAPTIVE_BUDGET_MIN_GAP, Math.ceil(maxIterations * ADAPTIVE_BUDGET_DECISION_PCT)));
|
|
323511
|
+
const remainingSet = new Set(remainingList);
|
|
323512
|
+
const freshFirst = [.../* @__PURE__ */ new Set([
|
|
323513
|
+
...adaptivePendingDiscoveryRoutes.filter((r) => remainingSet.has(r)),
|
|
323514
|
+
...remainingList
|
|
323515
|
+
])];
|
|
323516
|
+
adaptivePendingDiscoveryRoutes = [];
|
|
323517
|
+
if (decision.lease === "discovery") {
|
|
323518
|
+
adaptiveDiscoveryLeaseUsed = true;
|
|
323519
|
+
logs2.push(` [adaptive-budget] discovery lease +${decision.delta} \u2192 ${maxIterations}; ${remainingList.length} newly-found route(s)`);
|
|
323520
|
+
return {
|
|
323521
|
+
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.`,
|
|
323522
|
+
keepGoing: true
|
|
323523
|
+
};
|
|
323524
|
+
}
|
|
323525
|
+
captureCreditedThrough = captureRevision;
|
|
323526
|
+
logs2.push(` [adaptive-budget] capture lease +${decision.delta} \u2192 ${maxIterations}; ${remainingList.length} route(s) still un-captured`);
|
|
323527
|
+
return {
|
|
323528
|
+
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.`,
|
|
323529
|
+
keepGoing: true
|
|
323530
|
+
};
|
|
323531
|
+
}
|
|
323532
|
+
if (decision.action === "rescue") {
|
|
323533
|
+
adaptiveRescueUsed = true;
|
|
323534
|
+
const runwayTarget = Math.min(adaptiveHardCap, iteration + ADAPTIVE_BUDGET_RESCUE_WINDOW);
|
|
323535
|
+
if (runwayTarget > maxIterations)
|
|
323536
|
+
maxIterations = runwayTarget;
|
|
323537
|
+
adaptiveDecisionArmedAt = Math.min(maxIterations, iteration + ADAPTIVE_BUDGET_RESCUE_WINDOW);
|
|
323538
|
+
logs2.push(` [adaptive-budget] stall rescue \u2192 steering to ${remainingList.length} un-captured route(s) (cap ${maxIterations})`);
|
|
323539
|
+
return {
|
|
323540
|
+
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.`,
|
|
323541
|
+
keepGoing: true
|
|
323542
|
+
};
|
|
323543
|
+
}
|
|
323544
|
+
if (decision.action === "finish") {
|
|
323545
|
+
if (decision.reason === "drained") {
|
|
323546
|
+
adaptiveDecisionArmedAt = Number.POSITIVE_INFINITY;
|
|
323547
|
+
logs2.push(" [adaptive-budget] frontier drained \u2014 quiescent (will re-arm on new discovery)");
|
|
323548
|
+
} else {
|
|
323549
|
+
adaptiveComplete = true;
|
|
323550
|
+
adaptiveDecisionArmedAt = Number.POSITIVE_INFINITY;
|
|
323551
|
+
logs2.push(` [adaptive-budget] finishing (${decision.reason}); ${remainingList.length} route(s) left un-captured`);
|
|
323552
|
+
}
|
|
323553
|
+
return { directive: null, keepGoing: false };
|
|
323554
|
+
}
|
|
323555
|
+
adaptiveDecisionArmedAt = Math.min(iteration + 5, maxIterations);
|
|
323556
|
+
return { directive: null, keepGoing: true };
|
|
323557
|
+
};
|
|
323558
|
+
const tryAdaptiveBoundaryExtend = () => {
|
|
323559
|
+
if (!adaptiveActive || adaptiveComplete)
|
|
323560
|
+
return false;
|
|
323561
|
+
refreshAdaptiveDiscovery();
|
|
323562
|
+
const { directive, keepGoing } = runBudgetController();
|
|
323563
|
+
if (keepGoing && maxIterations > iteration) {
|
|
323564
|
+
if (directive) {
|
|
323565
|
+
if (useV3) {
|
|
323566
|
+
transientDirectives.push(directive);
|
|
323567
|
+
} else {
|
|
323568
|
+
messages.push({ role: "user", content: directive });
|
|
323569
|
+
}
|
|
323570
|
+
}
|
|
323571
|
+
return true;
|
|
323572
|
+
}
|
|
323573
|
+
return false;
|
|
323574
|
+
};
|
|
323575
|
+
while (!explorationComplete && (iteration < maxIterations || tryAdaptiveBoundaryExtend())) {
|
|
322719
323576
|
iteration++;
|
|
322720
323577
|
(0, exploration_state_js_1.reduceEvent)(state2, { type: "ITERATION_STARTED", iteration });
|
|
323578
|
+
if (adaptiveActive)
|
|
323579
|
+
refreshAdaptiveDiscovery();
|
|
322721
323580
|
const iterStartedAt = Date.now();
|
|
322722
323581
|
logs2.push(`
|
|
322723
323582
|
-- Explore iteration ${iteration}/${maxIterations} --`);
|
|
@@ -322761,7 +323620,7 @@ ${inboxPromptBlock}`;
|
|
|
322761
323620
|
logs2.push(` Deep mode escalation: forced action after ${deepStaleChecks * 5} stale iterations`);
|
|
322762
323621
|
} else if (deepStaleChecks >= 3 && !deepStaleNudgeSent) {
|
|
322763
323622
|
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.`;
|
|
323623
|
+
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
323624
|
if (useV3) {
|
|
322766
323625
|
transientDirectives.push(nudgeMsg);
|
|
322767
323626
|
} else {
|
|
@@ -322795,14 +323654,39 @@ ${inboxPromptBlock}`;
|
|
|
322795
323654
|
}
|
|
322796
323655
|
logs2.push(` Snapshot-loop nudge: ${itersSinceCapture} iterations without capture_page`);
|
|
322797
323656
|
}
|
|
322798
|
-
if (
|
|
323657
|
+
if (adaptiveActive && !adaptiveComplete && adaptiveKnownKeys.size > staleKnownKeysMark) {
|
|
323658
|
+
staleKnownKeysMark = adaptiveKnownKeys.size;
|
|
323659
|
+
if (currentSurveyProgressMark === lastSurveyProgressMark && staleIterations > 0) {
|
|
323660
|
+
logs2.push(` Stall check: frontier grew to ${adaptiveKnownKeys.size} identities \u2014 counting as progress`);
|
|
323661
|
+
}
|
|
323662
|
+
staleIterations = 0;
|
|
323663
|
+
} else if (currentSurveyProgressMark === lastSurveyProgressMark) {
|
|
322799
323664
|
staleIterations++;
|
|
322800
323665
|
if (staleIterations >= 4) {
|
|
322801
|
-
|
|
322802
|
-
|
|
322803
|
-
|
|
322804
|
-
|
|
322805
|
-
|
|
323666
|
+
if (adaptiveActive && !adaptiveComplete) {
|
|
323667
|
+
const { directive, keepGoing } = runBudgetController({ allowExtension: false });
|
|
323668
|
+
if (keepGoing) {
|
|
323669
|
+
staleIterations = 2;
|
|
323670
|
+
if (directive) {
|
|
323671
|
+
if (useV3) {
|
|
323672
|
+
transientDirectives.push(directive);
|
|
323673
|
+
} else {
|
|
323674
|
+
messages.push({ role: "user", content: directive });
|
|
323675
|
+
}
|
|
323676
|
+
}
|
|
323677
|
+
} else {
|
|
323678
|
+
logs2.push(` Survey hard stop (adaptive terminal): no survey progress in ${staleIterations * 5} iterations`);
|
|
323679
|
+
explorationComplete = true;
|
|
323680
|
+
summary = `Exploration force-stopped after ${iteration} iterations - no survey progress in ${staleIterations * 5} iterations. Visited ${currentVisitedSize} pages.`;
|
|
323681
|
+
break;
|
|
323682
|
+
}
|
|
323683
|
+
} else {
|
|
323684
|
+
logs2.push(` Survey hard stop: no survey progress in ${staleIterations * 5} iterations, forcing completion`);
|
|
323685
|
+
explorationComplete = true;
|
|
323686
|
+
summary = `Exploration force-stopped after ${iteration} iterations - no survey progress in ${staleIterations * 5} iterations. Visited ${currentVisitedSize} pages.`;
|
|
323687
|
+
break;
|
|
323688
|
+
}
|
|
323689
|
+
} else if (staleIterations >= 2 && !adaptiveActive) {
|
|
322806
323690
|
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
323691
|
if (useV3) {
|
|
322808
323692
|
transientDirectives.push(loopMsg);
|
|
@@ -322849,7 +323733,23 @@ ${inboxPromptBlock}`;
|
|
|
322849
323733
|
lastJourneyProgress = currentJourneyProgress;
|
|
322850
323734
|
}
|
|
322851
323735
|
}
|
|
322852
|
-
if (iteration >=
|
|
323736
|
+
if (adaptiveActive && !adaptiveComplete && iteration >= adaptiveDecisionArmedAt) {
|
|
323737
|
+
const { directive, keepGoing } = runBudgetController();
|
|
323738
|
+
if (directive) {
|
|
323739
|
+
if (useV3) {
|
|
323740
|
+
transientDirectives.push(directive);
|
|
323741
|
+
} else {
|
|
323742
|
+
messages.push({ role: "user", content: directive });
|
|
323743
|
+
}
|
|
323744
|
+
}
|
|
323745
|
+
if (!keepGoing && adaptiveComplete) {
|
|
323746
|
+
explorationComplete = true;
|
|
323747
|
+
summary = summary || `Exploration finished after ${iteration} iterations (adaptive budget: coverage incomplete).`;
|
|
323748
|
+
logs2.push(` [adaptive-budget] terminal at iteration ${iteration}`);
|
|
323749
|
+
break;
|
|
323750
|
+
}
|
|
323751
|
+
}
|
|
323752
|
+
if (!adaptiveActive && iteration >= Math.floor(maxIterations * 0.9)) {
|
|
322853
323753
|
const budgetTestCount = useV2 ? state2.recordedJourneys.length : localJourneys.length;
|
|
322854
323754
|
const budgetBlocking = linkFrontierEnabled ? (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2) : [];
|
|
322855
323755
|
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 +323841,7 @@ ${recent}`);
|
|
|
322941
323841
|
if (remainingFocusAreas.length > 0)
|
|
322942
323842
|
progressLines.push(`### Focus Areas NOT YET Visited: ${remainingFocusAreas.join(", ")} - explore these next!`);
|
|
322943
323843
|
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.");
|
|
323844
|
+
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
323845
|
}
|
|
322946
323846
|
progressLines.push("");
|
|
322947
323847
|
progressLines.push("## NEXT ACTIONS");
|
|
@@ -322952,7 +323852,7 @@ ${recent}`);
|
|
|
322952
323852
|
});
|
|
322953
323853
|
if (untestedAreas.length > 0) {
|
|
322954
323854
|
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");
|
|
323855
|
+
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
323856
|
} else {
|
|
322957
323857
|
progressLines.push("All focus areas have tests. Look for additional edge cases or call finish_exploration.");
|
|
322958
323858
|
}
|
|
@@ -323005,7 +323905,8 @@ ${currentSummary}`;
|
|
|
323005
323905
|
const brief = (0, exploration_prompt_builder_js_2.buildControlBrief)(state2, {
|
|
323006
323906
|
mode: options?.mode ?? "full",
|
|
323007
323907
|
maxIterations,
|
|
323008
|
-
focusAreas: options?.focusAreas
|
|
323908
|
+
focusAreas: options?.focusAreas,
|
|
323909
|
+
adaptiveBudget: adaptiveActive
|
|
323009
323910
|
});
|
|
323010
323911
|
messages.push({ role: "user", content: brief });
|
|
323011
323912
|
lastBriefRoute = currentStateRoute;
|
|
@@ -323024,7 +323925,8 @@ ${currentSummary}`;
|
|
|
323024
323925
|
recentExchanges,
|
|
323025
323926
|
incremental: options?.incremental,
|
|
323026
323927
|
userNotes: options?.userNotes,
|
|
323027
|
-
linkFrontierEnabled
|
|
323928
|
+
linkFrontierEnabled,
|
|
323929
|
+
adaptiveBudget: adaptiveActive
|
|
323028
323930
|
});
|
|
323029
323931
|
transientDirectives.length = 0;
|
|
323030
323932
|
}
|
|
@@ -323233,6 +324135,9 @@ ${currentSummary}`;
|
|
|
323233
324135
|
sessionState: normalized.sessionState
|
|
323234
324136
|
});
|
|
323235
324137
|
ingestLinksFromText(snapshotText.snapshotText);
|
|
324138
|
+
if (domHarvestEnabled) {
|
|
324139
|
+
await harvestDomLinksIntoFrontier();
|
|
324140
|
+
}
|
|
323236
324141
|
const isSurvey = options?.mode === "survey";
|
|
323237
324142
|
const subNavItems = [];
|
|
323238
324143
|
for (const el of normalized.elements) {
|
|
@@ -323741,6 +324646,30 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
323741
324646
|
logs2.push(` finish accepted with ${blocking.length} uncovered discovered route(s): ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(blocking, 10)}`);
|
|
323742
324647
|
}
|
|
323743
324648
|
}
|
|
324649
|
+
if (adaptiveActive && !adaptiveComplete) {
|
|
324650
|
+
refreshAdaptiveDiscovery();
|
|
324651
|
+
const remainingList = adaptiveDebtRoutes();
|
|
324652
|
+
if (remainingList.length > 0 && iteration < adaptiveHardCap) {
|
|
324653
|
+
let reason = null;
|
|
324654
|
+
if (iteration >= adaptiveDecisionArmedAt) {
|
|
324655
|
+
const { directive, keepGoing } = runBudgetController();
|
|
324656
|
+
if (keepGoing) {
|
|
324657
|
+
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.`;
|
|
324658
|
+
}
|
|
324659
|
+
} else {
|
|
324660
|
+
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.`;
|
|
324661
|
+
}
|
|
324662
|
+
if (reason) {
|
|
324663
|
+
toolResults.push({
|
|
324664
|
+
role: "tool",
|
|
324665
|
+
tool_call_id: toolCall.id,
|
|
324666
|
+
content: JSON.stringify({ acknowledged: false, keepExploring: true, reason })
|
|
324667
|
+
});
|
|
324668
|
+
logs2.push(` [adaptive-budget] finish bounced: ${remainingList.length} route(s) still un-captured`);
|
|
324669
|
+
continue;
|
|
324670
|
+
}
|
|
324671
|
+
}
|
|
324672
|
+
}
|
|
323744
324673
|
explorationComplete = true;
|
|
323745
324674
|
summary = String(toolArgs.summary || "");
|
|
323746
324675
|
(0, exploration_state_js_1.reduceEvent)(state2, { type: "EXPLORATION_FINISHED", summary });
|
|
@@ -323798,6 +324727,18 @@ Exploration complete: ${summary}`);
|
|
|
323798
324727
|
continue;
|
|
323799
324728
|
}
|
|
323800
324729
|
if (toolName === "fill_credential" || toolName === "fill_credential_form") {
|
|
324730
|
+
if (emailVerificationUnavailable) {
|
|
324731
|
+
const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
|
|
324732
|
+
if (codeGuard.blocked) {
|
|
324733
|
+
logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
|
|
324734
|
+
toolResults.push({
|
|
324735
|
+
role: "tool",
|
|
324736
|
+
tool_call_id: toolCall.id,
|
|
324737
|
+
content: codeGuard.message ?? "BLOCKED: cannot receive verification email"
|
|
324738
|
+
});
|
|
324739
|
+
continue;
|
|
324740
|
+
}
|
|
324741
|
+
}
|
|
323801
324742
|
const fullVarMap = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
323802
324743
|
const { secrets: dispatchSecrets } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMap);
|
|
323803
324744
|
try {
|
|
@@ -323880,6 +324821,18 @@ Exploration complete: ${summary}`);
|
|
|
323880
324821
|
continue;
|
|
323881
324822
|
}
|
|
323882
324823
|
}
|
|
324824
|
+
if (emailVerificationUnavailable) {
|
|
324825
|
+
const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
|
|
324826
|
+
if (codeGuard.blocked) {
|
|
324827
|
+
logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
|
|
324828
|
+
toolResults.push({
|
|
324829
|
+
role: "tool",
|
|
324830
|
+
tool_call_id: toolCall.id,
|
|
324831
|
+
content: codeGuard.message ?? "BLOCKED: cannot receive verification email"
|
|
324832
|
+
});
|
|
324833
|
+
continue;
|
|
324834
|
+
}
|
|
324835
|
+
}
|
|
323883
324836
|
if (inboxState?.provisioned && toolCallLooksLikeSignupSubmit(toolName, toolArgs)) {
|
|
323884
324837
|
accountAlreadyExistsRecoveryExpiresAt = iteration + 2;
|
|
323885
324838
|
}
|
|
@@ -324123,6 +325076,11 @@ Exploration complete: ${summary}`);
|
|
|
324123
325076
|
logs2.push(` \u26A0 Out of scope: ${detectedRoute} (focus: ${navFocusAreas.join(", ")})`);
|
|
324124
325077
|
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
325078
|
}
|
|
325079
|
+
} else if (surveyNavScope) {
|
|
325080
|
+
if (!(0, survey_scope_js_1.routeWithinSurveyBasePath)(detectedRoute, surveyNavScope) && !(0, survey_scope_js_1.hasAuthFlowSegment)(detectedRoute)) {
|
|
325081
|
+
logs2.push(` \u26A0 Out of survey scope: ${detectedRoute} (entry scope: ${surveyNavScope.basePath})`);
|
|
325082
|
+
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.`;
|
|
325083
|
+
}
|
|
324126
325084
|
}
|
|
324127
325085
|
}
|
|
324128
325086
|
}
|
|
@@ -324418,6 +325376,7 @@ Fatal error: ${msg}`);
|
|
|
324418
325376
|
const status = fatalErr?.status;
|
|
324419
325377
|
const lower = msg.toLowerCase();
|
|
324420
325378
|
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";
|
|
325379
|
+
const truncatedTransient = (0, ai_retry_js_1.isTransientRateError)(fatalErr);
|
|
324421
325380
|
if (useV2) {
|
|
324422
325381
|
try {
|
|
324423
325382
|
(0, exploration_state_js_1.flushUnflushedCommands)(state2, variableContext.allTestVariables ?? variableContext.publicTestVariables);
|
|
@@ -324462,7 +325421,8 @@ Fatal error: ${msg}`);
|
|
|
324462
325421
|
collectedTransitions: state2.collectedTransitions,
|
|
324463
325422
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
|
|
324464
325423
|
truncated: true,
|
|
324465
|
-
truncatedReason
|
|
325424
|
+
truncatedReason,
|
|
325425
|
+
truncatedTransient
|
|
324466
325426
|
};
|
|
324467
325427
|
}
|
|
324468
325428
|
return {
|
|
@@ -324482,7 +325442,8 @@ Fatal error: ${msg}`);
|
|
|
324482
325442
|
collectedTransitions,
|
|
324483
325443
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
|
|
324484
325444
|
truncated: true,
|
|
324485
|
-
truncatedReason
|
|
325445
|
+
truncatedReason,
|
|
325446
|
+
truncatedTransient
|
|
324486
325447
|
};
|
|
324487
325448
|
} finally {
|
|
324488
325449
|
if (inboxReader) {
|
|
@@ -324511,9 +325472,12 @@ var require_discover_feature_explorer = __commonJS({
|
|
|
324511
325472
|
"../runner-core/dist/services/discover-feature-explorer.js"(exports) {
|
|
324512
325473
|
"use strict";
|
|
324513
325474
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
325475
|
+
exports.buildFeatureExplorePrompt = buildFeatureExplorePrompt;
|
|
325476
|
+
exports.buildIncrementalExplorePrompt = buildIncrementalExplorePrompt;
|
|
324514
325477
|
exports.runFeatureExplorePhase = runFeatureExplorePhase;
|
|
324515
325478
|
var discover_explorer_js_1 = require_discover_explorer();
|
|
324516
325479
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
325480
|
+
var credential_tools_js_1 = require_credential_tools();
|
|
324517
325481
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
324518
325482
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
324519
325483
|
function buildFeatureExplorerConfig(focusAreas, incremental, otherRoutes) {
|
|
@@ -324578,7 +325542,8 @@ var require_discover_feature_explorer = __commonJS({
|
|
|
324578
325542
|
const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
324579
325543
|
const hasCredentials = Object.keys(browserCredentials).length > 0;
|
|
324580
325544
|
const registerMode = !!((0, verification_inbox_js_1.loadVerificationInboxConfig)() && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext));
|
|
324581
|
-
const
|
|
325545
|
+
const emailVerificationUnavailable = !registerMode && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext) && !(0, credential_tools_js_1.hasUsableLoginIdentity)(browserCredentials, { isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail });
|
|
325546
|
+
const credentialLines = Object.entries(browserCredentials).filter(([k]) => k !== "DISCOVERY_SHOULD_REGISTER").map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
|
|
324582
325547
|
const otherFeaturesBlock = featureOpts.otherFeatures && featureOpts.otherFeatures.length > 0 ? `
|
|
324583
325548
|
## Other Features (DO NOT TEST)
|
|
324584
325549
|
These are separate features. Do NOT navigate to their pages, do NOT interact with them, do NOT call record_journey for them:
|
|
@@ -324689,6 +325654,22 @@ ${transitionBlock}
|
|
|
324689
325654
|
${(() => {
|
|
324690
325655
|
const isAuthFeature = featureOpts.focusAreas.some((fa) => /\/(auth|login|sign-?in|register|signup)\b/i.test(fa));
|
|
324691
325656
|
if (isAuthFeature && hasCredentials) {
|
|
325657
|
+
if (emailVerificationUnavailable) {
|
|
325658
|
+
return `## Login/Registration IS Your Test Assignment (email verification UNAVAILABLE)
|
|
325659
|
+
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.
|
|
325660
|
+
|
|
325661
|
+
Follow this order:
|
|
325662
|
+
1. Navigate to your first assigned auth page (e.g., the login page)
|
|
325663
|
+
2. Take a \`browser_snapshot\` to see what's there
|
|
325664
|
+
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)
|
|
325665
|
+
4. Call \`record_journey\` for the validation flow
|
|
325666
|
+
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
|
|
325667
|
+
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
|
|
325668
|
+
7. If you are signed in with NO email round-trip, call \`record_journey\` for the happy path
|
|
325669
|
+
8. Call \`capture_page\` on each auth page AFTER interacting (this saves the successful commands for test generation)
|
|
325670
|
+
|
|
325671
|
+
When done, call \`finish_exploration\` - if you hit the wall, note that the authenticated happy path is BLOCKED (cannot receive verification email).`;
|
|
325672
|
+
}
|
|
324692
325673
|
return `## Login/Registration IS Your Test Assignment
|
|
324693
325674
|
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
325675
|
|
|
@@ -324743,7 +325724,7 @@ ${otherFeaturesBlock}
|
|
|
324743
325724
|
4. Take \`browser_snapshot\` of the result page
|
|
324744
325725
|
- 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
325726
|
5. Call \`record_journey\` with:
|
|
324746
|
-
- All routes visited in \`routes\` array (e.g., ["/auth/login", "/dashboard"])
|
|
325727
|
+
- All routes visited in \`routes\` array (e.g., ${emailVerificationUnavailable ? '["/", "/contact"]' : '["/auth/login", "/dashboard"]'})
|
|
324747
325728
|
- Specific steps describing what you did
|
|
324748
325729
|
- expectedOutcome with the EXACT result you observed
|
|
324749
325730
|
- assertions with STABLE structural elements from the result page
|
|
@@ -324755,11 +325736,15 @@ ${otherFeaturesBlock}
|
|
|
324755
325736
|
4. Fix the errors and resubmit successfully \u2192 observe recovery
|
|
324756
325737
|
5. Call \`record_journey\` ONCE for this entire combined flow
|
|
324757
325738
|
|
|
324758
|
-
|
|
325739
|
+
${emailVerificationUnavailable ? `**Auth pages (login/register) - this runner CANNOT receive email:**
|
|
325740
|
+
- 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
|
|
325741
|
+
- 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
|
|
325742
|
+
- 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)
|
|
325743
|
+
- 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
325744
|
- You arrive at the page NOT logged in - test errors FIRST while in this state
|
|
324760
325745
|
- Empty fields \u2192 invalid data \u2192 call record_journey
|
|
324761
325746
|
- THEN do the happy path - valid credentials \u2192 call record_journey
|
|
324762
|
-
- This order avoids needing to log out between journeys
|
|
325747
|
+
- This order avoids needing to log out between journeys`}
|
|
324763
325748
|
|
|
324764
325749
|
### For CRUD Features - Combine into a Single Journey:
|
|
324765
325750
|
If the feature has Create/View/Edit/Delete operations across pages:
|
|
@@ -324784,7 +325769,7 @@ When you have thoroughly explored all the feature pages and tested the key flows
|
|
|
324784
325769
|
- Pages you visited and what you found there
|
|
324785
325770
|
|
|
324786
325771
|
## 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!\`
|
|
325772
|
+
${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
325773
|
- **REGISTRATION/SIGNUP** email fields: MUST be unique every run \u2192 \`testuser-\${Date.now()}@example.com\`
|
|
324789
325774
|
- Passwords for registration: \`TestPass123!\` (meets typical strength rules)`}
|
|
324790
325775
|
- **Other form fields**: use realistic static values (Jane Smith, 555-0100, https://example.com)
|
|
@@ -324806,7 +325791,17 @@ When calling \`record_journey\`:
|
|
|
324806
325791
|
|
|
324807
325792
|
**Example record_journey with good assertions:**
|
|
324808
325793
|
\`\`\`
|
|
324809
|
-
record_journey({
|
|
325794
|
+
${emailVerificationUnavailable ? `record_journey({
|
|
325795
|
+
name: "Login Form - Validation",
|
|
325796
|
+
route: "/auth/login",
|
|
325797
|
+
routes: ["/", "/auth/login"],
|
|
325798
|
+
steps: ["Navigate to /", "Click 'Sign In'", "Submit the empty form", "Observe validation errors"],
|
|
325799
|
+
expectedOutcome: "URL matches /auth/login. Validation error 'Email is required' is visible.",
|
|
325800
|
+
assertions: [
|
|
325801
|
+
{ type: "url", value: "/auth/login" },
|
|
325802
|
+
{ type: "error-text", value: "Email is required" }
|
|
325803
|
+
]
|
|
325804
|
+
})` : `record_journey({
|
|
324810
325805
|
name: "Login - Happy Path",
|
|
324811
325806
|
route: "/auth/login",
|
|
324812
325807
|
routes: ["/", "/auth/login", "/dashboard"],
|
|
@@ -324817,7 +325812,7 @@ record_journey({
|
|
|
324817
325812
|
{ type: "nav-link", locator: "getByRole('link', { name: 'Dashboard' })" },
|
|
324818
325813
|
{ type: "heading", locator: "getByRole('heading', { name: 'Dashboard' })" }
|
|
324819
325814
|
]
|
|
324820
|
-
})
|
|
325815
|
+
})`}
|
|
324821
325816
|
\`\`\`
|
|
324822
325817
|
BAD assertions (do NOT use): \`{ type: "heading", locator: "getByRole('heading', { name: 'Welcome back, John!' })" }\` - user-specific text
|
|
324823
325818
|
|
|
@@ -324853,13 +325848,16 @@ Do NOT re-propose a test with the same name or identical steps as any item above
|
|
|
324853
325848
|
${hasCredentials ? `
|
|
324854
325849
|
## Credentials (browser interaction ONLY - NEVER include in record_journey steps)
|
|
324855
325850
|
${credentialLines}
|
|
325851
|
+
` : ""}${emailVerificationUnavailable ? `
|
|
325852
|
+
${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}
|
|
324856
325853
|
` : ""}`;
|
|
324857
325854
|
}
|
|
324858
325855
|
function buildIncrementalExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts) {
|
|
324859
325856
|
const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
324860
325857
|
const hasCredentials = Object.keys(browserCredentials).length > 0;
|
|
324861
325858
|
const registerMode = !!((0, verification_inbox_js_1.loadVerificationInboxConfig)() && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext));
|
|
324862
|
-
const
|
|
325859
|
+
const emailVerificationUnavailable = !registerMode && (0, inbox_tools_js_1.hasRegisterIntent)(variableContext) && !(0, credential_tools_js_1.hasUsableLoginIdentity)(browserCredentials, { isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail });
|
|
325860
|
+
const credentialLines = Object.entries(browserCredentials).filter(([k]) => k !== "DISCOVERY_SHOULD_REGISTER").map(([k, v]) => `- **${k}**: \`${v}\``).join("\n");
|
|
324863
325861
|
const existingTestsBlock = featureOpts.existingTestDetails && featureOpts.existingTestDetails.length > 0 ? featureOpts.existingTestDetails.map((t, i) => {
|
|
324864
325862
|
const stepsStr = t.steps.length > 0 ? `
|
|
324865
325863
|
Steps: ${t.steps.join(" \u2192 ")}` : "";
|
|
@@ -324931,7 +325929,7 @@ ${featureOpts.appBrief}
|
|
|
324931
325929
|
${featureOpts.focusAreas.map((fa) => `- ${fa}`).join("\n")}
|
|
324932
325930
|
${pageElementsBlock}${otherFeaturesBlock}
|
|
324933
325931
|
## 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"}
|
|
325932
|
+
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
325933
|
2. Take \`browser_snapshot\` on each page
|
|
324936
325934
|
3. Compare what you see with the existing tests - what flows are NOT tested?
|
|
324937
325935
|
4. For each gap you find, interact with the page to verify the flow works, then call \`record_journey\`
|
|
@@ -324955,11 +325953,14 @@ ${featureOpts.priorInsight}
|
|
|
324955
325953
|
${hasCredentials ? `
|
|
324956
325954
|
## Credentials (browser interaction ONLY)
|
|
324957
325955
|
${credentialLines}
|
|
325956
|
+
` : ""}${emailVerificationUnavailable ? `
|
|
325957
|
+
${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}
|
|
324958
325958
|
` : ""}`;
|
|
324959
325959
|
}
|
|
324960
325960
|
async function runFeatureExplorePhase(mcpClient, baseUrl, variableContext, existingSiteMap, projectId, existingTestNames, featureOpts) {
|
|
324961
325961
|
const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
|
|
324962
|
-
const
|
|
325962
|
+
const registerIntent = (0, inbox_tools_js_1.hasRegisterIntent)(variableContext);
|
|
325963
|
+
const registerMode = !!(inboxConfig && registerIntent);
|
|
324963
325964
|
let inboxPasswordKey = "TEST_USER_PASSWORD";
|
|
324964
325965
|
if (registerMode && inboxConfig) {
|
|
324965
325966
|
const inboxIdentity = (0, inbox_tools_js_1.createInboxRunState)(inboxConfig, { tagSeed: baseUrl, projectScope: projectId });
|
|
@@ -324970,6 +325971,9 @@ ${credentialLines}
|
|
|
324970
325971
|
passwordKey: inboxPasswordKey
|
|
324971
325972
|
});
|
|
324972
325973
|
}
|
|
325974
|
+
const emailVerificationUnavailable = registerIntent && !inboxConfig && !(0, credential_tools_js_1.hasUsableLoginIdentity)(variableContext.allTestVariables ?? variableContext.publicTestVariables, {
|
|
325975
|
+
isPlaceholderIdentityValue: inbox_tools_js_1.isThrowawayRegisterSeedEmail
|
|
325976
|
+
});
|
|
324973
325977
|
const isIncremental = featureOpts.incremental && existingTestNames.length > 0;
|
|
324974
325978
|
const systemPrompt = isIncremental ? buildIncrementalExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts) : buildFeatureExplorePrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, featureOpts);
|
|
324975
325979
|
const hasCredentials = Object.keys(variableContext.allTestVariables ?? variableContext.publicTestVariables).length > 0;
|
|
@@ -324980,6 +325984,8 @@ ${credentialLines}
|
|
|
324980
325984
|
const focusHint = featureOpts.userNotes ? ` The user asked you to focus on: "${featureOpts.userNotes}".` : "";
|
|
324981
325985
|
if (registerMode) {
|
|
324982
325986
|
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."`;
|
|
325987
|
+
} else if (emailVerificationUnavailable) {
|
|
325988
|
+
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
325989
|
} else if (isAuthFeature) {
|
|
324984
325990
|
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
325991
|
} else if (hasCredentials) {
|
|
@@ -324991,6 +325997,8 @@ ${credentialLines}
|
|
|
324991
325997
|
let navigationInstruction;
|
|
324992
325998
|
if (registerMode) {
|
|
324993
325999
|
navigationInstruction = `Navigate to ${baseUrl} and create your account:${registerHint}. Then go to your assigned feature pages: ${featureOpts.focusAreas.join(", ")}.`;
|
|
326000
|
+
} else if (emailVerificationUnavailable) {
|
|
326001
|
+
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
326002
|
} else if (isAuthFeature) {
|
|
324995
326003
|
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
326004
|
} else if (hasCredentials) {
|
|
@@ -325223,8 +326231,14 @@ var require_auth_capture_run = __commonJS({
|
|
|
325223
326231
|
exports.filterLoginCaptureCredentials = filterLoginCaptureCredentials;
|
|
325224
326232
|
exports.buildLoginCaptureCredentials = buildLoginCaptureCredentials;
|
|
325225
326233
|
exports.getLoginCaptureMaxIterations = getLoginCaptureMaxIterations;
|
|
326234
|
+
exports.getCaptureRateLimitMaxRetries = getCaptureRateLimitMaxRetries;
|
|
326235
|
+
exports.getCaptureRateLimitMaxDelayMs = getCaptureRateLimitMaxDelayMs;
|
|
326236
|
+
exports.getAuthCaptureTimeoutMs = getAuthCaptureTimeoutMs;
|
|
326237
|
+
exports.getAuthCaptureRunRetryMaxAttempts = getAuthCaptureRunRetryMaxAttempts;
|
|
326238
|
+
exports.classifyCaptureMiss = classifyCaptureMiss;
|
|
325226
326239
|
exports.harvestAuthProofLive = harvestAuthProofLive;
|
|
325227
326240
|
exports.findCredentialLiteralLeaks = findCredentialLiteralLeaks;
|
|
326241
|
+
exports.runLoginCaptureRunWithOutcome = runLoginCaptureRunWithOutcome;
|
|
325228
326242
|
exports.runLoginCaptureRun = runLoginCaptureRun;
|
|
325229
326243
|
exports.getRegisterCaptureMaxIterations = getRegisterCaptureMaxIterations;
|
|
325230
326244
|
exports.runRegisterCaptureRun = runRegisterCaptureRun;
|
|
@@ -325237,6 +326251,7 @@ var require_auth_capture_run = __commonJS({
|
|
|
325237
326251
|
var playwright_native_js_1 = require_playwright_native();
|
|
325238
326252
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
325239
326253
|
var credential_tools_js_1 = require_credential_tools();
|
|
326254
|
+
var ai_retry_js_1 = require_ai_retry();
|
|
325240
326255
|
var LOGIN_CAPTURE_CONTROL_VARIABLES = /* @__PURE__ */ new Set([
|
|
325241
326256
|
"BASE_URL",
|
|
325242
326257
|
"DISCOVERY_SHOULD_REGISTER",
|
|
@@ -325270,6 +326285,36 @@ var require_auth_capture_run = __commonJS({
|
|
|
325270
326285
|
}
|
|
325271
326286
|
var CAPTURE_RATE_LIMIT_MAX_RETRIES = 2;
|
|
325272
326287
|
var CAPTURE_RATE_LIMIT_MAX_DELAY_MS = 8e3;
|
|
326288
|
+
function readNonNegativeIntEnv(env, name, fallback) {
|
|
326289
|
+
const raw = env[name]?.trim();
|
|
326290
|
+
if (raw === void 0 || raw === "" || !/^\d+$/.test(raw))
|
|
326291
|
+
return fallback;
|
|
326292
|
+
const parsed = Number(raw);
|
|
326293
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
326294
|
+
}
|
|
326295
|
+
function getCaptureRateLimitMaxRetries(env = process.env) {
|
|
326296
|
+
return readNonNegativeIntEnv(env, "AUTH_CAPTURE_RATE_LIMIT_MAX_RETRIES", CAPTURE_RATE_LIMIT_MAX_RETRIES);
|
|
326297
|
+
}
|
|
326298
|
+
function getCaptureRateLimitMaxDelayMs(env = process.env) {
|
|
326299
|
+
return readPositiveIntEnv2(env, "AUTH_CAPTURE_RATE_LIMIT_MAX_DELAY_MS", CAPTURE_RATE_LIMIT_MAX_DELAY_MS);
|
|
326300
|
+
}
|
|
326301
|
+
var DEFAULT_AUTH_CAPTURE_TIMEOUT_MS = 4 * 60 * 1e3;
|
|
326302
|
+
function getAuthCaptureTimeoutMs(env = process.env) {
|
|
326303
|
+
return readPositiveIntEnv2(env, "AUTH_CAPTURE_TIMEOUT_MS", DEFAULT_AUTH_CAPTURE_TIMEOUT_MS);
|
|
326304
|
+
}
|
|
326305
|
+
var DEFAULT_AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS = 2;
|
|
326306
|
+
function getAuthCaptureRunRetryMaxAttempts(env = process.env) {
|
|
326307
|
+
return readPositiveIntEnv2(env, "AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS", DEFAULT_AUTH_CAPTURE_RUN_RETRY_MAX_ATTEMPTS);
|
|
326308
|
+
}
|
|
326309
|
+
function classifyCaptureMiss(explore, fallback) {
|
|
326310
|
+
if (!explore.truncated)
|
|
326311
|
+
return fallback;
|
|
326312
|
+
if (explore.truncatedTransient)
|
|
326313
|
+
return "rate_limited";
|
|
326314
|
+
if (explore.truncatedReason === "timeout")
|
|
326315
|
+
return "timeout";
|
|
326316
|
+
return "error";
|
|
326317
|
+
}
|
|
325273
326318
|
function buildLoginCaptureSystemPrompt(baseUrl, availableKeys, maxIterations = getLoginCaptureMaxIterations()) {
|
|
325274
326319
|
const keyLines = availableKeys.map((k) => `- \`${k}\``).join("\n");
|
|
325275
326320
|
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 +326338,7 @@ ${keyLines || "- (none provided)"}
|
|
|
325293
326338
|
## Hard rules
|
|
325294
326339
|
- Do NOT submit registration, password-reset, or any other forms. Login only.
|
|
325295
326340
|
- Do NOT fill validation / negative values. Happy-path login only.
|
|
326341
|
+
- 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
326342
|
- Use RELATIVE routes in capture_page (e.g. \`/login\`, \`/dashboard\`), never absolute URLs.
|
|
325297
326343
|
- If login fails after 2 attempts, call \`capture_page\` noting the failure and call \`finish_exploration\`.
|
|
325298
326344
|
- You have a hard limit of ${maxIterations} iterations. Finish well before then.`;
|
|
@@ -325463,17 +326509,17 @@ ${keyLines || "- (none provided)"}
|
|
|
325463
326509
|
}
|
|
325464
326510
|
return false;
|
|
325465
326511
|
}
|
|
325466
|
-
async function
|
|
326512
|
+
async function runLoginCaptureRunWithOutcome(args) {
|
|
325467
326513
|
const { loginBrowser, baseUrl, projectId, model, onAudit, logs: logs2 } = args;
|
|
325468
326514
|
const credentials = filterLoginCaptureCredentials(args.credentials);
|
|
325469
326515
|
if ((0, agentic_cli_constants_js_1.getAgenticEngine)(model) !== "native") {
|
|
325470
326516
|
logs2(` auth-capture skipped: engine for ${model} is not native (V1 is native-engine-only)`);
|
|
325471
|
-
return null;
|
|
326517
|
+
return { recipe: null, outcome: "skipped" };
|
|
325472
326518
|
}
|
|
325473
326519
|
const { secrets } = (0, credential_tools_js_1.partitionTestVariables)(credentials);
|
|
325474
326520
|
if (Object.keys(secrets).length === 0) {
|
|
325475
326521
|
logs2(" auth-capture skipped: no secret credentials available to log in with");
|
|
325476
|
-
return null;
|
|
326522
|
+
return { recipe: null, outcome: "skipped" };
|
|
325477
326523
|
}
|
|
325478
326524
|
const auditRemap = onAudit ? (info) => {
|
|
325479
326525
|
try {
|
|
@@ -325494,7 +326540,7 @@ ${keyLines || "- (none provided)"}
|
|
|
325494
326540
|
const mcpClient = handle.mcpClient;
|
|
325495
326541
|
if (!mcpClient) {
|
|
325496
326542
|
logs2(" auth-capture aborted: instrumented context has no MCP client");
|
|
325497
|
-
return null;
|
|
326543
|
+
return { recipe: null, outcome: "skipped" };
|
|
325498
326544
|
}
|
|
325499
326545
|
const availableKeys = Object.keys(credentials);
|
|
325500
326546
|
const maxIterations = getLoginCaptureMaxIterations();
|
|
@@ -325504,8 +326550,8 @@ ${keyLines || "- (none provided)"}
|
|
|
325504
326550
|
enableRecordJourney: false,
|
|
325505
326551
|
// login-only — no feature journeys to record
|
|
325506
326552
|
maxIterationsOverride: maxIterations,
|
|
325507
|
-
rateLimitMaxRetries:
|
|
325508
|
-
rateLimitMaxDelayMs:
|
|
326553
|
+
rateLimitMaxRetries: getCaptureRateLimitMaxRetries(),
|
|
326554
|
+
rateLimitMaxDelayMs: getCaptureRateLimitMaxDelayMs()
|
|
325509
326555
|
};
|
|
325510
326556
|
const exploreResult = await (0, discover_explorer_js_1.runExplorePhase)(mcpClient, baseUrl, {
|
|
325511
326557
|
publicTestVariables: credentials,
|
|
@@ -325520,6 +326566,7 @@ ${keyLines || "- (none provided)"}
|
|
|
325520
326566
|
onProgress: void 0
|
|
325521
326567
|
}, loginConfig);
|
|
325522
326568
|
logs2(` auth-capture explore: ${exploreResult.pagesDiscovered} pages, ${exploreResult.collectedPages.length} captured`);
|
|
326569
|
+
const exploreMiss = (fallback) => classifyCaptureMiss(exploreResult, fallback);
|
|
325523
326570
|
const loginObservedOrigins = exploreResult.observedOrigins ?? [];
|
|
325524
326571
|
try {
|
|
325525
326572
|
args.onObservedOrigins?.(loginObservedOrigins);
|
|
@@ -325535,7 +326582,7 @@ ${keyLines || "- (none provided)"}
|
|
|
325535
326582
|
const login = (0, discover_generator_js_1.getLoginCommands)(loginSiteMap.pages, credentials);
|
|
325536
326583
|
if (!login) {
|
|
325537
326584
|
logs2(" \u26A0 auth unverified (login run produced no derivable login sequence) \u2014 falling back to model-authored login");
|
|
325538
|
-
return null;
|
|
326585
|
+
return { recipe: null, outcome: exploreMiss("no_login") };
|
|
325539
326586
|
}
|
|
325540
326587
|
const scrubbedLogin = {
|
|
325541
326588
|
...login,
|
|
@@ -325545,19 +326592,19 @@ ${keyLines || "- (none provided)"}
|
|
|
325545
326592
|
const authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
|
|
325546
326593
|
if (!authProof) {
|
|
325547
326594
|
logs2(" \u26A0 auth unverified (no durable signed-in proof on the authenticated page) \u2014 falling back to model-authored login");
|
|
325548
|
-
return null;
|
|
326595
|
+
return { recipe: null, outcome: exploreMiss("verify_failed") };
|
|
325549
326596
|
}
|
|
325550
326597
|
const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(scrubbedLogin, authProof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
|
|
325551
326598
|
const rendered = (0, auth_recipe_js_1.renderAuthFn)(candidate);
|
|
325552
326599
|
const leaks = findCredentialLiteralLeaks(rendered, credentials);
|
|
325553
326600
|
if (leaks.length > 0) {
|
|
325554
326601
|
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;
|
|
326602
|
+
return { recipe: null, outcome: "credential_leak" };
|
|
325556
326603
|
}
|
|
325557
326604
|
const planLeaks = findCredentialLiteralLeaks(JSON.stringify(candidate.replayPlan ?? []), credentials);
|
|
325558
326605
|
if (planLeaks.length > 0) {
|
|
325559
326606
|
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;
|
|
326607
|
+
return { recipe: null, outcome: "credential_leak" };
|
|
325561
326608
|
}
|
|
325562
326609
|
try {
|
|
325563
326610
|
await handle.close();
|
|
@@ -325567,14 +326614,14 @@ ${keyLines || "- (none provided)"}
|
|
|
325567
326614
|
const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
|
|
325568
326615
|
if (!ok) {
|
|
325569
326616
|
logs2(" \u26A0 auth unverified (live-verify replay failed \u2014 fleet-config drift) \u2014 falling back to model-authored login");
|
|
325570
|
-
return null;
|
|
326617
|
+
return { recipe: null, outcome: exploreMiss("verify_failed") };
|
|
325571
326618
|
}
|
|
325572
326619
|
const proofDesc = authProof.kind === "role" ? `role:${authProof.role}${authProof.name ? `(${authProof.name})` : ""}` : authProof.kind === "testid" ? `testid:${authProof.testId}` : `url:${authProof.route}`;
|
|
325573
326620
|
logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${proofDesc}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
|
|
325574
|
-
return candidate;
|
|
326621
|
+
return { recipe: candidate, outcome: "captured" };
|
|
325575
326622
|
} catch (err) {
|
|
325576
326623
|
logs2(` \u26A0 auth-capture run failed: ${err instanceof Error ? err.message : String(err)} \u2014 falling back to model-authored login`);
|
|
325577
|
-
return null;
|
|
326624
|
+
return { recipe: null, outcome: (0, ai_retry_js_1.isTransientRateError)(err) ? "rate_limited" : "error" };
|
|
325578
326625
|
} finally {
|
|
325579
326626
|
if (handle) {
|
|
325580
326627
|
try {
|
|
@@ -325584,6 +326631,9 @@ ${keyLines || "- (none provided)"}
|
|
|
325584
326631
|
}
|
|
325585
326632
|
}
|
|
325586
326633
|
}
|
|
326634
|
+
async function runLoginCaptureRun(args) {
|
|
326635
|
+
return (await runLoginCaptureRunWithOutcome(args)).recipe;
|
|
326636
|
+
}
|
|
325587
326637
|
var DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS = 18;
|
|
325588
326638
|
function getRegisterCaptureMaxIterations(env = process.env) {
|
|
325589
326639
|
return readPositiveIntEnv2(env, "AUTH_CAPTURE_REGISTER_MAX_ITERATIONS", DEFAULT_MAX_REGISTER_CAPTURE_ITERATIONS);
|
|
@@ -325655,8 +326705,8 @@ ${keyLines || "- (none provided)"}
|
|
|
325655
326705
|
userMessage: buildRegisterCaptureUserMessage(baseUrl),
|
|
325656
326706
|
enableRecordJourney: false,
|
|
325657
326707
|
maxIterationsOverride: maxIterations,
|
|
325658
|
-
rateLimitMaxRetries:
|
|
325659
|
-
rateLimitMaxDelayMs:
|
|
326708
|
+
rateLimitMaxRetries: getCaptureRateLimitMaxRetries(),
|
|
326709
|
+
rateLimitMaxDelayMs: getCaptureRateLimitMaxDelayMs(),
|
|
325660
326710
|
// Register + poll the PROJECT-SCOPED address so it matches the one the
|
|
325661
326711
|
// standalone authored tests log in with (see #922).
|
|
325662
326712
|
inboxTagOverride: args.inboxTag
|
|
@@ -332388,6 +333438,8 @@ var require_discover_agent = __commonJS({
|
|
|
332388
333438
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
332389
333439
|
exports.runMobileDiscoveryOnRunner = exports.buildInMemorySiteMap = void 0;
|
|
332390
333440
|
exports.runCanvasPreflight = runCanvasPreflight;
|
|
333441
|
+
exports.runCaptureAttemptLoop = runCaptureAttemptLoop;
|
|
333442
|
+
exports.isCaptureRunRetryEnabled = isCaptureRunRetryEnabled;
|
|
332391
333443
|
exports.getFatalExploreError = getFatalExploreError;
|
|
332392
333444
|
exports.runSiteDiscoveryOnRunner = runSiteDiscoveryOnRunner2;
|
|
332393
333445
|
exports.runFeatureDiscoveryOnRunner = runFeatureDiscoveryOnRunner2;
|
|
@@ -332636,8 +333688,56 @@ Respond with ONLY valid JSON:
|
|
|
332636
333688
|
}
|
|
332637
333689
|
return merged;
|
|
332638
333690
|
}
|
|
332639
|
-
|
|
332640
|
-
|
|
333691
|
+
function computeCaptureRetryBackoffMs(attempt) {
|
|
333692
|
+
const capped = Math.min(3e3 * Math.pow(2, attempt - 1), 15e3);
|
|
333693
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
333694
|
+
return Math.round(capped * jitter);
|
|
333695
|
+
}
|
|
333696
|
+
async function runCaptureAttemptLoop(args) {
|
|
333697
|
+
const { runCapture, maxAttempts, deadline, minRetryBudgetMs, now, sleep, backoffMs, log: log2, onAttempt } = args;
|
|
333698
|
+
let attemptsMade = 0;
|
|
333699
|
+
let lastOutcome = "error";
|
|
333700
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
333701
|
+
if (now() >= deadline) {
|
|
333702
|
+
lastOutcome = "timeout";
|
|
333703
|
+
break;
|
|
333704
|
+
}
|
|
333705
|
+
attemptsMade = attempt;
|
|
333706
|
+
onAttempt?.(attempt);
|
|
333707
|
+
const { recipe, outcome } = await runCapture(attempt);
|
|
333708
|
+
lastOutcome = outcome;
|
|
333709
|
+
if (recipe)
|
|
333710
|
+
return { recipe, outcome, attempts: attemptsMade };
|
|
333711
|
+
if (attempt >= maxAttempts || outcome !== "rate_limited")
|
|
333712
|
+
break;
|
|
333713
|
+
const remaining = deadline - now();
|
|
333714
|
+
if (remaining < minRetryBudgetMs) {
|
|
333715
|
+
log2(`auth-prologue: transient rate-limit miss, but only ${Math.round(remaining / 1e3)}s of the capture wall remain \u2014 not retrying`);
|
|
333716
|
+
break;
|
|
333717
|
+
}
|
|
333718
|
+
const backoff = Math.min(backoffMs(attempt), remaining - minRetryBudgetMs);
|
|
333719
|
+
if (backoff > 0) {
|
|
333720
|
+
log2(`auth-prologue: login-capture hit a transient rate limit (attempt ${attempt}/${maxAttempts}) \u2014 retrying in ${Math.round(backoff / 1e3)}s`);
|
|
333721
|
+
await sleep(backoff);
|
|
333722
|
+
}
|
|
333723
|
+
if (deadline - now() < minRetryBudgetMs) {
|
|
333724
|
+
log2("auth-prologue: capture wall nearly exhausted after backoff \u2014 not retrying");
|
|
333725
|
+
break;
|
|
333726
|
+
}
|
|
333727
|
+
}
|
|
333728
|
+
return { recipe: null, outcome: lastOutcome, attempts: attemptsMade };
|
|
333729
|
+
}
|
|
333730
|
+
function logAuthGenerationStats(logs2, tests, authRecipe) {
|
|
333731
|
+
const needsAuth = tests.filter((t) => t.tags.includes("@needs-auth"));
|
|
333732
|
+
if (needsAuth.length === 0)
|
|
333733
|
+
return;
|
|
333734
|
+
const recipeStamped = needsAuth.filter((t) => /VALIDATE_QA_AUTH/.test(t.playwrightCode ?? "")).length;
|
|
333735
|
+
logs2.push(`[auth-capture] needsAuthTests=${needsAuth.length} recipeStamped=${recipeStamped} inlineLogin=${needsAuth.length - recipeStamped} recipeAvailable=${authRecipe ? "yes" : "no"}`);
|
|
333736
|
+
}
|
|
333737
|
+
function isCaptureRunRetryEnabled(env = process.env) {
|
|
333738
|
+
return (0, shared_1.parseBoolFlag)(env.AUTH_CAPTURE_RUN_RETRY, true);
|
|
333739
|
+
}
|
|
333740
|
+
function maybeStartLoginCaptureRun(ctx, callbackOptions, onAICall, pushLog, onObservedOrigins, onOutcome) {
|
|
332641
333741
|
if (!(0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && ctx.mode !== "capture")
|
|
332642
333742
|
return null;
|
|
332643
333743
|
if (ctx.authRecipe) {
|
|
@@ -332662,25 +333762,44 @@ Respond with ONLY valid JSON:
|
|
|
332662
333762
|
return null;
|
|
332663
333763
|
}
|
|
332664
333764
|
const credentials = (0, auth_capture_run_js_1.buildLoginCaptureCredentials)(ctx.variableContext);
|
|
333765
|
+
const runStart = Date.now();
|
|
333766
|
+
const timeoutMs = (0, auth_capture_run_js_1.getAuthCaptureTimeoutMs)();
|
|
333767
|
+
const deadline = runStart + timeoutMs;
|
|
333768
|
+
const retryEnabled = isCaptureRunRetryEnabled();
|
|
333769
|
+
const maxAttempts = retryEnabled ? (0, auth_capture_run_js_1.getAuthCaptureRunRetryMaxAttempts)() : 1;
|
|
333770
|
+
const MIN_RETRY_BUDGET_MS = 45e3;
|
|
333771
|
+
let attemptsMade = 0;
|
|
332665
333772
|
const run2 = (async () => {
|
|
332666
333773
|
let lease = null;
|
|
332667
333774
|
try {
|
|
332668
333775
|
lease = await acquire();
|
|
332669
|
-
const
|
|
332670
|
-
|
|
332671
|
-
|
|
332672
|
-
|
|
332673
|
-
|
|
332674
|
-
|
|
332675
|
-
|
|
332676
|
-
|
|
332677
|
-
|
|
332678
|
-
|
|
333776
|
+
const leased = lease;
|
|
333777
|
+
return await runCaptureAttemptLoop({
|
|
333778
|
+
maxAttempts,
|
|
333779
|
+
deadline,
|
|
333780
|
+
minRetryBudgetMs: MIN_RETRY_BUDGET_MS,
|
|
333781
|
+
now: Date.now,
|
|
333782
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
333783
|
+
backoffMs: computeCaptureRetryBackoffMs,
|
|
333784
|
+
log: pushLog,
|
|
333785
|
+
onAttempt: (n) => {
|
|
333786
|
+
attemptsMade = n;
|
|
333787
|
+
},
|
|
333788
|
+
runCapture: () => (0, auth_capture_run_js_1.runLoginCaptureRunWithOutcome)({
|
|
333789
|
+
loginBrowser: leased.browser,
|
|
333790
|
+
baseUrl: ctx.baseUrl,
|
|
333791
|
+
credentials,
|
|
333792
|
+
projectId: ctx.projectId,
|
|
333793
|
+
model,
|
|
333794
|
+
existingSiteMap: ctx.existingSiteMap,
|
|
333795
|
+
onAudit: onAICall,
|
|
333796
|
+
onObservedOrigins,
|
|
333797
|
+
logs: pushLog
|
|
333798
|
+
})
|
|
332679
333799
|
});
|
|
332680
|
-
return recipe;
|
|
332681
333800
|
} catch (err) {
|
|
332682
333801
|
pushLog(`auth-prologue: login-capture run threw (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
332683
|
-
return null;
|
|
333802
|
+
return { recipe: null, outcome: "error", attempts: attemptsMade };
|
|
332684
333803
|
} finally {
|
|
332685
333804
|
if (lease) {
|
|
332686
333805
|
try {
|
|
@@ -332693,16 +333812,19 @@ Respond with ONLY valid JSON:
|
|
|
332693
333812
|
let timer;
|
|
332694
333813
|
const timeout = new Promise((resolve2) => {
|
|
332695
333814
|
timer = setTimeout(() => {
|
|
332696
|
-
pushLog(`auth-prologue: login-capture run exceeded ${
|
|
332697
|
-
resolve2(null);
|
|
332698
|
-
},
|
|
333815
|
+
pushLog(`auth-prologue: login-capture run exceeded ${Math.round(timeoutMs / 1e3)}s \u2014 proceeding without a recipe`);
|
|
333816
|
+
resolve2({ recipe: null, outcome: "timeout", attempts: attemptsMade });
|
|
333817
|
+
}, Math.max(0, deadline - Date.now()));
|
|
332699
333818
|
timer.unref?.();
|
|
332700
333819
|
});
|
|
332701
|
-
|
|
333820
|
+
return Promise.race([run2, timeout]).finally(() => {
|
|
332702
333821
|
if (timer)
|
|
332703
333822
|
clearTimeout(timer);
|
|
332704
|
-
})
|
|
332705
|
-
|
|
333823
|
+
}).then((res) => {
|
|
333824
|
+
pushLog(`[auth-capture] outcome=${res.outcome} attempts=${res.attempts} durationMs=${Date.now() - runStart} mode=${ctx.mode ?? "discovery"}`);
|
|
333825
|
+
onOutcome?.(res.outcome);
|
|
333826
|
+
return res.recipe;
|
|
333827
|
+
}).catch(() => null);
|
|
332706
333828
|
}
|
|
332707
333829
|
function getFatalExploreError(exploreResult) {
|
|
332708
333830
|
const prefix = "Fatal error:";
|
|
@@ -332717,12 +333839,15 @@ Respond with ONLY valid JSON:
|
|
|
332717
333839
|
logs2.push(`Base URL: ${ctx.baseUrl}`);
|
|
332718
333840
|
onLog?.("starting", logs2.join("\n"));
|
|
332719
333841
|
const loginObservedOrigins = [];
|
|
333842
|
+
let captureOutcome;
|
|
332720
333843
|
const loginRun = maybeStartLoginCaptureRun(ctx, callbackOptions, onAICall, (line) => {
|
|
332721
333844
|
logs2.push(line);
|
|
332722
333845
|
onLog?.("progress", logs2.join("\n"));
|
|
332723
333846
|
}, (origins) => {
|
|
332724
333847
|
for (const origin of origins)
|
|
332725
333848
|
loginObservedOrigins.push(origin);
|
|
333849
|
+
}, (outcome) => {
|
|
333850
|
+
captureOutcome = outcome;
|
|
332726
333851
|
});
|
|
332727
333852
|
const authRecipe = loginRun ? await loginRun : null;
|
|
332728
333853
|
const duration = (Date.now() - startTime) / 1e3;
|
|
@@ -332747,7 +333872,7 @@ Respond with ONLY valid JSON:
|
|
|
332747
333872
|
},
|
|
332748
333873
|
duration,
|
|
332749
333874
|
mode: "capture",
|
|
332750
|
-
...authRecipe ? {} : { error:
|
|
333875
|
+
...authRecipe ? {} : { error: `Login capture did not produce a verified recipe${captureOutcome ? ` (${captureOutcome})` : ""}` }
|
|
332751
333876
|
};
|
|
332752
333877
|
}
|
|
332753
333878
|
const mode = ctx.mode;
|
|
@@ -332927,14 +334052,18 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
332927
334052
|
const updatedSiteMap = (0, sitemap_builder_js_1.buildInMemorySiteMap)(ctx.projectId, ctx.baseUrl, exploreResult.collectedPages, exploreResult.collectedTransitions, ctx.existingSiteMap);
|
|
332928
334053
|
if (updatedSiteMap.pages.length === 0) {
|
|
332929
334054
|
logs2.push("No site map data available - skipping plan and generate phases");
|
|
332930
|
-
|
|
332931
|
-
await loginRunP;
|
|
334055
|
+
const earlyAuthRecipe = loginRunP ? await loginRunP : null;
|
|
332932
334056
|
const duration2 = (Date.now() - startTime) / 1e3;
|
|
332933
334057
|
const fatalExploreError = getFatalExploreError(exploreResult);
|
|
332934
334058
|
return {
|
|
332935
334059
|
collectedPages: exploreResult.collectedPages,
|
|
332936
334060
|
collectedTransitions: exploreResult.collectedTransitions,
|
|
332937
334061
|
tests: [],
|
|
334062
|
+
// Persist only a FRESHLY-captured recipe — never re-persist a reused cached one
|
|
334063
|
+
// (ctx.authRecipe set) or a late warm run could overwrite/re-verify a newer recipe.
|
|
334064
|
+
authRecipe: ctx.authRecipe ? void 0 : earlyAuthRecipe,
|
|
334065
|
+
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
334066
|
+
resolvedAppOrigin: earlyAuthRecipe?.appOrigin ?? void 0,
|
|
332938
334067
|
healthObservations: healthObserver?.getSummary(),
|
|
332939
334068
|
logs: logs2.join("\n"),
|
|
332940
334069
|
screenshots,
|
|
@@ -333022,12 +334151,6 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
|
|
|
333022
334151
|
logs2.push(surveyHealth.totalIssues > 0 ? `
|
|
333023
334152
|
Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.length} console errors, ${surveyHealth.failedRequests.length} failed requests` : "\nHealth observer: no issues detected");
|
|
333024
334153
|
}
|
|
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
334154
|
let mergedWarning = planResult.warning;
|
|
333032
334155
|
if (exploreResult.truncated) {
|
|
333033
334156
|
const reasonLabel = exploreResult.truncatedReason === "rate_limit" ? "AI rate-limit" : exploreResult.truncatedReason === "timeout" ? "AI timeout" : "AI error";
|
|
@@ -333048,6 +334171,12 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
|
|
|
333048
334171
|
if (surveyAuthRecipe) {
|
|
333049
334172
|
logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
|
|
333050
334173
|
}
|
|
334174
|
+
const duration2 = (Date.now() - startTime) / 1e3;
|
|
334175
|
+
const featureCount = featureGroups?.length || finalPlan.testSuites.length;
|
|
334176
|
+
const summary2 = `Survey complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elementsFound} elements, ${featureCount} features identified in ${duration2.toFixed(1)}s`;
|
|
334177
|
+
logs2.push(`
|
|
334178
|
+
=== Survey Complete (${duration2.toFixed(1)}s) ===`);
|
|
334179
|
+
logs2.push(summary2);
|
|
333051
334180
|
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
334181
|
return {
|
|
333053
334182
|
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
@@ -333110,6 +334239,7 @@ Generation complete: ${generateResult.tests.length} tests generated (${generateE
|
|
|
333110
334239
|
const tags = t.tags.join(", ");
|
|
333111
334240
|
logs2.push(` ${i + 1}. ${t.name} [${tags}]${t.verified ? " \u2713live" : ""}`);
|
|
333112
334241
|
});
|
|
334242
|
+
logAuthGenerationStats(logs2, generateResult.tests, authRecipe);
|
|
333113
334243
|
onLog?.("generate", logs2.join("\n"));
|
|
333114
334244
|
const postGenClose = await handle.close();
|
|
333115
334245
|
handle = null;
|
|
@@ -333479,8 +334609,7 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
|
|
|
333479
334609
|
}
|
|
333480
334610
|
if (!isPlanReuseResume && updatedSiteMap.pages.length === 0) {
|
|
333481
334611
|
logs2.push("No site map data - skipping plan and generate phases");
|
|
333482
|
-
|
|
333483
|
-
await loginRunP;
|
|
334612
|
+
const earlyAuthRecipe = loginRunP ? await loginRunP : null;
|
|
333484
334613
|
const earlyClose = await handle.close();
|
|
333485
334614
|
handle = null;
|
|
333486
334615
|
discoveryApiCalls = earlyClose.apiCalls;
|
|
@@ -333491,6 +334620,11 @@ Feature explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResul
|
|
|
333491
334620
|
collectedPages: exploreResult.collectedPages,
|
|
333492
334621
|
collectedTransitions: exploreResult.collectedTransitions,
|
|
333493
334622
|
tests: [],
|
|
334623
|
+
// Persist only a FRESHLY-captured recipe — never re-persist a reused cached one
|
|
334624
|
+
// (ctx.authRecipe set) or a late warm run could overwrite/re-verify a newer recipe.
|
|
334625
|
+
authRecipe: ctx.authRecipe ? void 0 : earlyAuthRecipe,
|
|
334626
|
+
observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
|
|
334627
|
+
resolvedAppOrigin: earlyAuthRecipe?.appOrigin ?? void 0,
|
|
333494
334628
|
logs: logs2.join("\n"),
|
|
333495
334629
|
screenshots,
|
|
333496
334630
|
apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
|
|
@@ -333631,6 +334765,7 @@ Generation complete: ${generateResult.tests.length} tests (${generateElapsed}s e
|
|
|
333631
334765
|
const tags = t.tags.join(", ");
|
|
333632
334766
|
logs2.push(` ${i + 1}. ${t.name} [${tags}]${t.verified ? " \u2713live" : ""}`);
|
|
333633
334767
|
});
|
|
334768
|
+
logAuthGenerationStats(logs2, generateResult.tests, authRecipe);
|
|
333634
334769
|
onLog?.("generate", logs2.join("\n"));
|
|
333635
334770
|
const postGenClose = await handle.close();
|
|
333636
334771
|
handle = null;
|
|
@@ -346391,7 +347526,6 @@ var require_repo_analysis_runner = __commonJS({
|
|
|
346391
347526
|
const softTestTarget = intEnv("REPO_ANALYSIS_SOFT_TEST_TARGET", 20, 1, 50);
|
|
346392
347527
|
const maxEndpointsPerFeature = intEnv("REPO_ANALYSIS_MAX_ENDPOINTS_PER_FEATURE", 80, 1, 400);
|
|
346393
347528
|
const maxFacts = intEnv("REPO_ANALYSIS_MAX_FACTS", 40, 1, 120);
|
|
346394
|
-
const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
|
|
346395
347529
|
const provider = "anthropic";
|
|
346396
347530
|
const model = agentic_cli_constants_js_1.CLAUDE_CODE_DEFAULT_MODEL;
|
|
346397
347531
|
log2(`=== Repo Analysis Runner (Claude CLI, no browser${isMobileAnalysis ? ", mobile\u2192Appium" : ", web\u2192Playwright"}) ===`);
|
|
@@ -348095,7 +349229,7 @@ var require_package4 = __commonJS({
|
|
|
348095
349229
|
"package.json"(exports, module) {
|
|
348096
349230
|
module.exports = {
|
|
348097
349231
|
name: "@validate.qa/runner",
|
|
348098
|
-
version: "1.0.
|
|
349232
|
+
version: "1.0.54",
|
|
348099
349233
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
348100
349234
|
bin: {
|
|
348101
349235
|
"validate-runner": "dist/cli.js"
|