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