@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.mjs
CHANGED
|
@@ -1772,7 +1772,12 @@ var init_constants = __esm({
|
|
|
1772
1772
|
"VALIDATE_TASK_ARN",
|
|
1773
1773
|
"VALIDATE_RUNNER_AUTH",
|
|
1774
1774
|
"VALIDATE_SERVER_URL",
|
|
1775
|
-
"VALIDATE_LIVE_BROWSER_STATE_PATH"
|
|
1775
|
+
"VALIDATE_LIVE_BROWSER_STATE_PATH",
|
|
1776
|
+
// Register-and-verify test identity (parent-minted per run; see runner-core
|
|
1777
|
+
// test-inbox-broker.ts). A customer variable must never spoof the address a
|
|
1778
|
+
// generated test registers with or the derived account password.
|
|
1779
|
+
"VQA_TEST_INBOX_EMAIL",
|
|
1780
|
+
"VQA_TEST_ACCOUNT_PASSWORD"
|
|
1776
1781
|
]);
|
|
1777
1782
|
SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1778
1783
|
// Runtime essentials - Node.js and Playwright need these to function
|
|
@@ -2258,7 +2263,8 @@ function selectorCandidatesForStep(step) {
|
|
|
2258
2263
|
const hasExplicitResourceId = !!nonEmpty(bundle?.resourceId);
|
|
2259
2264
|
const pushValueCandidates = (value) => {
|
|
2260
2265
|
candidates.push(selectorForValue(value));
|
|
2261
|
-
|
|
2266
|
+
const looksLikeIdToken = /^[A-Za-z_][\w.:-]*$/.test(value);
|
|
2267
|
+
if (!hasExplicitResourceId && looksLikeIdToken && !/^(~|id=|css=|\/|\()/.test(value) && !value.includes(":id/")) {
|
|
2262
2268
|
candidates.push(`id=${value}`);
|
|
2263
2269
|
}
|
|
2264
2270
|
};
|
|
@@ -3868,8 +3874,9 @@ function inlineLocatorBindings(stmts) {
|
|
|
3868
3874
|
function stripLoginScaffolding(code) {
|
|
3869
3875
|
const out = [];
|
|
3870
3876
|
let inAuth = false;
|
|
3877
|
+
let inInbox = false;
|
|
3871
3878
|
for (const ln of code.split("\n")) {
|
|
3872
|
-
if (!inAuth && AUTH_START.test(ln)) {
|
|
3879
|
+
if (!inAuth && !inInbox && AUTH_START.test(ln)) {
|
|
3873
3880
|
inAuth = true;
|
|
3874
3881
|
continue;
|
|
3875
3882
|
}
|
|
@@ -3878,6 +3885,15 @@ function stripLoginScaffolding(code) {
|
|
|
3878
3885
|
inAuth = false;
|
|
3879
3886
|
continue;
|
|
3880
3887
|
}
|
|
3888
|
+
if (!inInbox && INBOX_START.test(ln)) {
|
|
3889
|
+
inInbox = true;
|
|
3890
|
+
continue;
|
|
3891
|
+
}
|
|
3892
|
+
if (inInbox) {
|
|
3893
|
+
if (INBOX_END.test(ln))
|
|
3894
|
+
inInbox = false;
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3881
3897
|
if (LOGIN_CALL_LINE.test(ln))
|
|
3882
3898
|
continue;
|
|
3883
3899
|
out.push(ln);
|
|
@@ -4266,7 +4282,7 @@ function validateAssertionStrength(code, opts = {}) {
|
|
|
4266
4282
|
signals
|
|
4267
4283
|
};
|
|
4268
4284
|
}
|
|
4269
|
-
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;
|
|
4285
|
+
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;
|
|
4270
4286
|
var init_assertion_strength = __esm({
|
|
4271
4287
|
"../shared/dist/assertion-strength.js"() {
|
|
4272
4288
|
"use strict";
|
|
@@ -4316,6 +4332,8 @@ var init_assertion_strength = __esm({
|
|
|
4316
4332
|
AUTH_START = /^\s*\/\/\s*>>>\s*VALIDATE_QA_AUTH/;
|
|
4317
4333
|
AUTH_END = /^\s*\/\/\s*<<<\s*VALIDATE_QA_AUTH/;
|
|
4318
4334
|
LOGIN_CALL_LINE = /^\s*await\s+__vqaLogin\s*\(\s*page\s*\)\s*;?\s*$/;
|
|
4335
|
+
INBOX_START = /^\s*\/\/\s*>>>\s*VALIDATE_QA_INBOX/;
|
|
4336
|
+
INBOX_END = /^\s*\/\/\s*<<<\s*VALIDATE_QA_INBOX/;
|
|
4319
4337
|
NEGATED_ASSERT = /\.not\.(toBeVisible|toContainText|toHaveText|toBeChecked)\b|\.toBeHidden\s*\(/;
|
|
4320
4338
|
TYPED_INPUT = /\.(fill|type|pressSequentially|setInputFiles)\s*\(/;
|
|
4321
4339
|
TITLE_LINE = /^\s*(?:test|describe)(?:\.\w+)?\s*\(/;
|
|
@@ -4358,10 +4376,7 @@ function isEnvironmentalError(run2) {
|
|
|
4358
4376
|
return false;
|
|
4359
4377
|
if (run2.status !== "ERROR")
|
|
4360
4378
|
return false;
|
|
4361
|
-
|
|
4362
|
-
if (typeof err !== "string")
|
|
4363
|
-
return false;
|
|
4364
|
-
return ENVIRONMENTAL_ERROR_PREFIXES.some((p) => err.startsWith(p));
|
|
4379
|
+
return classifyErrorMessage(run2.error) !== null;
|
|
4365
4380
|
}
|
|
4366
4381
|
function isCountedFailure(run2) {
|
|
4367
4382
|
if (!run2)
|
|
@@ -4380,9 +4395,10 @@ function environmentalReason(run2) {
|
|
|
4380
4395
|
function classifyErrorMessage(error2) {
|
|
4381
4396
|
if (typeof error2 !== "string")
|
|
4382
4397
|
return null;
|
|
4383
|
-
|
|
4398
|
+
const normalized = error2.replace(/^\s+/, "").replace(/^Error:\s*/i, "");
|
|
4399
|
+
if (normalized.startsWith("RATE_LIMIT_BLOCKED:"))
|
|
4384
4400
|
return "rate_limit";
|
|
4385
|
-
if (
|
|
4401
|
+
if (normalized.startsWith("CONFIG_ISSUE:"))
|
|
4386
4402
|
return "config_issue";
|
|
4387
4403
|
return null;
|
|
4388
4404
|
}
|
|
@@ -7788,12 +7804,16 @@ var require_mobile_keyboard = __commonJS({
|
|
|
7788
7804
|
"use strict";
|
|
7789
7805
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7790
7806
|
exports.shouldRetryTapAfterKeyboardDismiss = shouldRetryTapAfterKeyboardDismiss;
|
|
7807
|
+
exports.hasFocusedEditableElement = hasFocusedEditableElement;
|
|
7791
7808
|
exports.dismissKeyboardForRetry = dismissKeyboardForRetry;
|
|
7792
7809
|
var EDITABLE_FOCUSED_ROLE_RE = /(EditText|TextField|SecureTextField|SearchField|TextView)$/i;
|
|
7793
7810
|
var OUTSIDE_VIEWPORT_ERROR_RE = /(outside|out of).*?(bounds|viewport|visible|screen|window)|coordinate.*?(invalid|outside)/i;
|
|
7794
7811
|
function shouldRetryTapAfterKeyboardDismiss(error2, elements) {
|
|
7795
7812
|
if (!error2 || !OUTSIDE_VIEWPORT_ERROR_RE.test(error2))
|
|
7796
7813
|
return false;
|
|
7814
|
+
return hasFocusedEditableElement(elements);
|
|
7815
|
+
}
|
|
7816
|
+
function hasFocusedEditableElement(elements) {
|
|
7797
7817
|
return elements.some((el) => el.focused === true && EDITABLE_FOCUSED_ROLE_RE.test(el.role));
|
|
7798
7818
|
}
|
|
7799
7819
|
async function dismissKeyboardForRetry(driver) {
|
|
@@ -72600,7 +72620,7 @@ var require_verification_inbox = __commonJS({
|
|
|
72600
72620
|
};
|
|
72601
72621
|
})();
|
|
72602
72622
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
72603
|
-
exports.VerificationInboxReader = void 0;
|
|
72623
|
+
exports.VerificationInboxReader = exports.InboxAuthError = void 0;
|
|
72604
72624
|
exports.isVerificationInboxConfigured = isVerificationInboxConfigured;
|
|
72605
72625
|
exports.describeVerificationInboxConfigGap = describeVerificationInboxConfigGap;
|
|
72606
72626
|
exports.loadVerificationInboxConfig = loadVerificationInboxConfig;
|
|
@@ -72612,8 +72632,10 @@ var require_verification_inbox = __commonJS({
|
|
|
72612
72632
|
exports.applyPlusTag = applyPlusTag;
|
|
72613
72633
|
exports.deriveRegistrationPassword = deriveRegistrationPassword;
|
|
72614
72634
|
exports.extractVerificationCode = extractVerificationCode;
|
|
72635
|
+
exports.extractVerificationCodeDetailed = extractVerificationCodeDetailed;
|
|
72615
72636
|
exports.extractVerificationLink = extractVerificationLink;
|
|
72616
72637
|
exports.collectMessageUrls = collectMessageUrls;
|
|
72638
|
+
exports.isInboxAuthFailure = isInboxAuthFailure;
|
|
72617
72639
|
var node_crypto_1 = __require("crypto");
|
|
72618
72640
|
var DEFAULT_MAILBOX = "INBOX";
|
|
72619
72641
|
var DEFAULT_GMAIL_SPAM_MAILBOX = "[Gmail]/Spam";
|
|
@@ -72786,6 +72808,9 @@ var require_verification_inbox = __commonJS({
|
|
|
72786
72808
|
var CODE_TOKEN = new RegExp(`\\b(${CODE_ALT})\\b`, "gi");
|
|
72787
72809
|
var YEAR_CONTEXT = /(©|\(c\)|copyright|all rights reserved|®)/i;
|
|
72788
72810
|
function extractVerificationCode(text, html, subject) {
|
|
72811
|
+
return extractVerificationCodeDetailed(text, html, subject).code;
|
|
72812
|
+
}
|
|
72813
|
+
function extractVerificationCodeDetailed(text, html, subject) {
|
|
72789
72814
|
const subj = collapseSpacedDigits(subject || "");
|
|
72790
72815
|
const bodies = [text || "", stripHtml(html || "")].map(collapseSpacedDigits);
|
|
72791
72816
|
const withSubject = [subj, ...bodies];
|
|
@@ -72794,7 +72819,7 @@ var require_verification_inbox = __commonJS({
|
|
|
72794
72819
|
if (lone) {
|
|
72795
72820
|
const c = cleanCode(lone[1]);
|
|
72796
72821
|
if (c)
|
|
72797
|
-
return c;
|
|
72822
|
+
return { code: c, confidence: "subject" };
|
|
72798
72823
|
}
|
|
72799
72824
|
}
|
|
72800
72825
|
for (const body of withSubject) {
|
|
@@ -72806,26 +72831,28 @@ var require_verification_inbox = __commonJS({
|
|
|
72806
72831
|
continue;
|
|
72807
72832
|
const m = matchBestCode(line);
|
|
72808
72833
|
if (m)
|
|
72809
|
-
return m;
|
|
72834
|
+
return { code: m, confidence: "keyword" };
|
|
72810
72835
|
}
|
|
72811
72836
|
const windowed = body.match(new RegExp(`${CODE_KEYWORDS.source}[\\s\\S]{0,40}?\\b(${CODE_ALT})\\b`, "i"));
|
|
72812
72837
|
if (windowed && windowed[1]) {
|
|
72813
72838
|
const cleaned = cleanCode(windowed[1]);
|
|
72814
72839
|
if (cleaned && !looksLikeYearOrDate(cleaned))
|
|
72815
|
-
return cleaned;
|
|
72840
|
+
return { code: cleaned, confidence: "keyword" };
|
|
72816
72841
|
}
|
|
72817
72842
|
}
|
|
72843
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
72818
72844
|
for (const body of bodies) {
|
|
72819
72845
|
if (!body)
|
|
72820
72846
|
continue;
|
|
72821
|
-
const
|
|
72822
|
-
|
|
72823
|
-
|
|
72824
|
-
|
|
72825
|
-
if (real)
|
|
72826
|
-
return real;
|
|
72847
|
+
for (const n of body.match(/\b[0-9]{4,8}\b/g) ?? []) {
|
|
72848
|
+
if (!looksLikeYearOrDate(n))
|
|
72849
|
+
distinct.add(n);
|
|
72850
|
+
}
|
|
72827
72851
|
}
|
|
72828
|
-
|
|
72852
|
+
if (distinct.size === 1) {
|
|
72853
|
+
return { code: distinct.values().next().value ?? null, confidence: "fallback" };
|
|
72854
|
+
}
|
|
72855
|
+
return { code: null, confidence: null };
|
|
72829
72856
|
}
|
|
72830
72857
|
function matchBestCode(line) {
|
|
72831
72858
|
const hasYearContext = YEAR_CONTEXT.test(line);
|
|
@@ -72947,6 +72974,28 @@ var require_verification_inbox = __commonJS({
|
|
|
72947
72974
|
}
|
|
72948
72975
|
var MAX_WAIT_CEILING_MS = 18e4;
|
|
72949
72976
|
var MIN_POLL_MS = 2e3;
|
|
72977
|
+
var InboxAuthError = class extends Error {
|
|
72978
|
+
constructor(message) {
|
|
72979
|
+
super(message);
|
|
72980
|
+
this.name = "InboxAuthError";
|
|
72981
|
+
}
|
|
72982
|
+
};
|
|
72983
|
+
exports.InboxAuthError = InboxAuthError;
|
|
72984
|
+
function isInboxAuthFailure(error2) {
|
|
72985
|
+
if (!error2)
|
|
72986
|
+
return false;
|
|
72987
|
+
if (error2 instanceof InboxAuthError)
|
|
72988
|
+
return true;
|
|
72989
|
+
const e = error2;
|
|
72990
|
+
if (e.authenticationFailed === true)
|
|
72991
|
+
return true;
|
|
72992
|
+
const hay = `${e.responseText ?? ""} ${e.message ?? ""}`;
|
|
72993
|
+
return /\bAUTHENTICATIONFAILED\b|\bInvalid credentials\b/i.test(hay);
|
|
72994
|
+
}
|
|
72995
|
+
function isPlusTagged(address) {
|
|
72996
|
+
const at = address.indexOf("@");
|
|
72997
|
+
return at > 0 && address.slice(0, at).includes("+");
|
|
72998
|
+
}
|
|
72950
72999
|
var VerificationInboxReader = class {
|
|
72951
73000
|
config;
|
|
72952
73001
|
client = null;
|
|
@@ -72976,7 +73025,14 @@ var require_verification_inbox = __commonJS({
|
|
|
72976
73025
|
});
|
|
72977
73026
|
client.on("error", () => {
|
|
72978
73027
|
});
|
|
72979
|
-
|
|
73028
|
+
try {
|
|
73029
|
+
await client.connect();
|
|
73030
|
+
} catch (error2) {
|
|
73031
|
+
if (isInboxAuthFailure(error2)) {
|
|
73032
|
+
throw new InboxAuthError(`IMAP authentication rejected for ${this.config.user} \u2014 the inbox app password is revoked or wrong`);
|
|
73033
|
+
}
|
|
73034
|
+
throw error2;
|
|
73035
|
+
}
|
|
72980
73036
|
this.client = client;
|
|
72981
73037
|
return client;
|
|
72982
73038
|
})();
|
|
@@ -73021,7 +73077,10 @@ var require_verification_inbox = __commonJS({
|
|
|
73021
73077
|
} finally {
|
|
73022
73078
|
lock.release();
|
|
73023
73079
|
}
|
|
73024
|
-
} catch {
|
|
73080
|
+
} catch (error2) {
|
|
73081
|
+
if (isInboxAuthFailure(error2)) {
|
|
73082
|
+
throw error2 instanceof InboxAuthError ? error2 : new InboxAuthError(`IMAP authentication rejected for ${this.config.user} \u2014 the inbox app password is revoked or wrong`);
|
|
73083
|
+
}
|
|
73025
73084
|
return { uid: 0, uidValidity: 0 };
|
|
73026
73085
|
}
|
|
73027
73086
|
}
|
|
@@ -73072,23 +73131,52 @@ var require_verification_inbox = __commonJS({
|
|
|
73072
73131
|
}
|
|
73073
73132
|
}
|
|
73074
73133
|
/**
|
|
73075
|
-
* Poll until a
|
|
73076
|
-
*
|
|
73134
|
+
* Poll until a message with a STRONG verification signal (subject/keyword
|
|
73135
|
+
* code or a positive-scoring link) arrives, or the deadline passes.
|
|
73136
|
+
*
|
|
73137
|
+
* Signal-less mail addressed to the same recipient (a welcome email, a
|
|
73138
|
+
* receipt) is remembered but polled PAST — returning it immediately used to
|
|
73139
|
+
* hand welcome-mail order numbers to the caller as "the code" whenever the
|
|
73140
|
+
* welcome email out-raced the verification email. Only at the deadline is
|
|
73141
|
+
* the newest signal-less message surfaced (better than pretending the
|
|
73142
|
+
* mailbox is empty: the caller can still read it and decide).
|
|
73077
73143
|
*/
|
|
73078
73144
|
async waitForMessage(opts) {
|
|
73079
73145
|
const maxWaitMs = Math.min(opts.maxWaitMs ?? 9e4, MAX_WAIT_CEILING_MS);
|
|
73080
73146
|
const pollIntervalMs = Math.max(opts.pollIntervalMs ?? 5e3, MIN_POLL_MS);
|
|
73081
73147
|
const deadline = startTime() + maxWaitMs;
|
|
73148
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
|
|
73149
|
+
const satisfies = (m) => {
|
|
73150
|
+
if (opts.require === "code")
|
|
73151
|
+
return m.code !== null && m.codeConfidence !== "fallback";
|
|
73152
|
+
if (opts.require === "link")
|
|
73153
|
+
return m.link !== null;
|
|
73154
|
+
return m.hasVerificationSignal;
|
|
73155
|
+
};
|
|
73156
|
+
let weakest = null;
|
|
73157
|
+
const check2 = async () => {
|
|
73158
|
+
if (opts.stats)
|
|
73159
|
+
opts.stats.filteredOut = 0;
|
|
73160
|
+
const messages = await this.fetchMatching({ ...opts, limit }, true);
|
|
73161
|
+
if (!opts.require)
|
|
73162
|
+
return messages[0] ?? null;
|
|
73163
|
+
const strong = messages.find((m) => satisfies(m) && !m.senderMismatch) ?? messages.find(satisfies);
|
|
73164
|
+
if (strong)
|
|
73165
|
+
return strong;
|
|
73166
|
+
if (messages[0])
|
|
73167
|
+
weakest = messages[0];
|
|
73168
|
+
return null;
|
|
73169
|
+
};
|
|
73082
73170
|
do {
|
|
73083
|
-
const
|
|
73084
|
-
if (
|
|
73085
|
-
return
|
|
73171
|
+
const found = await check2();
|
|
73172
|
+
if (found)
|
|
73173
|
+
return found;
|
|
73086
73174
|
const remaining = deadline - startTime();
|
|
73087
73175
|
if (remaining <= 0)
|
|
73088
73176
|
break;
|
|
73089
73177
|
await sleep(Math.min(pollIntervalMs, remaining));
|
|
73090
73178
|
} while (startTime() < deadline);
|
|
73091
|
-
return
|
|
73179
|
+
return await check2() ?? weakest;
|
|
73092
73180
|
}
|
|
73093
73181
|
async fetchMatching(opts, withBody) {
|
|
73094
73182
|
const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
|
|
@@ -73102,7 +73190,7 @@ var require_verification_inbox = __commonJS({
|
|
|
73102
73190
|
throw e;
|
|
73103
73191
|
}
|
|
73104
73192
|
}
|
|
73105
|
-
results.sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
73193
|
+
results.sort((a, b) => Number(a.senderMismatch ?? false) - Number(b.senderMismatch ?? false) || Date.parse(b.date) - Date.parse(a.date));
|
|
73106
73194
|
return results.slice(0, limit);
|
|
73107
73195
|
}
|
|
73108
73196
|
async fetchMatchingInMailbox(opts, withBody, mailbox, limit) {
|
|
@@ -73110,6 +73198,7 @@ var require_verification_inbox = __commonJS({
|
|
|
73110
73198
|
const since = new Date(Math.max(0, opts.sinceMs));
|
|
73111
73199
|
const lock = await client.getMailboxLock(mailbox);
|
|
73112
73200
|
const results = [];
|
|
73201
|
+
const taggedRecipient = isPlusTagged(opts.toAddress);
|
|
73113
73202
|
try {
|
|
73114
73203
|
try {
|
|
73115
73204
|
await client.noop();
|
|
@@ -73153,17 +73242,23 @@ var require_verification_inbox = __commonJS({
|
|
|
73153
73242
|
const receivedMs = (parsed.date ?? msg.envelope?.date ?? /* @__PURE__ */ new Date(0)).getTime();
|
|
73154
73243
|
if (!useUid && receivedMs < opts.sinceMs - 6e4)
|
|
73155
73244
|
continue;
|
|
73156
|
-
|
|
73245
|
+
const filterActive = Boolean(opts.fromMatch || opts.subjectMatch);
|
|
73246
|
+
const filterMatched = (!opts.fromMatch || matchField(parsed.from?.text, opts.fromMatch)) && (!opts.subjectMatch || matchField(parsed.subject, opts.subjectMatch));
|
|
73247
|
+
if (filterActive && !filterMatched && !taggedRecipient) {
|
|
73248
|
+
if (opts.stats)
|
|
73249
|
+
opts.stats.filteredOut += 1;
|
|
73157
73250
|
continue;
|
|
73158
|
-
|
|
73159
|
-
|
|
73160
|
-
results.push({ uid: msg.uid, parsed, receivedMs });
|
|
73251
|
+
}
|
|
73252
|
+
results.push({ uid: msg.uid, parsed, receivedMs, senderMismatch: filterActive && !filterMatched });
|
|
73161
73253
|
}
|
|
73162
73254
|
} finally {
|
|
73163
73255
|
lock.release();
|
|
73164
73256
|
}
|
|
73165
|
-
results.sort((a, b) => b.receivedMs - a.receivedMs);
|
|
73166
|
-
return results.slice(0, limit).map((r) =>
|
|
73257
|
+
results.sort((a, b) => Number(a.senderMismatch) - Number(b.senderMismatch) || b.receivedMs - a.receivedMs);
|
|
73258
|
+
return results.slice(0, limit).map((r) => ({
|
|
73259
|
+
...toMessage(messageId(mailbox, String(r.uid), this.config.mailbox), r.parsed, withBody, mailbox),
|
|
73260
|
+
...r.senderMismatch ? { senderMismatch: true } : {}
|
|
73261
|
+
}));
|
|
73167
73262
|
}
|
|
73168
73263
|
async close() {
|
|
73169
73264
|
const client = this.client;
|
|
@@ -73194,12 +73289,18 @@ var require_verification_inbox = __commonJS({
|
|
|
73194
73289
|
const text = parsed.text || "";
|
|
73195
73290
|
const html = typeof parsed.html === "string" ? parsed.html : "";
|
|
73196
73291
|
const summary = toSummary({ uid, parsed, mailbox });
|
|
73292
|
+
const { code, confidence } = extractVerificationCodeDetailed(text, html, parsed.subject || "");
|
|
73293
|
+
const link = extractVerificationLink(text, html);
|
|
73197
73294
|
return {
|
|
73198
73295
|
...summary,
|
|
73199
73296
|
text: withBody ? text : "",
|
|
73200
73297
|
html: withBody ? html : "",
|
|
73201
|
-
code
|
|
73202
|
-
link
|
|
73298
|
+
code,
|
|
73299
|
+
link,
|
|
73300
|
+
codeConfidence: confidence,
|
|
73301
|
+
// A fallback-confidence code is NOT a signal: it is exactly the shape a
|
|
73302
|
+
// welcome/receipt email produces (one stray number, no keyword context).
|
|
73303
|
+
hasVerificationSignal: code !== null && confidence !== "fallback" || link !== null
|
|
73203
73304
|
};
|
|
73204
73305
|
}
|
|
73205
73306
|
function toSummary(input) {
|
|
@@ -73397,7 +73498,11 @@ var require_inbox_tools = __commonJS({
|
|
|
73397
73498
|
function err(message) {
|
|
73398
73499
|
return { content: JSON.stringify({ error: message }), didMutate: false };
|
|
73399
73500
|
}
|
|
73501
|
+
var INBOX_AUTH_DEAD_GUIDANCE = "the inbox authentication is PERMANENTLY rejected (revoked/wrong app password) \u2014 STOP calling inbox tools; none of them can succeed this run. Treat email verification as UNAVAILABLE: hard-stop at any emailed code/link wall, NEVER type a guessed code, and report verification-gated flows as BLOCKED (blockedReason: inbox authentication failed). Continue with publicly reachable coverage.";
|
|
73400
73502
|
async function dispatchInboxTool(toolName, toolArgs, ctx) {
|
|
73503
|
+
if (ctx.state.authDead) {
|
|
73504
|
+
return err(`${toolName} skipped: ${INBOX_AUTH_DEAD_GUIDANCE}`);
|
|
73505
|
+
}
|
|
73401
73506
|
try {
|
|
73402
73507
|
switch (toolName) {
|
|
73403
73508
|
case "provision_identity":
|
|
@@ -73419,6 +73524,10 @@ var require_inbox_tools = __commonJS({
|
|
|
73419
73524
|
}
|
|
73420
73525
|
} catch (e) {
|
|
73421
73526
|
const message = e instanceof Error ? e.message : String(e);
|
|
73527
|
+
if ((0, verification_inbox_js_1.isInboxAuthFailure)(e)) {
|
|
73528
|
+
ctx.state.authDead = true;
|
|
73529
|
+
return err(`${toolName} failed: ${INBOX_AUTH_DEAD_GUIDANCE}`);
|
|
73530
|
+
}
|
|
73422
73531
|
return err(`${toolName} failed: ${message}`);
|
|
73423
73532
|
}
|
|
73424
73533
|
}
|
|
@@ -73476,6 +73585,7 @@ var require_inbox_tools = __commonJS({
|
|
|
73476
73585
|
return notReady;
|
|
73477
73586
|
const maxWaitSeconds = clampNumber(args.maxWaitSeconds, 90, 5, 180);
|
|
73478
73587
|
const { toAddress, altAddress } = inboxRecipients(ctx.state);
|
|
73588
|
+
const stats = { filteredOut: 0 };
|
|
73479
73589
|
const msg = await ctx.reader.waitForMessage({
|
|
73480
73590
|
toAddress,
|
|
73481
73591
|
altAddress,
|
|
@@ -73484,13 +73594,16 @@ var require_inbox_tools = __commonJS({
|
|
|
73484
73594
|
sinceUidValidity: ctx.state.sinceUidValidity,
|
|
73485
73595
|
fromMatch: asStr(args.senderMatch),
|
|
73486
73596
|
subjectMatch: asStr(args.subjectMatch),
|
|
73487
|
-
maxWaitMs: maxWaitSeconds * 1e3
|
|
73597
|
+
maxWaitMs: maxWaitSeconds * 1e3,
|
|
73598
|
+
stats
|
|
73488
73599
|
});
|
|
73489
73600
|
if (!msg) {
|
|
73601
|
+
const filterHint = stats.filteredOut > 0 ? ` NOTE: ${stats.filteredOut} email(s) addressed to you WERE found but were excluded by your senderMatch/subjectMatch filter \u2014 transactional mail often comes from a delivery provider (mailgun/sendgrid/amazonses), so call wait_for_verification again WITHOUT senderMatch.` : "";
|
|
73490
73602
|
return ok({
|
|
73491
73603
|
found: false,
|
|
73492
73604
|
waitedSeconds: maxWaitSeconds,
|
|
73493
|
-
|
|
73605
|
+
...stats.filteredOut > 0 ? { filteredOut: stats.filteredOut } : {},
|
|
73606
|
+
hint: `No verification email yet \u2014 usually still in transit, not necessarily a failure.${filterHint} Call wait_for_verification AGAIN (safe to repeat; allow 30-90s total) and you may click an in-app "Resend" control AT MOST ONCE. Do NOT re-submit the signup form, do NOT create another account, and do not immediately assume your credentials are wrong \u2014 your account most likely already exists. BUT if you have already re-polled several times AND resent once and still nothing arrives, the signup may have genuinely failed: check the page for an error/validation message and report it (finish / needs-review) rather than re-polling forever \u2014 and still do not re-register. Use check_inbox to see what has arrived.`
|
|
73494
73607
|
}, ctx.maxResultChars);
|
|
73495
73608
|
}
|
|
73496
73609
|
rememberLink(ctx.state, msg);
|
|
@@ -73502,6 +73615,8 @@ var require_inbox_tools = __commonJS({
|
|
|
73502
73615
|
subject: msg.subject,
|
|
73503
73616
|
from: msg.from,
|
|
73504
73617
|
code: msg.code,
|
|
73618
|
+
...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." } : {},
|
|
73619
|
+
...msg.senderMismatch ? { senderMismatch: true, senderNote: "This email is addressed to your exact registration address but its sender did not match your senderMatch \u2014 the recipient match is authoritative for your plus-tagged address." } : {},
|
|
73505
73620
|
link,
|
|
73506
73621
|
next: msg.code && link ? "Both a code and a confirmation link were found. Prefer typing the code into the verification field; if the app instead wants you to click, call open_verification_link with the link (and if the page it lands on then asks for the code, enter the code there)." : msg.code ? "Type this code into the verification field." : link ? "Call open_verification_link with this link to confirm. It navigates your current tab to the verify page; after it confirms, return to the app to keep exploring." : msg.link ? "A confirmation link was found but points at a private/internal host and was withheld for safety. Read the email body if you need to inspect it." : "No code/link auto-detected \u2014 call read_verification_email to read the body."
|
|
73507
73622
|
}, ctx.maxResultChars);
|
|
@@ -73635,20 +73750,42 @@ var require_inbox_tools = __commonJS({
|
|
|
73635
73750
|
return false;
|
|
73636
73751
|
if (host === "::1" || host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd"))
|
|
73637
73752
|
return false;
|
|
73638
|
-
|
|
73639
|
-
|
|
73640
|
-
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
73641
|
-
if (a === 127 || a === 0 || a === 10)
|
|
73753
|
+
if (host.includes(":")) {
|
|
73754
|
+
if (host === "::" || host === "::ffff:0:0")
|
|
73642
73755
|
return false;
|
|
73643
|
-
|
|
73644
|
-
|
|
73645
|
-
if (a === 192 && b === 168)
|
|
73646
|
-
return false;
|
|
73647
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
73648
|
-
return false;
|
|
73649
|
-
if (a >= 224)
|
|
73756
|
+
const dotted = host.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
73757
|
+
if (dotted && !ipv4IsPublic(dotted[1]))
|
|
73650
73758
|
return false;
|
|
73759
|
+
const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
73760
|
+
if (mapped) {
|
|
73761
|
+
const hi = parseInt(mapped[1], 16);
|
|
73762
|
+
const lo = parseInt(mapped[2], 16);
|
|
73763
|
+
const ipv4 = `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
73764
|
+
if (!ipv4IsPublic(ipv4))
|
|
73765
|
+
return false;
|
|
73766
|
+
}
|
|
73651
73767
|
}
|
|
73768
|
+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) && !ipv4IsPublic(host))
|
|
73769
|
+
return false;
|
|
73770
|
+
return true;
|
|
73771
|
+
}
|
|
73772
|
+
function ipv4IsPublic(ipv4) {
|
|
73773
|
+
const m = ipv4.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
73774
|
+
if (!m)
|
|
73775
|
+
return true;
|
|
73776
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
73777
|
+
if (a > 255 || b > 255 || Number(m[3]) > 255 || Number(m[4]) > 255)
|
|
73778
|
+
return false;
|
|
73779
|
+
if (a === 127 || a === 0 || a === 10)
|
|
73780
|
+
return false;
|
|
73781
|
+
if (a === 169 && b === 254)
|
|
73782
|
+
return false;
|
|
73783
|
+
if (a === 192 && b === 168)
|
|
73784
|
+
return false;
|
|
73785
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
73786
|
+
return false;
|
|
73787
|
+
if (a >= 224)
|
|
73788
|
+
return false;
|
|
73652
73789
|
return true;
|
|
73653
73790
|
}
|
|
73654
73791
|
function clampNumber(v, dflt, min, max) {
|
|
@@ -75298,6 +75435,9 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75298
75435
|
let totalBreadthContinuationGranted = 0;
|
|
75299
75436
|
let progressMarkAtLastBreadthContinuation = null;
|
|
75300
75437
|
let lastPostSubmitConfirmationToken = null;
|
|
75438
|
+
let keyboardRecoveryScreenId = null;
|
|
75439
|
+
let keyboardRecoverySameScreenTurns = 0;
|
|
75440
|
+
let lastKeyboardRecoveryIteration = -Infinity;
|
|
75301
75441
|
const maybeGrantBreadthContinuation = (iteration, breadthCoverage) => {
|
|
75302
75442
|
if (mode !== "survey" || breadthCoverage.complete || iteration < Math.floor(maxIterations * 0.8))
|
|
75303
75443
|
return 0;
|
|
@@ -75368,6 +75508,29 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75368
75508
|
log2(` Post-submit confirmation snapshot failed non-fatally: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
75369
75509
|
}
|
|
75370
75510
|
}
|
|
75511
|
+
{
|
|
75512
|
+
const rawScreenId = state2.getCurrentScreenId();
|
|
75513
|
+
const webIdx = rawScreenId ? rawScreenId.indexOf(".web-") : -1;
|
|
75514
|
+
const hashIdx = rawScreenId ? rawScreenId.indexOf("#") : -1;
|
|
75515
|
+
const currentScreenId = rawScreenId && webIdx >= 0 ? `${rawScreenId.slice(0, hashIdx >= 0 && hashIdx < webIdx ? hashIdx : webIdx)}${rawScreenId.slice(webIdx)}` : rawScreenId;
|
|
75516
|
+
if (currentScreenId && currentScreenId === keyboardRecoveryScreenId) {
|
|
75517
|
+
keyboardRecoverySameScreenTurns += 1;
|
|
75518
|
+
} else {
|
|
75519
|
+
keyboardRecoveryScreenId = currentScreenId ?? null;
|
|
75520
|
+
keyboardRecoverySameScreenTurns = 0;
|
|
75521
|
+
}
|
|
75522
|
+
const keyboardLikelyOpen = (0, mobile_keyboard_js_1.hasFocusedEditableElement)(getLatest()?.screen.elements ?? []);
|
|
75523
|
+
if (keyboardRecoverySameScreenTurns >= 4 && keyboardLikelyOpen && iteration - lastKeyboardRecoveryIteration > 4) {
|
|
75524
|
+
lastKeyboardRecoveryIteration = iteration;
|
|
75525
|
+
try {
|
|
75526
|
+
await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
75527
|
+
log2(" Auto-dismissed the on-screen keyboard (several turns stalled on this screen with a focused input).");
|
|
75528
|
+
transientDirectives.push("HARNESS NOTE: the on-screen keyboard was just auto-dismissed because multiple turns stalled on this screen \u2014 controls it occluded (e.g. the form submit button) may now be visible. Call mobile_snapshot, then TAP the commit/submit control instead of re-filling fields.");
|
|
75529
|
+
} catch (error2) {
|
|
75530
|
+
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(error2);
|
|
75531
|
+
}
|
|
75532
|
+
}
|
|
75533
|
+
}
|
|
75371
75534
|
if (iteration > 6 && state2.isStaleProgress()) {
|
|
75372
75535
|
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.");
|
|
75373
75536
|
}
|
|
@@ -79950,10 +80113,34 @@ ${constraints.join("\n")}`);
|
|
|
79950
80113
|
const via = triggerLabel ? ` via ${JSON.stringify(triggerLabel)}` : "";
|
|
79951
80114
|
group.flows?.push(`${nameById.get(transition.fromScreenId) ?? transition.fromScreenId} \u2192 ${nameById.get(transition.toScreenId) ?? transition.toScreenId} (${transition.action}${via})`);
|
|
79952
80115
|
}
|
|
79953
|
-
return [...groups.values()].map(({ realm
|
|
79954
|
-
|
|
79955
|
-
|
|
79956
|
-
|
|
80116
|
+
return [...groups.values()].map(({ realm, ...group }) => {
|
|
80117
|
+
const seenPageKeys = /* @__PURE__ */ new Set();
|
|
80118
|
+
const routes = [];
|
|
80119
|
+
const pages = [];
|
|
80120
|
+
group.routes.forEach((route2, index) => {
|
|
80121
|
+
const webIdx = route2.indexOf(".web-");
|
|
80122
|
+
const hashIdx = route2.indexOf("#");
|
|
80123
|
+
const key = webIdx >= 0 ? `${route2.slice(0, hashIdx >= 0 && hashIdx < webIdx ? hashIdx : webIdx)}${route2.slice(webIdx)}` : route2;
|
|
80124
|
+
if (seenPageKeys.has(key))
|
|
80125
|
+
return;
|
|
80126
|
+
seenPageKeys.add(key);
|
|
80127
|
+
routes.push(route2);
|
|
80128
|
+
const page = group.pages?.[index];
|
|
80129
|
+
if (page)
|
|
80130
|
+
pages.push(page);
|
|
80131
|
+
});
|
|
80132
|
+
const flows = [...new Set(group.flows ?? [])];
|
|
80133
|
+
const screenNames = [...new Set(routes.map((route2) => nameById.get(route2) ?? route2))];
|
|
80134
|
+
const preview = screenNames.slice(0, 3).join(", ");
|
|
80135
|
+
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)` : ""}.`;
|
|
80136
|
+
return {
|
|
80137
|
+
...group,
|
|
80138
|
+
routes,
|
|
80139
|
+
pages,
|
|
80140
|
+
description,
|
|
80141
|
+
flows
|
|
80142
|
+
};
|
|
80143
|
+
});
|
|
79957
80144
|
}
|
|
79958
80145
|
function normalizeMobileSurveyFeatureGroups(groups, graph, appId, existingFeatureNames = []) {
|
|
79959
80146
|
const orderedScreens = plannableTargetAppScreens(graph, appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration || a.screenId.localeCompare(b.screenId));
|
|
@@ -82135,6 +82322,9 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82135
82322
|
function weakAssertionTargetReason(element) {
|
|
82136
82323
|
if (isAlwaysPresentContainer(element))
|
|
82137
82324
|
return "always-present app/system container";
|
|
82325
|
+
const hasIdentitySignal = Boolean(nonEmpty3(element.accessibilityId) || nonEmpty3(element.resourceId) || nonEmpty3(element.text) || nonEmpty3(element.label));
|
|
82326
|
+
if (!hasIdentitySignal)
|
|
82327
|
+
return "no identity locator (accessibility id / resource id / text) for the assert gate";
|
|
82138
82328
|
const structuralIdentity = [
|
|
82139
82329
|
element.accessibilityId,
|
|
82140
82330
|
element.resourceId,
|
|
@@ -83637,6 +83827,94 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
83637
83827
|
function detectInvalidMobileAuthSetup(steps, error2) {
|
|
83638
83828
|
return detectInvalidExternalAuthSetup(steps, error2) ?? detectInvalidAuthValidationSetup(steps, error2);
|
|
83639
83829
|
}
|
|
83830
|
+
var IDENTITY_GATE_ERROR_RE = /No identity locator \(accessibility id \/ resource id \/ text\) available for this step/;
|
|
83831
|
+
function isIdentityEqualityXpath(value) {
|
|
83832
|
+
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
83833
|
+
}
|
|
83834
|
+
function stepLacksIdentityAnchor(step) {
|
|
83835
|
+
const metadata = step.metadata;
|
|
83836
|
+
if (metadata && (metadata.appContext === "WEBVIEW" || typeof metadata.webCss === "string"))
|
|
83837
|
+
return false;
|
|
83838
|
+
const target = step.target;
|
|
83839
|
+
if (!target)
|
|
83840
|
+
return true;
|
|
83841
|
+
const bundle = target.locatorBundle;
|
|
83842
|
+
if (bundle?.accessibilityId || bundle?.resourceId)
|
|
83843
|
+
return false;
|
|
83844
|
+
if (bundle?.text?.trim() || bundle?.contentDesc?.trim())
|
|
83845
|
+
return false;
|
|
83846
|
+
if (bundle?.xpath && isIdentityEqualityXpath(bundle.xpath))
|
|
83847
|
+
return false;
|
|
83848
|
+
for (const raw of [target.primary, ...target.fallbacks ?? []]) {
|
|
83849
|
+
if (typeof raw !== "string")
|
|
83850
|
+
continue;
|
|
83851
|
+
const value = raw.trim();
|
|
83852
|
+
if (!value)
|
|
83853
|
+
continue;
|
|
83854
|
+
if (value.startsWith("css="))
|
|
83855
|
+
continue;
|
|
83856
|
+
if (value.startsWith("~") && value.length > 1)
|
|
83857
|
+
return false;
|
|
83858
|
+
if (value.startsWith("id=") && value.length > 3)
|
|
83859
|
+
return false;
|
|
83860
|
+
if (value.startsWith("//") || value.startsWith("(")) {
|
|
83861
|
+
if (isIdentityEqualityXpath(value))
|
|
83862
|
+
return false;
|
|
83863
|
+
continue;
|
|
83864
|
+
}
|
|
83865
|
+
if (value.startsWith("/"))
|
|
83866
|
+
continue;
|
|
83867
|
+
return false;
|
|
83868
|
+
}
|
|
83869
|
+
return true;
|
|
83870
|
+
}
|
|
83871
|
+
async function tryIdentityGateStaticRepair(steps, target, ctx, options) {
|
|
83872
|
+
if (!ctx.error || !IDENTITY_GATE_ERROR_RE.test(ctx.error))
|
|
83873
|
+
return null;
|
|
83874
|
+
const identityless = steps.filter(stepLacksIdentityAnchor);
|
|
83875
|
+
if (identityless.length === 0)
|
|
83876
|
+
return null;
|
|
83877
|
+
const isAssert = (step) => /^assert/i.test(step.action);
|
|
83878
|
+
if (!identityless.every(isAssert)) {
|
|
83879
|
+
return {
|
|
83880
|
+
passed: false,
|
|
83881
|
+
verdict: "unhealable_dynamic",
|
|
83882
|
+
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"
|
|
83883
|
+
};
|
|
83884
|
+
}
|
|
83885
|
+
const pruned = steps.filter((step) => !stepLacksIdentityAnchor(step));
|
|
83886
|
+
if (pruned.length === 0 || !pruned.some(isAssert) || !options.runNativeTest) {
|
|
83887
|
+
return {
|
|
83888
|
+
passed: false,
|
|
83889
|
+
verdict: "unhealable_dynamic",
|
|
83890
|
+
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"
|
|
83891
|
+
};
|
|
83892
|
+
}
|
|
83893
|
+
const reindexed = pruned.map((step, order) => ({ ...step, order }));
|
|
83894
|
+
try {
|
|
83895
|
+
const verify = await options.runNativeTest({ steps: reindexed, target, testVariables: ctx.testVariables });
|
|
83896
|
+
if (!verify.passed)
|
|
83897
|
+
return null;
|
|
83898
|
+
let healedAppiumCode;
|
|
83899
|
+
if (ctx.testName) {
|
|
83900
|
+
try {
|
|
83901
|
+
healedAppiumCode = (0, shared_1.buildAppiumCode)(ctx.testName, toDeterministicTarget(target), reindexed);
|
|
83902
|
+
} catch {
|
|
83903
|
+
}
|
|
83904
|
+
}
|
|
83905
|
+
return {
|
|
83906
|
+
passed: true,
|
|
83907
|
+
verdict: "fix",
|
|
83908
|
+
healedSteps: reindexed,
|
|
83909
|
+
...healedAppiumCode ? { healedAppiumCode } : {},
|
|
83910
|
+
healReason: `identity gate: dropped ${identityless.length} assert step(s) recorded without any identity locator; pruned test verified green in one replay`,
|
|
83911
|
+
stepResults: verify.stepResults,
|
|
83912
|
+
logs: verify.logs
|
|
83913
|
+
};
|
|
83914
|
+
} catch {
|
|
83915
|
+
return null;
|
|
83916
|
+
}
|
|
83917
|
+
}
|
|
83640
83918
|
function stripCodeFences(text) {
|
|
83641
83919
|
const trimmed = text.trim();
|
|
83642
83920
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/m.exec(trimmed);
|
|
@@ -83977,7 +84255,17 @@ ${args.directive}`);
|
|
|
83977
84255
|
break;
|
|
83978
84256
|
}
|
|
83979
84257
|
}
|
|
84258
|
+
if (options.deadlineAt && Date.now() >= options.deadlineAt) {
|
|
84259
|
+
return {
|
|
84260
|
+
passed: false,
|
|
84261
|
+
verdict: "config_issue",
|
|
84262
|
+
healReason: `mobile heal deadline exceeded during Phase 1 (after ${attempt} attempt(s)) \u2014 aborting before further model turns`,
|
|
84263
|
+
attemptsUsed: attempt,
|
|
84264
|
+
lastError
|
|
84265
|
+
};
|
|
84266
|
+
}
|
|
83980
84267
|
attempt += 1;
|
|
84268
|
+
options.onProgress?.(`Phase 1 attempt ${attempt}/${attemptCap} \u2014 asking the model for a script fix...`);
|
|
83981
84269
|
const userContent = buildUserContent({
|
|
83982
84270
|
attempt,
|
|
83983
84271
|
baseSteps,
|
|
@@ -84004,6 +84292,7 @@ ${args.directive}`);
|
|
|
84004
84292
|
}), { label: `mobile-heal-attempt-${attempt}`, deferTimeoutUntilArmed: true });
|
|
84005
84293
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
84006
84294
|
} catch {
|
|
84295
|
+
options.onProgress?.(`Phase 1 attempt ${attempt}: model call failed in transport \u2014 granting a recovery turn`);
|
|
84007
84296
|
markRecoverableTurn();
|
|
84008
84297
|
pendingDirective = "The previous attempt did not reach the model. Re-analyze and return one valid JSON verdict.";
|
|
84009
84298
|
continue;
|
|
@@ -84178,6 +84467,9 @@ ${args.directive}`);
|
|
|
84178
84467
|
const preflight = await tryRegistryPreflight(steps, target, ctx, options);
|
|
84179
84468
|
if (preflight)
|
|
84180
84469
|
return preflight;
|
|
84470
|
+
const identityRepair = await tryIdentityGateStaticRepair(steps, target, ctx, options);
|
|
84471
|
+
if (identityRepair)
|
|
84472
|
+
return identityRepair;
|
|
84181
84473
|
const script = await executeMobileScriptDrivenHeal(steps, target, ctx, options);
|
|
84182
84474
|
if (script.providerUnavailable) {
|
|
84183
84475
|
const fallback = await tryDeterministicProviderFallback({ steps, target, ctx, options });
|
|
@@ -84351,6 +84643,14 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
84351
84643
|
let bestStepResults;
|
|
84352
84644
|
let aiResponseCount = 0;
|
|
84353
84645
|
for (let pass = 1; pass <= maxPasses; pass += 1) {
|
|
84646
|
+
if (options.deadlineAt && Date.now() >= options.deadlineAt) {
|
|
84647
|
+
return {
|
|
84648
|
+
passed: false,
|
|
84649
|
+
verdict: "config_issue",
|
|
84650
|
+
healReason: `mobile heal deadline exceeded during live (Phase 2) healing at pass ${pass}/${maxPasses}`
|
|
84651
|
+
};
|
|
84652
|
+
}
|
|
84653
|
+
options.onProgress?.(`Phase 2 pass ${pass}/${maxPasses} \u2014 observing the live screen...`);
|
|
84354
84654
|
let snap;
|
|
84355
84655
|
try {
|
|
84356
84656
|
snap = await liveDriver.snapshot();
|
|
@@ -85554,12 +85854,14 @@ var require_run_callback_proxy = __commonJS({
|
|
|
85554
85854
|
}
|
|
85555
85855
|
const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
|
|
85556
85856
|
const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
|
|
85857
|
+
const localPaths = new Map(Object.entries(input.localRoutes ?? {}).map(([suffix, handler]) => [`${runPath}/${suffix}`, handler]));
|
|
85557
85858
|
const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
|
|
85558
85859
|
const authHeader = `Bearer ${token}`;
|
|
85559
85860
|
const server3 = (0, node_http_1.createServer)(async (req, res) => {
|
|
85560
85861
|
try {
|
|
85561
85862
|
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
85562
|
-
|
|
85863
|
+
const localHandler = localPaths.get(requestUrl.pathname);
|
|
85864
|
+
if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) && !localHandler || requestUrl.search) {
|
|
85563
85865
|
sendJson(res, 404, { error: "Callback route not available" });
|
|
85564
85866
|
return;
|
|
85565
85867
|
}
|
|
@@ -85578,6 +85880,11 @@ var require_run_callback_proxy = __commonJS({
|
|
|
85578
85880
|
sendJson(res, 400, { error: "Callback body must be a JSON object" });
|
|
85579
85881
|
return;
|
|
85580
85882
|
}
|
|
85883
|
+
if (localHandler) {
|
|
85884
|
+
const out = await localHandler(parsedBody);
|
|
85885
|
+
sendJson(res, out.status, out.body);
|
|
85886
|
+
return;
|
|
85887
|
+
}
|
|
85581
85888
|
const upstreamBody = JSON.stringify({
|
|
85582
85889
|
...parsedBody,
|
|
85583
85890
|
...input.taskArn ? { taskArn: input.taskArn } : {},
|
|
@@ -85636,6 +85943,668 @@ var require_run_callback_proxy = __commonJS({
|
|
|
85636
85943
|
}
|
|
85637
85944
|
});
|
|
85638
85945
|
|
|
85946
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
85947
|
+
var require_discovery_lint_flags = __commonJS({
|
|
85948
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
85949
|
+
"use strict";
|
|
85950
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
85951
|
+
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
85952
|
+
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
85953
|
+
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
85954
|
+
exports.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
85955
|
+
function strictOn(value) {
|
|
85956
|
+
return value === "true" || value === "1";
|
|
85957
|
+
}
|
|
85958
|
+
function structuralLocatorLintMode() {
|
|
85959
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
85960
|
+
if (v === "warn")
|
|
85961
|
+
return "warn";
|
|
85962
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
85963
|
+
return "enforce";
|
|
85964
|
+
return "off";
|
|
85965
|
+
}
|
|
85966
|
+
function isPwMethodPreflightEnabled() {
|
|
85967
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
85968
|
+
}
|
|
85969
|
+
function isVqaLoginDedupeEnabled() {
|
|
85970
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
85971
|
+
}
|
|
85972
|
+
function isTestgenEmailVerifyEnabled() {
|
|
85973
|
+
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
85974
|
+
}
|
|
85975
|
+
}
|
|
85976
|
+
});
|
|
85977
|
+
|
|
85978
|
+
// ../runner-core/dist/services/auth-recipe.js
|
|
85979
|
+
var require_auth_recipe = __commonJS({
|
|
85980
|
+
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
85981
|
+
"use strict";
|
|
85982
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
85983
|
+
exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = void 0;
|
|
85984
|
+
exports.computeRecipeHash = computeRecipeHash;
|
|
85985
|
+
exports.buildAuthRecipe = buildAuthRecipe;
|
|
85986
|
+
exports.authProofLocator = authProofLocator;
|
|
85987
|
+
exports.urlProofPredicateExpr = urlProofPredicateExpr;
|
|
85988
|
+
exports.renderAuthProofAssertion = renderAuthProofAssertion;
|
|
85989
|
+
exports.renderProofBaselineCheck = renderProofBaselineCheck;
|
|
85990
|
+
exports.renderAuthFn = renderAuthFn;
|
|
85991
|
+
exports.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
|
|
85992
|
+
exports.absoluteRoute = absoluteRoute;
|
|
85993
|
+
exports.renderReplayClickArrow = renderReplayClickArrow;
|
|
85994
|
+
exports.renderReplaySettleArrow = renderReplaySettleArrow;
|
|
85995
|
+
exports.renderAuthUrlWaitLine = renderAuthUrlWaitLine;
|
|
85996
|
+
exports.commandToReplayStep = commandToReplayStep;
|
|
85997
|
+
exports.buildReplayPlan = buildReplayPlan;
|
|
85998
|
+
exports.extractAuthRegion = extractAuthRegion;
|
|
85999
|
+
exports.stripAuthRegions = stripAuthRegions;
|
|
86000
|
+
exports.ensureExpectImported = ensureExpectImported;
|
|
86001
|
+
exports.insertAfterPwImport = insertAfterPwImport;
|
|
86002
|
+
exports.ensureLoginCall = ensureLoginCall;
|
|
86003
|
+
exports.upsertAuthBlock = upsertAuthBlock;
|
|
86004
|
+
exports.buildHealAuthStamp = buildHealAuthStamp;
|
|
86005
|
+
var crypto_1 = __require("crypto");
|
|
86006
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
86007
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
86008
|
+
exports.AUTH_MARKER_VERSION = "v3";
|
|
86009
|
+
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
86010
|
+
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
|
|
86011
|
+
var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
|
|
86012
|
+
var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
|
|
86013
|
+
function computeRecipeHash(input) {
|
|
86014
|
+
const canonical = JSON.stringify({
|
|
86015
|
+
preNavRoute: input.preNavRoute,
|
|
86016
|
+
preNavCmds: input.preNavCmds,
|
|
86017
|
+
route: input.route,
|
|
86018
|
+
cmds: input.cmds,
|
|
86019
|
+
redirectRoute: input.redirectRoute,
|
|
86020
|
+
authProof: input.authProof
|
|
86021
|
+
});
|
|
86022
|
+
return (0, crypto_1.createHash)("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
86023
|
+
}
|
|
86024
|
+
function buildAuthRecipe(login, authProof, verifiedAt) {
|
|
86025
|
+
const preNavRoute = login.preNavRoute ?? "/";
|
|
86026
|
+
const preNavCmds = login.preNavCmds ?? [];
|
|
86027
|
+
const recipeHash = computeRecipeHash({
|
|
86028
|
+
preNavRoute,
|
|
86029
|
+
preNavCmds,
|
|
86030
|
+
route: login.route,
|
|
86031
|
+
cmds: login.cmds,
|
|
86032
|
+
redirectRoute: login.redirectRoute,
|
|
86033
|
+
authProof
|
|
86034
|
+
});
|
|
86035
|
+
return {
|
|
86036
|
+
version: 1,
|
|
86037
|
+
recipeHash,
|
|
86038
|
+
preNavRoute,
|
|
86039
|
+
preNavCmds,
|
|
86040
|
+
route: login.route,
|
|
86041
|
+
cmds: login.cmds,
|
|
86042
|
+
redirectRoute: login.redirectRoute,
|
|
86043
|
+
authProof,
|
|
86044
|
+
replayPlan: login.replayPlan ?? [],
|
|
86045
|
+
verifiedAt
|
|
86046
|
+
};
|
|
86047
|
+
}
|
|
86048
|
+
function authProofLocator(proof) {
|
|
86049
|
+
if (proof.kind === "url" || proof.kind === "endpoint") {
|
|
86050
|
+
throw new Error(`authProofLocator: ${proof.kind} proof has no element locator; callers must branch on proof.kind`);
|
|
86051
|
+
}
|
|
86052
|
+
if (proof.kind === "testid") {
|
|
86053
|
+
return `page.getByTestId(${JSON.stringify(proof.testId)})`;
|
|
86054
|
+
}
|
|
86055
|
+
if (proof.name) {
|
|
86056
|
+
return `page.getByRole(${JSON.stringify(proof.role)}, { name: ${JSON.stringify(proof.name)} })`;
|
|
86057
|
+
}
|
|
86058
|
+
return `page.getByRole(${JSON.stringify(proof.role)}).first()`;
|
|
86059
|
+
}
|
|
86060
|
+
function stripToPathname(route2) {
|
|
86061
|
+
return route2.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/";
|
|
86062
|
+
}
|
|
86063
|
+
function urlProofPredicateExpr(route2, exact = false) {
|
|
86064
|
+
const p = stripToPathname(route2);
|
|
86065
|
+
if (p === "/")
|
|
86066
|
+
return `() => false`;
|
|
86067
|
+
const r = JSON.stringify(p);
|
|
86068
|
+
if (exact)
|
|
86069
|
+
return `(u) => u.pathname === ${r} || u.pathname === ${r} + '/'`;
|
|
86070
|
+
return `(u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/')`;
|
|
86071
|
+
}
|
|
86072
|
+
function renderStableExactUrlProof(pred, tabs) {
|
|
86073
|
+
return [
|
|
86074
|
+
`await page.waitForURL(${pred}, { timeout: 15000 });`,
|
|
86075
|
+
`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)');`,
|
|
86076
|
+
`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');`
|
|
86077
|
+
].join(`
|
|
86078
|
+
${tabs}`);
|
|
86079
|
+
}
|
|
86080
|
+
var ENDPOINT_ORIGIN_EXPR = `(page.url().match(/^https?:\\/\\/[^/]+/) || [''])[0]`;
|
|
86081
|
+
function renderEndpointProbe(url, tabs, mode, originMode) {
|
|
86082
|
+
const u = JSON.stringify(url);
|
|
86083
|
+
const target = originMode === "pageurl" ? `${ENDPOINT_ORIGIN_EXPR} + ${u}` : u;
|
|
86084
|
+
const lines = [
|
|
86085
|
+
`const __vqaResp = await page.request.get(${target});`,
|
|
86086
|
+
`const __vqaJson = __vqaResp.ok() ? await __vqaResp.json().catch(() => null) : null;`,
|
|
86087
|
+
`const __vqaUser = __vqaJson && (__vqaJson.user ?? __vqaJson.data?.user ?? __vqaJson);`,
|
|
86088
|
+
`const __vqaAuthed = !!(__vqaUser && (__vqaUser.id || __vqaUser.email || __vqaUser.sub));`,
|
|
86089
|
+
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');`
|
|
86090
|
+
];
|
|
86091
|
+
return lines.join(`
|
|
86092
|
+
${tabs}`);
|
|
86093
|
+
}
|
|
86094
|
+
function renderAuthProofAssertion(proof) {
|
|
86095
|
+
if (proof.kind === "endpoint") {
|
|
86096
|
+
return renderEndpointProbe(proof.url, " ", "authed", "baseurl");
|
|
86097
|
+
}
|
|
86098
|
+
if (proof.kind === "url") {
|
|
86099
|
+
const pred = urlProofPredicateExpr(proof.route, proof.exact);
|
|
86100
|
+
if (proof.exact)
|
|
86101
|
+
return renderStableExactUrlProof(pred, " ");
|
|
86102
|
+
return `await page.waitForURL(${pred}, { timeout: 15000 });`;
|
|
86103
|
+
}
|
|
86104
|
+
return `await expect(${authProofLocator(proof)}).toBeVisible({ timeout: 15000 });`;
|
|
86105
|
+
}
|
|
86106
|
+
var BASELINE_VISIBLE_TIMEOUT_MS = 15e3;
|
|
86107
|
+
function renderProofBaselineCheck(proof, tabs) {
|
|
86108
|
+
if (proof.kind === "endpoint") {
|
|
86109
|
+
return renderEndpointProbe(proof.url, tabs, "anon", "baseurl");
|
|
86110
|
+
}
|
|
86111
|
+
if (proof.kind === "url") {
|
|
86112
|
+
const pred = urlProofPredicateExpr(proof.route, proof.exact);
|
|
86113
|
+
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');`;
|
|
86114
|
+
}
|
|
86115
|
+
const loc = authProofLocator(proof);
|
|
86116
|
+
return [
|
|
86117
|
+
`if (await ${loc}.waitFor({ state: 'visible', timeout: ${BASELINE_VISIBLE_TIMEOUT_MS} }).then(() => true, () => false)) {`,
|
|
86118
|
+
` throw new Error('__vqa auth-baseline: authed-only proof visible while logged out \u2014 not a discriminating proof');`,
|
|
86119
|
+
`}`
|
|
86120
|
+
].join(`
|
|
86121
|
+
${tabs}`);
|
|
86122
|
+
}
|
|
86123
|
+
function normalizeCmd(cmd) {
|
|
86124
|
+
let c = cmd.trim();
|
|
86125
|
+
if (c.endsWith(";"))
|
|
86126
|
+
c = c.slice(0, -1);
|
|
86127
|
+
return ` ${c};`;
|
|
86128
|
+
}
|
|
86129
|
+
function renderAuthFn(recipe, opts) {
|
|
86130
|
+
const lines = [];
|
|
86131
|
+
lines.push(START_MARKER);
|
|
86132
|
+
lines.push(`async function __vqaLogin(page) {`);
|
|
86133
|
+
if (opts?.warmGuard) {
|
|
86134
|
+
const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
|
|
86135
|
+
const loginRoute = recipe.route ?? recipe.preNavRoute;
|
|
86136
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
|
|
86137
|
+
lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
|
|
86138
|
+
if (recipe.authProof.kind === "endpoint") {
|
|
86139
|
+
lines.push(` try {`);
|
|
86140
|
+
lines.push(` ${renderEndpointProbe(recipe.authProof.url, " ", "authed", "baseurl")}`);
|
|
86141
|
+
lines.push(` return;`);
|
|
86142
|
+
lines.push(` } catch {}`);
|
|
86143
|
+
} else if (recipe.authProof.kind === "url") {
|
|
86144
|
+
const pred = urlProofPredicateExpr(recipe.authProof.route, recipe.authProof.exact);
|
|
86145
|
+
lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
|
|
86146
|
+
lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
|
|
86147
|
+
} else if (loginRoute) {
|
|
86148
|
+
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
|
|
86149
|
+
} else {
|
|
86150
|
+
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
|
|
86151
|
+
}
|
|
86152
|
+
lines.push(` }`);
|
|
86153
|
+
}
|
|
86154
|
+
lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
|
|
86155
|
+
for (const cmd of recipe.preNavCmds)
|
|
86156
|
+
lines.push(normalizeCmd(cmd));
|
|
86157
|
+
if (recipe.preNavCmds.length === 0 && recipe.route && recipe.route !== recipe.preNavRoute) {
|
|
86158
|
+
lines.push(` await page.goto(${JSON.stringify(recipe.route)}, { waitUntil: 'domcontentloaded' });`);
|
|
86159
|
+
}
|
|
86160
|
+
for (const cmd of recipe.cmds)
|
|
86161
|
+
lines.push(normalizeCmd(cmd));
|
|
86162
|
+
lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
|
|
86163
|
+
lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
|
|
86164
|
+
if (opts?.warmGuard) {
|
|
86165
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
|
|
86166
|
+
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) {`);
|
|
86167
|
+
lines.push(` try {`);
|
|
86168
|
+
lines.push(` const __vqaState = await page.context().storageState();`);
|
|
86169
|
+
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 });`);
|
|
86170
|
+
lines.push(` } catch {}`);
|
|
86171
|
+
lines.push(` }`);
|
|
86172
|
+
}
|
|
86173
|
+
lines.push(`}`);
|
|
86174
|
+
lines.push(END_MARKER);
|
|
86175
|
+
return lines.join("\n");
|
|
86176
|
+
}
|
|
86177
|
+
exports.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
|
|
86178
|
+
"AUTH_FILE",
|
|
86179
|
+
"SHARED_AUTH_INJECT",
|
|
86180
|
+
"VALIDATE_RUN_ID",
|
|
86181
|
+
"VALIDATE_CLAIM_EPOCH",
|
|
86182
|
+
"VALIDATE_AUTH_CONFIG_HASH",
|
|
86183
|
+
"VALIDATE_TASK_ARN",
|
|
86184
|
+
"VALIDATE_RUNNER_AUTH",
|
|
86185
|
+
"VALIDATE_SERVER_URL",
|
|
86186
|
+
// Register-and-verify tests (VALIDATE_QA_INBOX region, inbox-stamp.ts): the
|
|
86187
|
+
// per-run minted registration address + its DERIVED throwaway-app password,
|
|
86188
|
+
// injected by the parent's test-inbox-broker — never project variables.
|
|
86189
|
+
"VQA_TEST_INBOX_EMAIL",
|
|
86190
|
+
"VQA_TEST_ACCOUNT_PASSWORD"
|
|
86191
|
+
]);
|
|
86192
|
+
function isReservedRunnerEnvVar(key) {
|
|
86193
|
+
return exports.RESERVED_RUNNER_ENV_VARS.has(key);
|
|
86194
|
+
}
|
|
86195
|
+
function absoluteRoute(route2, baseUrl) {
|
|
86196
|
+
try {
|
|
86197
|
+
return new URL(route2, baseUrl).href;
|
|
86198
|
+
} catch {
|
|
86199
|
+
return route2;
|
|
86200
|
+
}
|
|
86201
|
+
}
|
|
86202
|
+
function renderReplayClickArrow(step) {
|
|
86203
|
+
const loc = step.locator.trim();
|
|
86204
|
+
if (step.kind === "wait") {
|
|
86205
|
+
return `async (page) => {
|
|
86206
|
+
await ${loc}.waitFor({ state: 'visible', timeout: 15000 });
|
|
86207
|
+
}`;
|
|
86208
|
+
}
|
|
86209
|
+
if (step.kind === "press") {
|
|
86210
|
+
return `async (page) => {
|
|
86211
|
+
await ${loc}.press(${JSON.stringify(step.key ?? "Enter")});
|
|
86212
|
+
}`;
|
|
86213
|
+
}
|
|
86214
|
+
return `async (page) => {
|
|
86215
|
+
await ${loc}.click();
|
|
86216
|
+
}`;
|
|
86217
|
+
}
|
|
86218
|
+
function renderReplaySettleArrow(proof) {
|
|
86219
|
+
if (proof.kind === "endpoint") {
|
|
86220
|
+
return `async (page) => {
|
|
86221
|
+
${renderEndpointProbe(proof.url, " ", "authed", "pageurl")}
|
|
86222
|
+
}`;
|
|
86223
|
+
}
|
|
86224
|
+
if (proof.kind === "url") {
|
|
86225
|
+
const pred = urlProofPredicateExpr(proof.route, proof.exact);
|
|
86226
|
+
const body = proof.exact ? renderStableExactUrlProof(pred, " ") : `await page.waitForURL(${pred}, { timeout: 15000 });`;
|
|
86227
|
+
return `async (page) => {
|
|
86228
|
+
${body}
|
|
86229
|
+
}`;
|
|
86230
|
+
}
|
|
86231
|
+
return `async (page) => {
|
|
86232
|
+
await ${authProofLocator(proof)}.waitFor({ state: 'visible', timeout: 15000 });
|
|
86233
|
+
}`;
|
|
86234
|
+
}
|
|
86235
|
+
function renderAuthUrlWaitLine(redirectRoute) {
|
|
86236
|
+
const opts = `{ waitUntil: 'domcontentloaded', timeout: 8000 }`;
|
|
86237
|
+
const tail = `.catch(() => { /* URL drift \u2014 the auth-proof assertion below is the gate */ });`;
|
|
86238
|
+
const rr = redirectRoute ? stripToPathname(redirectRoute) : null;
|
|
86239
|
+
if (rr && rr !== "/") {
|
|
86240
|
+
const r = JSON.stringify(rr);
|
|
86241
|
+
return ` await page.waitForURL((u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/'), ${opts})${tail}`;
|
|
86242
|
+
}
|
|
86243
|
+
if (rr === "/") {
|
|
86244
|
+
return ` await page.waitForURL((u) => !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
|
|
86245
|
+
}
|
|
86246
|
+
return ` await page.waitForURL((u) => u.pathname !== '/' && !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
|
|
86247
|
+
}
|
|
86248
|
+
function locatorChainOf(cmd) {
|
|
86249
|
+
const m = cmd.match(/\bpage\.[\s\S]*?(?=\.(?:fill|click|check|press|selectOption|setChecked|waitFor)\s*\()/);
|
|
86250
|
+
return m ? m[0].trim() : null;
|
|
86251
|
+
}
|
|
86252
|
+
function parseRoleName(locator) {
|
|
86253
|
+
const role = locator.match(/getByRole\(\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
86254
|
+
const name = locator.match(/getByRole\([^)]*name:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByLabel\(\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByPlaceholder\(\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
86255
|
+
return { role, name };
|
|
86256
|
+
}
|
|
86257
|
+
function credentialKeyOf(cmd) {
|
|
86258
|
+
return cmd.match(/\.fill\(\s*process\.env\.([A-Za-z_$][\w$]*)!?\s*\)/)?.[1] ?? null;
|
|
86259
|
+
}
|
|
86260
|
+
function commandToReplayStep(cmd) {
|
|
86261
|
+
const chain = locatorChainOf(cmd);
|
|
86262
|
+
if (!chain)
|
|
86263
|
+
return null;
|
|
86264
|
+
const credKey = credentialKeyOf(cmd);
|
|
86265
|
+
if (cmd.includes(".fill(")) {
|
|
86266
|
+
if (!credKey)
|
|
86267
|
+
return null;
|
|
86268
|
+
const { role, name } = parseRoleName(chain);
|
|
86269
|
+
return { kind: "fill", credentialKey: credKey, locator: chain, role, name, element: name ?? role ?? "credential field" };
|
|
86270
|
+
}
|
|
86271
|
+
if (/\.waitFor\s*\(/.test(cmd))
|
|
86272
|
+
return { kind: "wait", locator: chain };
|
|
86273
|
+
const pressKey = cmd.match(/\.press\(\s*['"`]([^'"`]+)['"`]\s*\)/)?.[1];
|
|
86274
|
+
if (pressKey)
|
|
86275
|
+
return { kind: "press", locator: chain, key: pressKey };
|
|
86276
|
+
if (/\.(?:click|check|setChecked|selectOption)\s*\(/.test(cmd))
|
|
86277
|
+
return { kind: "click", locator: chain };
|
|
86278
|
+
return null;
|
|
86279
|
+
}
|
|
86280
|
+
function buildReplayPlan(login) {
|
|
86281
|
+
const plan = [];
|
|
86282
|
+
const preNavRoute = login.preNavRoute ?? "/";
|
|
86283
|
+
plan.push({ kind: "nav", route: preNavRoute });
|
|
86284
|
+
for (const c of login.preNavCmds ?? []) {
|
|
86285
|
+
const step = commandToReplayStep(c);
|
|
86286
|
+
if (step)
|
|
86287
|
+
plan.push(step);
|
|
86288
|
+
}
|
|
86289
|
+
if ((login.preNavCmds ?? []).length === 0 && login.route && login.route !== preNavRoute) {
|
|
86290
|
+
plan.push({ kind: "nav", route: login.route });
|
|
86291
|
+
}
|
|
86292
|
+
for (const c of login.cmds) {
|
|
86293
|
+
const step = commandToReplayStep(c);
|
|
86294
|
+
if (step)
|
|
86295
|
+
plan.push(step);
|
|
86296
|
+
}
|
|
86297
|
+
return plan;
|
|
86298
|
+
}
|
|
86299
|
+
function extractAuthRegion(code) {
|
|
86300
|
+
const m = code.match(new RegExp(AUTH_REGION_RE.source));
|
|
86301
|
+
return m ? m[0] : null;
|
|
86302
|
+
}
|
|
86303
|
+
function countAuthRegions(code) {
|
|
86304
|
+
const m = code.match(AUTH_REGION_RE);
|
|
86305
|
+
return m ? m.length : 0;
|
|
86306
|
+
}
|
|
86307
|
+
function stripAuthRegions(code) {
|
|
86308
|
+
return code.replace(AUTH_REGION_RE, "");
|
|
86309
|
+
}
|
|
86310
|
+
var PW_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?/;
|
|
86311
|
+
function ensureExpectImported(code) {
|
|
86312
|
+
const m = code.match(PW_IMPORT_RE);
|
|
86313
|
+
if (!m)
|
|
86314
|
+
return code;
|
|
86315
|
+
const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
86316
|
+
if (names.includes("expect"))
|
|
86317
|
+
return code;
|
|
86318
|
+
const next = [...names, "expect"];
|
|
86319
|
+
return code.replace(PW_IMPORT_RE, `import { ${next.join(", ")} } from '@playwright/test';`);
|
|
86320
|
+
}
|
|
86321
|
+
function insertAfterPwImport(code, block) {
|
|
86322
|
+
const m = code.match(PW_IMPORT_RE);
|
|
86323
|
+
if (!m || m.index === void 0) {
|
|
86324
|
+
return `${block}
|
|
86325
|
+
|
|
86326
|
+
${code.replace(/^\r?\n+/, "")}`;
|
|
86327
|
+
}
|
|
86328
|
+
const end = m.index + m[0].length;
|
|
86329
|
+
const head = code.slice(0, end);
|
|
86330
|
+
const tail = code.slice(end).replace(/^\r?\n+/, "");
|
|
86331
|
+
return `${head}
|
|
86332
|
+
|
|
86333
|
+
${block}
|
|
86334
|
+
|
|
86335
|
+
${tail}`;
|
|
86336
|
+
}
|
|
86337
|
+
var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
|
|
86338
|
+
var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
|
|
86339
|
+
function ensureLoginCall(code) {
|
|
86340
|
+
if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
|
|
86341
|
+
return code;
|
|
86342
|
+
let result = code;
|
|
86343
|
+
if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
|
|
86344
|
+
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
86345
|
+
}
|
|
86346
|
+
const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
|
|
86347
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
86348
|
+
const m = matches[i];
|
|
86349
|
+
if (m.index === void 0)
|
|
86350
|
+
continue;
|
|
86351
|
+
const insertAt = m.index + m[0].length;
|
|
86352
|
+
const following = result.slice(insertAt, insertAt + 60);
|
|
86353
|
+
if (/^\s*await\s+__vqaLogin\s*\(\s*page\s*\)/.test(following))
|
|
86354
|
+
continue;
|
|
86355
|
+
result = `${result.slice(0, insertAt)}
|
|
86356
|
+
await __vqaLogin(page);${result.slice(insertAt)}`;
|
|
86357
|
+
}
|
|
86358
|
+
return result;
|
|
86359
|
+
}
|
|
86360
|
+
function upsertAuthBlock(code, ctx) {
|
|
86361
|
+
if (ctx.mode === "generate") {
|
|
86362
|
+
if (!ctx.recipe || !ctx.isAuthNeeded)
|
|
86363
|
+
return code;
|
|
86364
|
+
let result = stripAuthRegions(code);
|
|
86365
|
+
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
86366
|
+
result = ensureExpectImported(result);
|
|
86367
|
+
const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
|
|
86368
|
+
result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
|
|
86369
|
+
result = ensureLoginCall(result);
|
|
86370
|
+
const n = countAuthRegions(result);
|
|
86371
|
+
if (n !== 1) {
|
|
86372
|
+
throw new Error(`upsertAuthBlock(generate): expected exactly 1 auth region after stamp, got ${n}`);
|
|
86373
|
+
}
|
|
86374
|
+
if (!LOGIN_CALL_RE.test(result)) {
|
|
86375
|
+
throw new Error("upsertAuthBlock(generate): stamped __vqaLogin but found no test() opener to inject the call");
|
|
86376
|
+
}
|
|
86377
|
+
return result;
|
|
86378
|
+
}
|
|
86379
|
+
try {
|
|
86380
|
+
if (!ctx.preservedAuthRegion)
|
|
86381
|
+
return code;
|
|
86382
|
+
let result = ensureExpectImported(code);
|
|
86383
|
+
if (countAuthRegions(result) >= 1) {
|
|
86384
|
+
result = result.replace(AUTH_REGION_RE, "");
|
|
86385
|
+
result = insertAfterPwImport(result, ctx.preservedAuthRegion);
|
|
86386
|
+
} else {
|
|
86387
|
+
result = insertAfterPwImport(result, ctx.preservedAuthRegion);
|
|
86388
|
+
}
|
|
86389
|
+
result = ensureLoginCall(result);
|
|
86390
|
+
if (countAuthRegions(result) !== 1) {
|
|
86391
|
+
throw new Error("heal re-stamp produced non-unique auth region");
|
|
86392
|
+
}
|
|
86393
|
+
return result;
|
|
86394
|
+
} catch (err) {
|
|
86395
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
86396
|
+
ctx.onSignal?.({ type: "auth-stamp-degraded", reason });
|
|
86397
|
+
return code;
|
|
86398
|
+
}
|
|
86399
|
+
}
|
|
86400
|
+
function buildHealAuthStamp(originalCode, failureError, logs2) {
|
|
86401
|
+
const region = extractAuthRegion(originalCode);
|
|
86402
|
+
const loginFailed = !!failureError && /__vqaLogin/.test(failureError);
|
|
86403
|
+
return {
|
|
86404
|
+
mode: "heal",
|
|
86405
|
+
preservedAuthRegion: region && !loginFailed ? region : null,
|
|
86406
|
+
onSignal: logs2 ? (s) => logs2.push(`\u26A0 auth-stamp ${s.type}: ${s.reason}`) : void 0
|
|
86407
|
+
};
|
|
86408
|
+
}
|
|
86409
|
+
}
|
|
86410
|
+
});
|
|
86411
|
+
|
|
86412
|
+
// ../runner-core/dist/services/inbox-stamp.js
|
|
86413
|
+
var require_inbox_stamp = __commonJS({
|
|
86414
|
+
"../runner-core/dist/services/inbox-stamp.js"(exports) {
|
|
86415
|
+
"use strict";
|
|
86416
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86417
|
+
exports.INBOX_HELPER_TOKEN_RE = exports.INBOX_HELPER_NAME = exports.INBOX_REGION_RE = exports.INBOX_MARKER_VERSION = void 0;
|
|
86418
|
+
exports.codeUsesInboxHelper = codeUsesInboxHelper;
|
|
86419
|
+
exports.stripInboxRegions = stripInboxRegions;
|
|
86420
|
+
exports.renderInboxFn = renderInboxFn;
|
|
86421
|
+
exports.upsertInboxBlock = upsertInboxBlock;
|
|
86422
|
+
var auth_recipe_js_1 = require_auth_recipe();
|
|
86423
|
+
exports.INBOX_MARKER_VERSION = "v1";
|
|
86424
|
+
var START_MARKER = `// >>> VALIDATE_QA_INBOX ${exports.INBOX_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
86425
|
+
var END_MARKER = `// <<< VALIDATE_QA_INBOX ${exports.INBOX_MARKER_VERSION} <<<`;
|
|
86426
|
+
exports.INBOX_REGION_RE = /\/\/ >>> VALIDATE_QA_INBOX[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_INBOX[^\n]*\r?\n?/g;
|
|
86427
|
+
exports.INBOX_HELPER_NAME = "__vqaWaitForEmail";
|
|
86428
|
+
exports.INBOX_HELPER_TOKEN_RE = /__vqaWaitForEmail\s*\(/;
|
|
86429
|
+
function codeUsesInboxHelper(code) {
|
|
86430
|
+
if (!code)
|
|
86431
|
+
return false;
|
|
86432
|
+
return exports.INBOX_HELPER_TOKEN_RE.test(code) || /VALIDATE_QA_INBOX/.test(code);
|
|
86433
|
+
}
|
|
86434
|
+
function stripInboxRegions(code) {
|
|
86435
|
+
return code.replace(exports.INBOX_REGION_RE, "");
|
|
86436
|
+
}
|
|
86437
|
+
function countInboxRegions(code) {
|
|
86438
|
+
const m = code.match(exports.INBOX_REGION_RE);
|
|
86439
|
+
return m ? m.length : 0;
|
|
86440
|
+
}
|
|
86441
|
+
function renderInboxFn() {
|
|
86442
|
+
const lines = [
|
|
86443
|
+
START_MARKER,
|
|
86444
|
+
`// Reads THIS run's verification email via the runner's inbox broker. The`,
|
|
86445
|
+
`// IMAP credential stays in the runner parent; this helper can only see mail`,
|
|
86446
|
+
`// addressed to the run's own registration address (VQA_TEST_INBOX_EMAIL).`,
|
|
86447
|
+
`// kind: 'code' \u2192 the one-time code, 'link' \u2192 the verification URL,`,
|
|
86448
|
+
`// 'either' / omitted \u2192 { code, link, subject, from } for flexible flows.`,
|
|
86449
|
+
`async function __vqaWaitForEmail(kind, opts) {`,
|
|
86450
|
+
` const want = kind === 'code' || kind === 'link' ? kind : 'either';`,
|
|
86451
|
+
` if (!process.env.VQA_TEST_INBOX_EMAIL || !process.env.VALIDATE_SERVER_URL || !process.env.VALIDATE_RUN_ID) {`,
|
|
86452
|
+
` throw new Error('CONFIG_ISSUE: email verification unavailable - this runner has no verification inbox, so a register-and-verify test cannot run here');`,
|
|
86453
|
+
` }`,
|
|
86454
|
+
` const timeoutMs = Math.min(Math.max((opts && opts.timeoutMs) || 150000, 5000), 170000);`,
|
|
86455
|
+
` const res = await fetch(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + encodeURIComponent(process.env.VALIDATE_RUN_ID) + '/inbox-wait', {`,
|
|
86456
|
+
` method: 'POST',`,
|
|
86457
|
+
` headers: { authorization: process.env.VALIDATE_RUNNER_AUTH || '', 'content-type': 'application/json' },`,
|
|
86458
|
+
` body: JSON.stringify({ kind: want, timeoutMs: timeoutMs }),`,
|
|
86459
|
+
` });`,
|
|
86460
|
+
` if (res.status === 404) {`,
|
|
86461
|
+
` throw new Error('CONFIG_ISSUE: email verification unavailable - the runner inbox broker is not active for this run');`,
|
|
86462
|
+
` }`,
|
|
86463
|
+
` let data = null;`,
|
|
86464
|
+
` try { data = await res.json(); } catch (e) { data = null; }`,
|
|
86465
|
+
` if (!res.ok || !data) { throw new Error('inbox broker request failed (status ' + res.status + ')'); }`,
|
|
86466
|
+
` if (!data.found) {`,
|
|
86467
|
+
` if (data.error) { throw new Error('CONFIG_ISSUE: verification inbox failed on this runner - ' + data.error); }`,
|
|
86468
|
+
` 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');`,
|
|
86469
|
+
` }`,
|
|
86470
|
+
` if (want === 'code' && !data.code) {`,
|
|
86471
|
+
` throw new Error('VERIFICATION_EMAIL_HAS_NO_CODE: an email arrived (subject: ' + JSON.stringify(data.subject || '') + ') but no one-time code could be extracted');`,
|
|
86472
|
+
` }`,
|
|
86473
|
+
` if (want === 'link' && !data.link) {`,
|
|
86474
|
+
` throw new Error('VERIFICATION_EMAIL_HAS_NO_LINK: an email arrived (subject: ' + JSON.stringify(data.subject || '') + ') but no verification link could be extracted');`,
|
|
86475
|
+
` }`,
|
|
86476
|
+
` return want === 'code' ? data.code : want === 'link' ? data.link : data;`,
|
|
86477
|
+
`}`,
|
|
86478
|
+
END_MARKER
|
|
86479
|
+
];
|
|
86480
|
+
return lines.join("\n");
|
|
86481
|
+
}
|
|
86482
|
+
function upsertInboxBlock(code) {
|
|
86483
|
+
const stripped = stripInboxRegions(code);
|
|
86484
|
+
if (!exports.INBOX_HELPER_TOKEN_RE.test(stripped))
|
|
86485
|
+
return stripped;
|
|
86486
|
+
const result = (0, auth_recipe_js_1.insertAfterPwImport)(stripped, renderInboxFn());
|
|
86487
|
+
const n = countInboxRegions(result);
|
|
86488
|
+
if (n !== 1) {
|
|
86489
|
+
throw new Error(`upsertInboxBlock: expected exactly 1 inbox region after stamp, got ${n}`);
|
|
86490
|
+
}
|
|
86491
|
+
return result;
|
|
86492
|
+
}
|
|
86493
|
+
}
|
|
86494
|
+
});
|
|
86495
|
+
|
|
86496
|
+
// ../runner-core/dist/services/test-inbox-broker.js
|
|
86497
|
+
var require_test_inbox_broker = __commonJS({
|
|
86498
|
+
"../runner-core/dist/services/test-inbox-broker.js"(exports) {
|
|
86499
|
+
"use strict";
|
|
86500
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86501
|
+
exports.INBOX_WAIT_ROUTE = exports.TEST_INBOX_PASSWORD_ENV = exports.TEST_INBOX_EMAIL_ENV = void 0;
|
|
86502
|
+
exports.deriveTestRunInboxTag = deriveTestRunInboxTag;
|
|
86503
|
+
exports.mintTestRunInboxIdentity = mintTestRunInboxIdentity;
|
|
86504
|
+
exports.decideInboxWaitResponse = decideInboxWaitResponse;
|
|
86505
|
+
exports.startTestInboxBroker = startTestInboxBroker;
|
|
86506
|
+
var node_crypto_1 = __require("crypto");
|
|
86507
|
+
var verification_inbox_js_1 = require_verification_inbox();
|
|
86508
|
+
var inbox_tools_js_1 = require_inbox_tools();
|
|
86509
|
+
exports.TEST_INBOX_EMAIL_ENV = "VQA_TEST_INBOX_EMAIL";
|
|
86510
|
+
exports.TEST_INBOX_PASSWORD_ENV = "VQA_TEST_ACCOUNT_PASSWORD";
|
|
86511
|
+
exports.INBOX_WAIT_ROUTE = "inbox-wait";
|
|
86512
|
+
var MIN_WAIT_MS = 5e3;
|
|
86513
|
+
var MAX_WAIT_MS = 17e4;
|
|
86514
|
+
var DEFAULT_WAIT_MS = 15e4;
|
|
86515
|
+
function deriveTestRunInboxTag(seed, projectId, runId) {
|
|
86516
|
+
const hostKey = (0, verification_inbox_js_1.seedToHostKey)(seed || "");
|
|
86517
|
+
const hash = (0, node_crypto_1.createHash)("sha256").update(`testrun|${hostKey}|${projectId ?? ""}|${runId}`).digest("hex").slice(0, 10);
|
|
86518
|
+
return `vqat${hash}`;
|
|
86519
|
+
}
|
|
86520
|
+
function mintTestRunInboxIdentity(config, input) {
|
|
86521
|
+
const tag = deriveTestRunInboxTag(input.baseUrl, input.projectId, input.runId);
|
|
86522
|
+
const email = (0, verification_inbox_js_1.applyPlusTag)(config.address, tag);
|
|
86523
|
+
return { email, password: (0, verification_inbox_js_1.deriveRegistrationPassword)(email), tag };
|
|
86524
|
+
}
|
|
86525
|
+
function decideInboxWaitResponse(msg, publicLink) {
|
|
86526
|
+
const code = msg && msg.code && msg.codeConfidence !== "fallback" ? msg.code : null;
|
|
86527
|
+
const link = publicLink;
|
|
86528
|
+
if (!msg || !code && !link)
|
|
86529
|
+
return { found: false };
|
|
86530
|
+
return { found: true, code, link, subject: msg.subject, from: msg.from };
|
|
86531
|
+
}
|
|
86532
|
+
async function startTestInboxBroker(opts) {
|
|
86533
|
+
const identity = mintTestRunInboxIdentity(opts.config, opts);
|
|
86534
|
+
const reader = new verification_inbox_js_1.VerificationInboxReader(opts.config);
|
|
86535
|
+
const sinceMs = Date.now();
|
|
86536
|
+
let sinceUid = 0;
|
|
86537
|
+
let sinceUidValidity = 0;
|
|
86538
|
+
let authDeadReason = null;
|
|
86539
|
+
try {
|
|
86540
|
+
const wm = await reader.getUidWatermark();
|
|
86541
|
+
sinceUid = wm.uid;
|
|
86542
|
+
sinceUidValidity = wm.uidValidity;
|
|
86543
|
+
} catch (error2) {
|
|
86544
|
+
if ((0, verification_inbox_js_1.isInboxAuthFailure)(error2)) {
|
|
86545
|
+
authDeadReason = error2 instanceof Error ? error2.message : String(error2);
|
|
86546
|
+
opts.log?.(`\u26A0 Inbox broker: IMAP authentication rejected at watermark probe \u2014 email waits will fail as CONFIG_ISSUE (${authDeadReason})`);
|
|
86547
|
+
} else {
|
|
86548
|
+
opts.log?.("\u26A0 Inbox broker: UID watermark unavailable, falling back to the time floor");
|
|
86549
|
+
}
|
|
86550
|
+
}
|
|
86551
|
+
opts.log?.(`\u{1F4EC} Inbox broker active for ${identity.email} (uid watermark ${sinceUid || "none"})`);
|
|
86552
|
+
let waiting = false;
|
|
86553
|
+
const handleWait = async (body) => {
|
|
86554
|
+
if (authDeadReason) {
|
|
86555
|
+
return { status: 200, body: { found: false, error: "inbox authentication failed (revoked/wrong app password)" } };
|
|
86556
|
+
}
|
|
86557
|
+
if (waiting) {
|
|
86558
|
+
return { status: 200, body: { found: false, error: "another verification wait is already in progress for this run" } };
|
|
86559
|
+
}
|
|
86560
|
+
waiting = true;
|
|
86561
|
+
const rawTimeout = typeof body.timeoutMs === "number" && Number.isFinite(body.timeoutMs) ? body.timeoutMs : DEFAULT_WAIT_MS;
|
|
86562
|
+
const timeoutMs = Math.min(Math.max(rawTimeout, MIN_WAIT_MS), MAX_WAIT_MS);
|
|
86563
|
+
const require2 = body.kind === "code" ? "code" : body.kind === "link" ? "link" : "signal";
|
|
86564
|
+
try {
|
|
86565
|
+
const stats = { filteredOut: 0 };
|
|
86566
|
+
const msg = await reader.waitForMessage({
|
|
86567
|
+
toAddress: identity.email,
|
|
86568
|
+
sinceMs,
|
|
86569
|
+
sinceUid,
|
|
86570
|
+
sinceUidValidity,
|
|
86571
|
+
maxWaitMs: timeoutMs,
|
|
86572
|
+
require: require2,
|
|
86573
|
+
stats
|
|
86574
|
+
});
|
|
86575
|
+
const publicLink = msg && msg.link && (0, inbox_tools_js_1.isPublicHttpUrl)(msg.link) ? msg.link : null;
|
|
86576
|
+
const decision = decideInboxWaitResponse(msg, publicLink);
|
|
86577
|
+
if (!decision.found) {
|
|
86578
|
+
opts.log?.(`\u{1F4ED} Inbox broker: no usable verification email for ${identity.tag} within ${Math.round(timeoutMs / 1e3)}s`);
|
|
86579
|
+
return { status: 200, body: { found: false } };
|
|
86580
|
+
}
|
|
86581
|
+
opts.log?.(`\u{1F4EC} Inbox broker: email for ${identity.tag} (code=${decision.code ? "yes" : "no"}, link=${decision.link ? "yes" : "no"})`);
|
|
86582
|
+
return { status: 200, body: { ...decision } };
|
|
86583
|
+
} catch (error2) {
|
|
86584
|
+
if ((0, verification_inbox_js_1.isInboxAuthFailure)(error2)) {
|
|
86585
|
+
authDeadReason = error2 instanceof Error ? error2.message : String(error2);
|
|
86586
|
+
return { status: 200, body: { found: false, error: "inbox authentication failed (revoked/wrong app password)" } };
|
|
86587
|
+
}
|
|
86588
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
86589
|
+
opts.log?.(`\u26A0 Inbox broker wait failed: ${message}`);
|
|
86590
|
+
return { status: 200, body: { found: false, error: `inbox read failed: ${message}` } };
|
|
86591
|
+
} finally {
|
|
86592
|
+
waiting = false;
|
|
86593
|
+
}
|
|
86594
|
+
};
|
|
86595
|
+
return {
|
|
86596
|
+
identity,
|
|
86597
|
+
childEnv: () => ({
|
|
86598
|
+
[exports.TEST_INBOX_EMAIL_ENV]: identity.email,
|
|
86599
|
+
[exports.TEST_INBOX_PASSWORD_ENV]: identity.password
|
|
86600
|
+
}),
|
|
86601
|
+
handleWait,
|
|
86602
|
+
close: () => reader.close()
|
|
86603
|
+
};
|
|
86604
|
+
}
|
|
86605
|
+
}
|
|
86606
|
+
});
|
|
86607
|
+
|
|
85639
86608
|
// ../runner-core/dist/services/playwright-native.js
|
|
85640
86609
|
var require_playwright_native = __commonJS({
|
|
85641
86610
|
"../runner-core/dist/services/playwright-native.js"(exports) {
|
|
@@ -85693,6 +86662,9 @@ var require_playwright_native = __commonJS({
|
|
|
85693
86662
|
var har_stream_js_1 = require_har_stream();
|
|
85694
86663
|
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
85695
86664
|
var run_callback_proxy_js_1 = require_run_callback_proxy();
|
|
86665
|
+
var inbox_stamp_js_1 = require_inbox_stamp();
|
|
86666
|
+
var test_inbox_broker_js_1 = require_test_inbox_broker();
|
|
86667
|
+
var verification_inbox_js_1 = require_verification_inbox();
|
|
85696
86668
|
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
85697
86669
|
Object.defineProperty(exports, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
85698
86670
|
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
@@ -85727,17 +86699,20 @@ var require_playwright_native = __commonJS({
|
|
|
85727
86699
|
let runDir = null;
|
|
85728
86700
|
let standingNeedleMatcher = null;
|
|
85729
86701
|
let callbackProxy;
|
|
86702
|
+
let inboxBroker;
|
|
85730
86703
|
try {
|
|
85731
86704
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
85732
86705
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
85733
86706
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
86707
|
+
const usesInboxHelper = !isApiTest2 && (0, inbox_stamp_js_1.codeUsesInboxHelper)(playwrightCode);
|
|
85734
86708
|
const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
|
|
85735
86709
|
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
85736
86710
|
explicit: extraHttpHeaders,
|
|
85737
86711
|
standingHeadersMode: effectiveStandingMode,
|
|
85738
86712
|
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
85739
86713
|
});
|
|
85740
|
-
|
|
86714
|
+
const needleValues = resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : [];
|
|
86715
|
+
standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(needleValues);
|
|
85741
86716
|
const cssDisableSetup = `
|
|
85742
86717
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
85743
86718
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -85976,7 +86951,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
85976
86951
|
module.exports = {
|
|
85977
86952
|
testDir: ${JSON.stringify(absTestDir)},
|
|
85978
86953
|
outputDir: ${JSON.stringify(absOutputDir)},
|
|
85979
|
-
timeout:
|
|
86954
|
+
timeout: ${usesInboxHelper ? 24e4 : 6e4},
|
|
85980
86955
|
retries: 0,
|
|
85981
86956
|
use: {
|
|
85982
86957
|
baseURL: ${JSON.stringify(baseUrl)},
|
|
@@ -86002,6 +86977,27 @@ module.exports = {
|
|
|
86002
86977
|
const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
|
|
86003
86978
|
const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
|
|
86004
86979
|
const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
|
|
86980
|
+
if (usesInboxHelper && validateContext?.runId) {
|
|
86981
|
+
const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
|
|
86982
|
+
if (!inboxConfig) {
|
|
86983
|
+
logs2.push("\u{1F4EA} Test uses __vqaWaitForEmail but this runner has no verification inbox configured \u2014 the helper will fail as CONFIG_ISSUE");
|
|
86984
|
+
} else {
|
|
86985
|
+
try {
|
|
86986
|
+
inboxBroker = await (0, test_inbox_broker_js_1.startTestInboxBroker)({
|
|
86987
|
+
config: inboxConfig,
|
|
86988
|
+
baseUrl,
|
|
86989
|
+
projectId: options.projectId ?? null,
|
|
86990
|
+
runId: validateContext.runId,
|
|
86991
|
+
log: (line) => logs2.push(line)
|
|
86992
|
+
});
|
|
86993
|
+
standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)([...needleValues, inboxBroker.identity.password]);
|
|
86994
|
+
} catch (error3) {
|
|
86995
|
+
logs2.push(`\u26A0 Inbox broker failed to start: ${error3 instanceof Error ? error3.message : String(error3)}`);
|
|
86996
|
+
}
|
|
86997
|
+
}
|
|
86998
|
+
} else if (usesInboxHelper) {
|
|
86999
|
+
logs2.push("\u{1F4EA} Test uses __vqaWaitForEmail but no run callback context is available \u2014 the helper will fail as CONFIG_ISSUE");
|
|
87000
|
+
}
|
|
86005
87001
|
if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
|
|
86006
87002
|
try {
|
|
86007
87003
|
callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
|
|
@@ -86010,7 +87006,8 @@ module.exports = {
|
|
|
86010
87006
|
upstreamAuthHeader: validateContext.authHeader,
|
|
86011
87007
|
taskArn: process.env.VALIDATE_TASK_ARN,
|
|
86012
87008
|
claimEpoch: callbackClaimEpoch,
|
|
86013
|
-
authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
|
|
87009
|
+
authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH,
|
|
87010
|
+
...inboxBroker ? { localRoutes: { [test_inbox_broker_js_1.INBOX_WAIT_ROUTE]: inboxBroker.handleWait } } : {}
|
|
86014
87011
|
});
|
|
86015
87012
|
} catch (error3) {
|
|
86016
87013
|
logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
|
|
@@ -86037,14 +87034,19 @@ module.exports = {
|
|
|
86037
87034
|
// Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
|
|
86038
87035
|
// both parent and generated __vqaLogin code; "false" must still cross this process
|
|
86039
87036
|
// boundary to disable the early publish POST. It remains a per-run toggle.
|
|
86040
|
-
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
87037
|
+
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {},
|
|
87038
|
+
// Register-and-verify tests: the run's minted registration identity. An
|
|
87039
|
+
// address + a DERIVED throwaway-app password — never the IMAP secret,
|
|
87040
|
+
// which stays in this parent (test-inbox-broker.ts). Useless without the
|
|
87041
|
+
// loopback proxy the helper reads mail through, hence the guard.
|
|
87042
|
+
...inboxBroker && callbackProxy ? inboxBroker.childEnv() : {}
|
|
86041
87043
|
};
|
|
86042
87044
|
delete childTestVariables.API_AUTH_HEADERS;
|
|
86043
87045
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
86044
87046
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
86045
87047
|
let playResult;
|
|
86046
87048
|
try {
|
|
86047
|
-
playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
|
|
87049
|
+
playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv, usesInboxHelper ? 3e5 : void 0);
|
|
86048
87050
|
} finally {
|
|
86049
87051
|
await stopLiveBrowserPolling?.();
|
|
86050
87052
|
}
|
|
@@ -86135,6 +87137,8 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
86135
87137
|
} finally {
|
|
86136
87138
|
await callbackProxy?.close().catch(() => {
|
|
86137
87139
|
});
|
|
87140
|
+
await inboxBroker?.close().catch(() => {
|
|
87141
|
+
});
|
|
86138
87142
|
if (runDir) {
|
|
86139
87143
|
await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
|
|
86140
87144
|
});
|
|
@@ -86186,7 +87190,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
86186
87190
|
stopped = true;
|
|
86187
87191
|
};
|
|
86188
87192
|
}
|
|
86189
|
-
function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
|
|
87193
|
+
function runPlaywright(configPath, testVariables, trustedRuntimeEnv, killAfterMs) {
|
|
86190
87194
|
return new Promise((resolvePromise) => {
|
|
86191
87195
|
const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
|
|
86192
87196
|
const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
|
|
@@ -86204,11 +87208,13 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
86204
87208
|
proc.stderr?.on("data", (chunk) => {
|
|
86205
87209
|
stderr += chunk.toString();
|
|
86206
87210
|
});
|
|
86207
|
-
const
|
|
87211
|
+
const killMs = killAfterMs ?? 12e4;
|
|
87212
|
+
const cancelKill = (0, spawn_with_tree_kill_js_1.scheduleTreeKill)(proc, killMs, {
|
|
86208
87213
|
graceMs: 1e4,
|
|
86209
87214
|
onTimeout: () => {
|
|
86210
87215
|
timedOut = true;
|
|
86211
|
-
stderr +=
|
|
87216
|
+
stderr += `
|
|
87217
|
+
[playwright killed after ${Math.round(killMs / 1e3)}s safety timeout]`;
|
|
86212
87218
|
}
|
|
86213
87219
|
});
|
|
86214
87220
|
const settle = (result) => {
|
|
@@ -89907,402 +90913,6 @@ var require_anchor_harvester = __commonJS({
|
|
|
89907
90913
|
}
|
|
89908
90914
|
});
|
|
89909
90915
|
|
|
89910
|
-
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
89911
|
-
var require_discovery_lint_flags = __commonJS({
|
|
89912
|
-
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
89913
|
-
"use strict";
|
|
89914
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
89915
|
-
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
89916
|
-
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
89917
|
-
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
89918
|
-
function strictOn(value) {
|
|
89919
|
-
return value === "true" || value === "1";
|
|
89920
|
-
}
|
|
89921
|
-
function structuralLocatorLintMode() {
|
|
89922
|
-
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
89923
|
-
if (v === "warn")
|
|
89924
|
-
return "warn";
|
|
89925
|
-
if (v === "true" || v === "1" || v === "enforce")
|
|
89926
|
-
return "enforce";
|
|
89927
|
-
return "off";
|
|
89928
|
-
}
|
|
89929
|
-
function isPwMethodPreflightEnabled() {
|
|
89930
|
-
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
89931
|
-
}
|
|
89932
|
-
function isVqaLoginDedupeEnabled() {
|
|
89933
|
-
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
89934
|
-
}
|
|
89935
|
-
}
|
|
89936
|
-
});
|
|
89937
|
-
|
|
89938
|
-
// ../runner-core/dist/services/auth-recipe.js
|
|
89939
|
-
var require_auth_recipe = __commonJS({
|
|
89940
|
-
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
89941
|
-
"use strict";
|
|
89942
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
89943
|
-
exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = void 0;
|
|
89944
|
-
exports.computeRecipeHash = computeRecipeHash;
|
|
89945
|
-
exports.buildAuthRecipe = buildAuthRecipe;
|
|
89946
|
-
exports.authProofLocator = authProofLocator;
|
|
89947
|
-
exports.urlProofPredicateExpr = urlProofPredicateExpr;
|
|
89948
|
-
exports.renderAuthProofAssertion = renderAuthProofAssertion;
|
|
89949
|
-
exports.renderAuthFn = renderAuthFn;
|
|
89950
|
-
exports.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
|
|
89951
|
-
exports.absoluteRoute = absoluteRoute;
|
|
89952
|
-
exports.renderReplayClickArrow = renderReplayClickArrow;
|
|
89953
|
-
exports.renderReplaySettleArrow = renderReplaySettleArrow;
|
|
89954
|
-
exports.renderAuthUrlWaitLine = renderAuthUrlWaitLine;
|
|
89955
|
-
exports.commandToReplayStep = commandToReplayStep;
|
|
89956
|
-
exports.buildReplayPlan = buildReplayPlan;
|
|
89957
|
-
exports.extractAuthRegion = extractAuthRegion;
|
|
89958
|
-
exports.stripAuthRegions = stripAuthRegions;
|
|
89959
|
-
exports.ensureExpectImported = ensureExpectImported;
|
|
89960
|
-
exports.ensureLoginCall = ensureLoginCall;
|
|
89961
|
-
exports.upsertAuthBlock = upsertAuthBlock;
|
|
89962
|
-
exports.buildHealAuthStamp = buildHealAuthStamp;
|
|
89963
|
-
var crypto_1 = __require("crypto");
|
|
89964
|
-
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
89965
|
-
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
89966
|
-
exports.AUTH_MARKER_VERSION = "v3";
|
|
89967
|
-
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
89968
|
-
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
|
|
89969
|
-
var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
|
|
89970
|
-
var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
|
|
89971
|
-
function computeRecipeHash(input) {
|
|
89972
|
-
const canonical = JSON.stringify({
|
|
89973
|
-
preNavRoute: input.preNavRoute,
|
|
89974
|
-
preNavCmds: input.preNavCmds,
|
|
89975
|
-
route: input.route,
|
|
89976
|
-
cmds: input.cmds,
|
|
89977
|
-
redirectRoute: input.redirectRoute,
|
|
89978
|
-
authProof: input.authProof
|
|
89979
|
-
});
|
|
89980
|
-
return (0, crypto_1.createHash)("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
89981
|
-
}
|
|
89982
|
-
function buildAuthRecipe(login, authProof, verifiedAt) {
|
|
89983
|
-
const preNavRoute = login.preNavRoute ?? "/";
|
|
89984
|
-
const preNavCmds = login.preNavCmds ?? [];
|
|
89985
|
-
const recipeHash = computeRecipeHash({
|
|
89986
|
-
preNavRoute,
|
|
89987
|
-
preNavCmds,
|
|
89988
|
-
route: login.route,
|
|
89989
|
-
cmds: login.cmds,
|
|
89990
|
-
redirectRoute: login.redirectRoute,
|
|
89991
|
-
authProof
|
|
89992
|
-
});
|
|
89993
|
-
return {
|
|
89994
|
-
version: 1,
|
|
89995
|
-
recipeHash,
|
|
89996
|
-
preNavRoute,
|
|
89997
|
-
preNavCmds,
|
|
89998
|
-
route: login.route,
|
|
89999
|
-
cmds: login.cmds,
|
|
90000
|
-
redirectRoute: login.redirectRoute,
|
|
90001
|
-
authProof,
|
|
90002
|
-
replayPlan: login.replayPlan ?? [],
|
|
90003
|
-
verifiedAt
|
|
90004
|
-
};
|
|
90005
|
-
}
|
|
90006
|
-
function authProofLocator(proof) {
|
|
90007
|
-
if (proof.kind === "url") {
|
|
90008
|
-
throw new Error("authProofLocator: url proof has no element locator; callers must branch on proof.kind");
|
|
90009
|
-
}
|
|
90010
|
-
if (proof.kind === "testid") {
|
|
90011
|
-
return `page.getByTestId(${JSON.stringify(proof.testId)})`;
|
|
90012
|
-
}
|
|
90013
|
-
if (proof.name) {
|
|
90014
|
-
return `page.getByRole(${JSON.stringify(proof.role)}, { name: ${JSON.stringify(proof.name)} })`;
|
|
90015
|
-
}
|
|
90016
|
-
return `page.getByRole(${JSON.stringify(proof.role)}).first()`;
|
|
90017
|
-
}
|
|
90018
|
-
function stripToPathname(route2) {
|
|
90019
|
-
return route2.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/";
|
|
90020
|
-
}
|
|
90021
|
-
function urlProofPredicateExpr(route2) {
|
|
90022
|
-
const p = stripToPathname(route2);
|
|
90023
|
-
if (p === "/")
|
|
90024
|
-
return `() => false`;
|
|
90025
|
-
const r = JSON.stringify(p);
|
|
90026
|
-
return `(u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/')`;
|
|
90027
|
-
}
|
|
90028
|
-
function renderAuthProofAssertion(proof) {
|
|
90029
|
-
if (proof.kind === "url") {
|
|
90030
|
-
return `await page.waitForURL(${urlProofPredicateExpr(proof.route)}, { timeout: 15000 });`;
|
|
90031
|
-
}
|
|
90032
|
-
return `await expect(${authProofLocator(proof)}).toBeVisible({ timeout: 15000 });`;
|
|
90033
|
-
}
|
|
90034
|
-
function normalizeCmd(cmd) {
|
|
90035
|
-
let c = cmd.trim();
|
|
90036
|
-
if (c.endsWith(";"))
|
|
90037
|
-
c = c.slice(0, -1);
|
|
90038
|
-
return ` ${c};`;
|
|
90039
|
-
}
|
|
90040
|
-
function renderAuthFn(recipe, opts) {
|
|
90041
|
-
const lines = [];
|
|
90042
|
-
lines.push(START_MARKER);
|
|
90043
|
-
lines.push(`async function __vqaLogin(page) {`);
|
|
90044
|
-
if (opts?.warmGuard) {
|
|
90045
|
-
const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
|
|
90046
|
-
const loginRoute = recipe.route ?? recipe.preNavRoute;
|
|
90047
|
-
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
|
|
90048
|
-
lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
|
|
90049
|
-
if (recipe.authProof.kind === "url") {
|
|
90050
|
-
const pred = urlProofPredicateExpr(recipe.authProof.route);
|
|
90051
|
-
lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
|
|
90052
|
-
lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
|
|
90053
|
-
} else if (loginRoute) {
|
|
90054
|
-
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
|
|
90055
|
-
} else {
|
|
90056
|
-
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
|
|
90057
|
-
}
|
|
90058
|
-
lines.push(` }`);
|
|
90059
|
-
}
|
|
90060
|
-
lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
|
|
90061
|
-
for (const cmd of recipe.preNavCmds)
|
|
90062
|
-
lines.push(normalizeCmd(cmd));
|
|
90063
|
-
if (recipe.preNavCmds.length === 0 && recipe.route && recipe.route !== recipe.preNavRoute) {
|
|
90064
|
-
lines.push(` await page.goto(${JSON.stringify(recipe.route)}, { waitUntil: 'domcontentloaded' });`);
|
|
90065
|
-
}
|
|
90066
|
-
for (const cmd of recipe.cmds)
|
|
90067
|
-
lines.push(normalizeCmd(cmd));
|
|
90068
|
-
lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
|
|
90069
|
-
lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
|
|
90070
|
-
if (opts?.warmGuard) {
|
|
90071
|
-
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
|
|
90072
|
-
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) {`);
|
|
90073
|
-
lines.push(` try {`);
|
|
90074
|
-
lines.push(` const __vqaState = await page.context().storageState();`);
|
|
90075
|
-
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 });`);
|
|
90076
|
-
lines.push(` } catch {}`);
|
|
90077
|
-
lines.push(` }`);
|
|
90078
|
-
}
|
|
90079
|
-
lines.push(`}`);
|
|
90080
|
-
lines.push(END_MARKER);
|
|
90081
|
-
return lines.join("\n");
|
|
90082
|
-
}
|
|
90083
|
-
exports.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
|
|
90084
|
-
"AUTH_FILE",
|
|
90085
|
-
"SHARED_AUTH_INJECT",
|
|
90086
|
-
"VALIDATE_RUN_ID",
|
|
90087
|
-
"VALIDATE_CLAIM_EPOCH",
|
|
90088
|
-
"VALIDATE_AUTH_CONFIG_HASH",
|
|
90089
|
-
"VALIDATE_TASK_ARN",
|
|
90090
|
-
"VALIDATE_RUNNER_AUTH",
|
|
90091
|
-
"VALIDATE_SERVER_URL"
|
|
90092
|
-
]);
|
|
90093
|
-
function isReservedRunnerEnvVar(key) {
|
|
90094
|
-
return exports.RESERVED_RUNNER_ENV_VARS.has(key);
|
|
90095
|
-
}
|
|
90096
|
-
function absoluteRoute(route2, baseUrl) {
|
|
90097
|
-
try {
|
|
90098
|
-
return new URL(route2, baseUrl).href;
|
|
90099
|
-
} catch {
|
|
90100
|
-
return route2;
|
|
90101
|
-
}
|
|
90102
|
-
}
|
|
90103
|
-
function renderReplayClickArrow(step) {
|
|
90104
|
-
const loc = step.locator.trim();
|
|
90105
|
-
if (step.kind === "wait") {
|
|
90106
|
-
return `async (page) => {
|
|
90107
|
-
await ${loc}.waitFor({ state: 'visible', timeout: 15000 });
|
|
90108
|
-
}`;
|
|
90109
|
-
}
|
|
90110
|
-
if (step.kind === "press") {
|
|
90111
|
-
return `async (page) => {
|
|
90112
|
-
await ${loc}.press(${JSON.stringify(step.key ?? "Enter")});
|
|
90113
|
-
}`;
|
|
90114
|
-
}
|
|
90115
|
-
return `async (page) => {
|
|
90116
|
-
await ${loc}.click();
|
|
90117
|
-
}`;
|
|
90118
|
-
}
|
|
90119
|
-
function renderReplaySettleArrow(proof) {
|
|
90120
|
-
if (proof.kind === "url") {
|
|
90121
|
-
return `async (page) => {
|
|
90122
|
-
await page.waitForURL(${urlProofPredicateExpr(proof.route)}, { timeout: 15000 });
|
|
90123
|
-
}`;
|
|
90124
|
-
}
|
|
90125
|
-
return `async (page) => {
|
|
90126
|
-
await ${authProofLocator(proof)}.waitFor({ state: 'visible', timeout: 15000 });
|
|
90127
|
-
}`;
|
|
90128
|
-
}
|
|
90129
|
-
function renderAuthUrlWaitLine(redirectRoute) {
|
|
90130
|
-
const opts = `{ waitUntil: 'domcontentloaded', timeout: 8000 }`;
|
|
90131
|
-
const tail = `.catch(() => { /* URL drift \u2014 the auth-proof assertion below is the gate */ });`;
|
|
90132
|
-
const rr = redirectRoute ? stripToPathname(redirectRoute) : null;
|
|
90133
|
-
if (rr && rr !== "/") {
|
|
90134
|
-
const r = JSON.stringify(rr);
|
|
90135
|
-
return ` await page.waitForURL((u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/'), ${opts})${tail}`;
|
|
90136
|
-
}
|
|
90137
|
-
if (rr === "/") {
|
|
90138
|
-
return ` await page.waitForURL((u) => !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
|
|
90139
|
-
}
|
|
90140
|
-
return ` await page.waitForURL((u) => u.pathname !== '/' && !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
|
|
90141
|
-
}
|
|
90142
|
-
function locatorChainOf(cmd) {
|
|
90143
|
-
const m = cmd.match(/\bpage\.[\s\S]*?(?=\.(?:fill|click|check|press|selectOption|setChecked|waitFor)\s*\()/);
|
|
90144
|
-
return m ? m[0].trim() : null;
|
|
90145
|
-
}
|
|
90146
|
-
function parseRoleName(locator) {
|
|
90147
|
-
const role = locator.match(/getByRole\(\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
90148
|
-
const name = locator.match(/getByRole\([^)]*name:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByLabel\(\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByPlaceholder\(\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
90149
|
-
return { role, name };
|
|
90150
|
-
}
|
|
90151
|
-
function credentialKeyOf(cmd) {
|
|
90152
|
-
return cmd.match(/\.fill\(\s*process\.env\.([A-Za-z_$][\w$]*)!?\s*\)/)?.[1] ?? null;
|
|
90153
|
-
}
|
|
90154
|
-
function commandToReplayStep(cmd) {
|
|
90155
|
-
const chain = locatorChainOf(cmd);
|
|
90156
|
-
if (!chain)
|
|
90157
|
-
return null;
|
|
90158
|
-
const credKey = credentialKeyOf(cmd);
|
|
90159
|
-
if (cmd.includes(".fill(")) {
|
|
90160
|
-
if (!credKey)
|
|
90161
|
-
return null;
|
|
90162
|
-
const { role, name } = parseRoleName(chain);
|
|
90163
|
-
return { kind: "fill", credentialKey: credKey, locator: chain, role, name, element: name ?? role ?? "credential field" };
|
|
90164
|
-
}
|
|
90165
|
-
if (/\.waitFor\s*\(/.test(cmd))
|
|
90166
|
-
return { kind: "wait", locator: chain };
|
|
90167
|
-
const pressKey = cmd.match(/\.press\(\s*['"`]([^'"`]+)['"`]\s*\)/)?.[1];
|
|
90168
|
-
if (pressKey)
|
|
90169
|
-
return { kind: "press", locator: chain, key: pressKey };
|
|
90170
|
-
if (/\.(?:click|check|setChecked|selectOption)\s*\(/.test(cmd))
|
|
90171
|
-
return { kind: "click", locator: chain };
|
|
90172
|
-
return null;
|
|
90173
|
-
}
|
|
90174
|
-
function buildReplayPlan(login) {
|
|
90175
|
-
const plan = [];
|
|
90176
|
-
const preNavRoute = login.preNavRoute ?? "/";
|
|
90177
|
-
plan.push({ kind: "nav", route: preNavRoute });
|
|
90178
|
-
for (const c of login.preNavCmds ?? []) {
|
|
90179
|
-
const step = commandToReplayStep(c);
|
|
90180
|
-
if (step)
|
|
90181
|
-
plan.push(step);
|
|
90182
|
-
}
|
|
90183
|
-
if ((login.preNavCmds ?? []).length === 0 && login.route && login.route !== preNavRoute) {
|
|
90184
|
-
plan.push({ kind: "nav", route: login.route });
|
|
90185
|
-
}
|
|
90186
|
-
for (const c of login.cmds) {
|
|
90187
|
-
const step = commandToReplayStep(c);
|
|
90188
|
-
if (step)
|
|
90189
|
-
plan.push(step);
|
|
90190
|
-
}
|
|
90191
|
-
return plan;
|
|
90192
|
-
}
|
|
90193
|
-
function extractAuthRegion(code) {
|
|
90194
|
-
const m = code.match(new RegExp(AUTH_REGION_RE.source));
|
|
90195
|
-
return m ? m[0] : null;
|
|
90196
|
-
}
|
|
90197
|
-
function countAuthRegions(code) {
|
|
90198
|
-
const m = code.match(AUTH_REGION_RE);
|
|
90199
|
-
return m ? m.length : 0;
|
|
90200
|
-
}
|
|
90201
|
-
function stripAuthRegions(code) {
|
|
90202
|
-
return code.replace(AUTH_REGION_RE, "");
|
|
90203
|
-
}
|
|
90204
|
-
var PW_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?/;
|
|
90205
|
-
function ensureExpectImported(code) {
|
|
90206
|
-
const m = code.match(PW_IMPORT_RE);
|
|
90207
|
-
if (!m)
|
|
90208
|
-
return code;
|
|
90209
|
-
const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
90210
|
-
if (names.includes("expect"))
|
|
90211
|
-
return code;
|
|
90212
|
-
const next = [...names, "expect"];
|
|
90213
|
-
return code.replace(PW_IMPORT_RE, `import { ${next.join(", ")} } from '@playwright/test';`);
|
|
90214
|
-
}
|
|
90215
|
-
function insertAfterPwImport(code, block) {
|
|
90216
|
-
const m = code.match(PW_IMPORT_RE);
|
|
90217
|
-
if (!m || m.index === void 0) {
|
|
90218
|
-
return `${block}
|
|
90219
|
-
|
|
90220
|
-
${code.replace(/^\r?\n+/, "")}`;
|
|
90221
|
-
}
|
|
90222
|
-
const end = m.index + m[0].length;
|
|
90223
|
-
const head = code.slice(0, end);
|
|
90224
|
-
const tail = code.slice(end).replace(/^\r?\n+/, "");
|
|
90225
|
-
return `${head}
|
|
90226
|
-
|
|
90227
|
-
${block}
|
|
90228
|
-
|
|
90229
|
-
${tail}`;
|
|
90230
|
-
}
|
|
90231
|
-
var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
|
|
90232
|
-
var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
|
|
90233
|
-
function ensureLoginCall(code) {
|
|
90234
|
-
if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
|
|
90235
|
-
return code;
|
|
90236
|
-
let result = code;
|
|
90237
|
-
if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
|
|
90238
|
-
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
90239
|
-
}
|
|
90240
|
-
const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
|
|
90241
|
-
for (let i = matches.length - 1; i >= 0; i--) {
|
|
90242
|
-
const m = matches[i];
|
|
90243
|
-
if (m.index === void 0)
|
|
90244
|
-
continue;
|
|
90245
|
-
const insertAt = m.index + m[0].length;
|
|
90246
|
-
const following = result.slice(insertAt, insertAt + 60);
|
|
90247
|
-
if (/^\s*await\s+__vqaLogin\s*\(\s*page\s*\)/.test(following))
|
|
90248
|
-
continue;
|
|
90249
|
-
result = `${result.slice(0, insertAt)}
|
|
90250
|
-
await __vqaLogin(page);${result.slice(insertAt)}`;
|
|
90251
|
-
}
|
|
90252
|
-
return result;
|
|
90253
|
-
}
|
|
90254
|
-
function upsertAuthBlock(code, ctx) {
|
|
90255
|
-
if (ctx.mode === "generate") {
|
|
90256
|
-
if (!ctx.recipe || !ctx.isAuthNeeded)
|
|
90257
|
-
return code;
|
|
90258
|
-
let result = stripAuthRegions(code);
|
|
90259
|
-
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
90260
|
-
result = ensureExpectImported(result);
|
|
90261
|
-
const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
|
|
90262
|
-
result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
|
|
90263
|
-
result = ensureLoginCall(result);
|
|
90264
|
-
const n = countAuthRegions(result);
|
|
90265
|
-
if (n !== 1) {
|
|
90266
|
-
throw new Error(`upsertAuthBlock(generate): expected exactly 1 auth region after stamp, got ${n}`);
|
|
90267
|
-
}
|
|
90268
|
-
if (!LOGIN_CALL_RE.test(result)) {
|
|
90269
|
-
throw new Error("upsertAuthBlock(generate): stamped __vqaLogin but found no test() opener to inject the call");
|
|
90270
|
-
}
|
|
90271
|
-
return result;
|
|
90272
|
-
}
|
|
90273
|
-
try {
|
|
90274
|
-
if (!ctx.preservedAuthRegion)
|
|
90275
|
-
return code;
|
|
90276
|
-
let result = ensureExpectImported(code);
|
|
90277
|
-
if (countAuthRegions(result) >= 1) {
|
|
90278
|
-
result = result.replace(AUTH_REGION_RE, "");
|
|
90279
|
-
result = insertAfterPwImport(result, ctx.preservedAuthRegion);
|
|
90280
|
-
} else {
|
|
90281
|
-
result = insertAfterPwImport(result, ctx.preservedAuthRegion);
|
|
90282
|
-
}
|
|
90283
|
-
result = ensureLoginCall(result);
|
|
90284
|
-
if (countAuthRegions(result) !== 1) {
|
|
90285
|
-
throw new Error("heal re-stamp produced non-unique auth region");
|
|
90286
|
-
}
|
|
90287
|
-
return result;
|
|
90288
|
-
} catch (err) {
|
|
90289
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
90290
|
-
ctx.onSignal?.({ type: "auth-stamp-degraded", reason });
|
|
90291
|
-
return code;
|
|
90292
|
-
}
|
|
90293
|
-
}
|
|
90294
|
-
function buildHealAuthStamp(originalCode, failureError, logs2) {
|
|
90295
|
-
const region = extractAuthRegion(originalCode);
|
|
90296
|
-
const loginFailed = !!failureError && /__vqaLogin/.test(failureError);
|
|
90297
|
-
return {
|
|
90298
|
-
mode: "heal",
|
|
90299
|
-
preservedAuthRegion: region && !loginFailed ? region : null,
|
|
90300
|
-
onSignal: logs2 ? (s) => logs2.push(`\u26A0 auth-stamp ${s.type}: ${s.reason}`) : void 0
|
|
90301
|
-
};
|
|
90302
|
-
}
|
|
90303
|
-
}
|
|
90304
|
-
});
|
|
90305
|
-
|
|
90306
90916
|
// ../runner-core/dist/services/auth-replay.js
|
|
90307
90917
|
var require_auth_replay = __commonJS({
|
|
90308
90918
|
"../runner-core/dist/services/auth-replay.js"(exports) {
|
|
@@ -94179,9 +94789,41 @@ var require_discover_generator = __commonJS({
|
|
|
94179
94789
|
var reveal_diff_js_1 = require_reveal_diff();
|
|
94180
94790
|
var anchor_harvester_js_1 = require_anchor_harvester();
|
|
94181
94791
|
var auth_recipe_js_1 = require_auth_recipe();
|
|
94792
|
+
var inbox_stamp_js_1 = require_inbox_stamp();
|
|
94182
94793
|
var auth_replay_js_1 = require_auth_replay();
|
|
94183
94794
|
var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
|
|
94184
94795
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
94796
|
+
var inbox_tools_js_1 = require_inbox_tools();
|
|
94797
|
+
var verification_inbox_js_1 = require_verification_inbox();
|
|
94798
|
+
var test_inbox_broker_js_1 = require_test_inbox_broker();
|
|
94799
|
+
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;
|
|
94800
|
+
function scenarioLooksLikeRegistration(scenario) {
|
|
94801
|
+
const hay = `${scenario.name ?? ""} ${scenario.description ?? ""} ${scenario.entryRoute ?? ""} ${(scenario.targetPages ?? []).join(" ")}`;
|
|
94802
|
+
return REGISTER_SCENARIO_RE.test(hay);
|
|
94803
|
+
}
|
|
94804
|
+
function buildEmailVerifyGenBlock(genEmail, genPassword) {
|
|
94805
|
+
return `
|
|
94806
|
+
|
|
94807
|
+
## EMAIL VERIFICATION TEST \u2014 register a fresh account and verify it (LIVE, then EMIT)
|
|
94808
|
+
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.
|
|
94809
|
+
|
|
94810
|
+
LIVE (to prove the flow while generating):
|
|
94811
|
+
- Register on the app's signup form using this throwaway account:
|
|
94812
|
+
- email: \`${genEmail}\`
|
|
94813
|
+
- password: \`${genPassword}\`
|
|
94814
|
+
Type them with browser_type and submit the form ONCE.
|
|
94815
|
+
- 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\`.
|
|
94816
|
+
- Reach the verified / signed-in state and confirm it with a browser_verify_* call, so your final assertion is proven.
|
|
94817
|
+
|
|
94818
|
+
EMIT (the code you put in finish_generate) \u2014 do NOT hardcode the throwaway values above:
|
|
94819
|
+
- 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.
|
|
94820
|
+
- Read the verification email at runtime with the injected helper (it is provided for you \u2014 do NOT define it, do NOT import it):
|
|
94821
|
+
- code flow: \`const code = await __vqaWaitForEmail('code');\` then type \`code\` into the verification field.
|
|
94822
|
+
- link flow: \`const link = await __vqaWaitForEmail('link');\` then \`await page.goto(link);\`.
|
|
94823
|
+
- if unsure: \`const email = await __vqaWaitForEmail();\` returns \`{ code, link, subject, from }\`.
|
|
94824
|
+
- Tag the test \`@email-verify\`. Emit EXACTLY ONE test() in this file (one registration per run).
|
|
94825
|
+
- 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.`;
|
|
94826
|
+
}
|
|
94185
94827
|
exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
|
|
94186
94828
|
"browser_install",
|
|
94187
94829
|
"browser_close",
|
|
@@ -95368,6 +96010,11 @@ ${testOpen}`;
|
|
|
95368
96010
|
if (options?.authStamp) {
|
|
95369
96011
|
result = (0, auth_recipe_js_1.upsertAuthBlock)(result, options.authStamp);
|
|
95370
96012
|
}
|
|
96013
|
+
try {
|
|
96014
|
+
result = (0, inbox_stamp_js_1.upsertInboxBlock)(result);
|
|
96015
|
+
} catch {
|
|
96016
|
+
result = (0, inbox_stamp_js_1.stripInboxRegions)(result);
|
|
96017
|
+
}
|
|
95371
96018
|
return result;
|
|
95372
96019
|
}
|
|
95373
96020
|
function ensureDiscoveredTag(tags) {
|
|
@@ -95454,6 +96101,16 @@ ${testOpen}`;
|
|
|
95454
96101
|
logs2?.push(` \u2717 Rejected "${t.name}": JS parse error \u2014 ${msg}`);
|
|
95455
96102
|
return false;
|
|
95456
96103
|
}
|
|
96104
|
+
{
|
|
96105
|
+
const modelBody = (0, inbox_stamp_js_1.stripInboxRegions)(code);
|
|
96106
|
+
if (inbox_stamp_js_1.INBOX_HELPER_TOKEN_RE.test(modelBody)) {
|
|
96107
|
+
const openerCount = (stripStringsAndComments(modelBody).match(/\btest(?:\.(?:only|skip|fixme))?\s*\(/g) ?? []).length;
|
|
96108
|
+
if (openerCount > 1) {
|
|
96109
|
+
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`);
|
|
96110
|
+
return false;
|
|
96111
|
+
}
|
|
96112
|
+
}
|
|
96113
|
+
}
|
|
95457
96114
|
const codeForArgOrderScan = stripStringsAndComments(code);
|
|
95458
96115
|
if (/test\s*\(\s*,\s*(?:async\s*)?(?:\([^)]*\)|function\s*\([^)]*\))\s*(?:=>)?\s*\{[\s\S]*?\}\s*,\s*\{\s*(?:tags?|annotations)\s*:/.test(codeForArgOrderScan)) {
|
|
95459
96116
|
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)`);
|
|
@@ -96274,6 +96931,23 @@ ${testOpen}`;
|
|
|
96274
96931
|
return { offendingLines: offending };
|
|
96275
96932
|
}
|
|
96276
96933
|
var ACTION_METHODS = ["click", "dblclick", "tap", "fill", "type", "pressSequentially", "press", "selectOption", "hover", "check", "uncheck", "setInputFiles", "dragTo", "focus", "goto", "waitForURL"];
|
|
96934
|
+
var DYNAMIC_VERIFICATION_ARG = "<verification-dynamic>";
|
|
96935
|
+
function collectVerificationHelperVars(code) {
|
|
96936
|
+
const vars = /* @__PURE__ */ new Set();
|
|
96937
|
+
const assignRe = /(?:(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*await\s+__vqaWaitForEmail\s*\(/g;
|
|
96938
|
+
let m;
|
|
96939
|
+
while ((m = assignRe.exec(code)) !== null)
|
|
96940
|
+
vars.add(m[1]);
|
|
96941
|
+
const destructRe = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*await\s+__vqaWaitForEmail\s*\(/g;
|
|
96942
|
+
while ((m = destructRe.exec(code)) !== null) {
|
|
96943
|
+
for (const part of m[1].split(",")) {
|
|
96944
|
+
const name = part.split(":").pop()?.trim().replace(/\s.*$/, "");
|
|
96945
|
+
if (name && /^[A-Za-z_$][\w$]*$/.test(name))
|
|
96946
|
+
vars.add(name);
|
|
96947
|
+
}
|
|
96948
|
+
}
|
|
96949
|
+
return vars;
|
|
96950
|
+
}
|
|
96277
96951
|
function extractStringArg(text) {
|
|
96278
96952
|
const m = text.match(/^\s*(['"`])((?:\\.|(?!\1).)*)\1/);
|
|
96279
96953
|
if (!m)
|
|
@@ -96543,6 +97217,8 @@ ${testOpen}`;
|
|
|
96543
97217
|
if (!locatorsMatch(ledger.locator, submitted.locator))
|
|
96544
97218
|
return false;
|
|
96545
97219
|
if (ledger.method === "fill" || ledger.method === "type") {
|
|
97220
|
+
if (submitted.arg === DYNAMIC_VERIFICATION_ARG)
|
|
97221
|
+
return true;
|
|
96546
97222
|
if (ledger.arg === "<env>" || ledger.arg === "<test-value>") {
|
|
96547
97223
|
return submitted.arg === "<env>" || submitted.arg === "<test-value>";
|
|
96548
97224
|
}
|
|
@@ -96637,6 +97313,25 @@ ${testOpen}`;
|
|
|
96637
97313
|
}
|
|
96638
97314
|
}
|
|
96639
97315
|
const submittedSigs = extractActionSignatures(playwrightCode, credentials);
|
|
97316
|
+
const helperVars = collectVerificationHelperVars(playwrightCode);
|
|
97317
|
+
if (helperVars.size > 0) {
|
|
97318
|
+
const usesHelperVar = (text) => {
|
|
97319
|
+
for (const v of helperVars) {
|
|
97320
|
+
if (new RegExp(`\\b${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text))
|
|
97321
|
+
return true;
|
|
97322
|
+
}
|
|
97323
|
+
return false;
|
|
97324
|
+
};
|
|
97325
|
+
for (const sub of submittedSigs) {
|
|
97326
|
+
if ((sub.method === "fill" || sub.method === "type") && sub.arg && usesHelperVar(sub.arg)) {
|
|
97327
|
+
sub.arg = DYNAMIC_VERIFICATION_ARG;
|
|
97328
|
+
} else if (sub.method === "goto") {
|
|
97329
|
+
const gotoArg = sub.raw.match(/\.goto\s*\(\s*([^),]+)/);
|
|
97330
|
+
if (gotoArg && usesHelperVar(gotoArg[1]))
|
|
97331
|
+
sub.arg = DYNAMIC_VERIFICATION_ARG;
|
|
97332
|
+
}
|
|
97333
|
+
}
|
|
97334
|
+
}
|
|
96640
97335
|
const offending = [];
|
|
96641
97336
|
let firstGotoConsumed = false;
|
|
96642
97337
|
let cursor = 0;
|
|
@@ -96644,6 +97339,8 @@ ${testOpen}`;
|
|
|
96644
97339
|
for (const sub of submittedSigs) {
|
|
96645
97340
|
if (sub.method === "waitForURL")
|
|
96646
97341
|
continue;
|
|
97342
|
+
if (sub.method === "goto" && sub.arg === DYNAMIC_VERIFICATION_ARG)
|
|
97343
|
+
continue;
|
|
96647
97344
|
if (sub.method === "goto" && !firstGotoConsumed) {
|
|
96648
97345
|
firstGotoConsumed = true;
|
|
96649
97346
|
if (expectedEntry !== null && sub.arg !== expectedEntry) {
|
|
@@ -96969,7 +97666,7 @@ expect(secondTitle).toBeTruthy();
|
|
|
96969
97666
|
${tailBlock}`;
|
|
96970
97667
|
}
|
|
96971
97668
|
function evaluateAmbiguityGate(finalCode, alreadyAttempted, hasRevisionBudget) {
|
|
96972
|
-
const findings = (0, agentic_discovery_prompts_js_1.scanAmbiguousLocators)((0, auth_recipe_js_1.stripAuthRegions)(finalCode));
|
|
97669
|
+
const findings = (0, agentic_discovery_prompts_js_1.scanAmbiguousLocators)((0, auth_recipe_js_1.stripAuthRegions)((0, inbox_stamp_js_1.stripInboxRegions)(finalCode)));
|
|
96973
97670
|
return { fire: findings.length > 0 && !alreadyAttempted && hasRevisionBudget, findings };
|
|
96974
97671
|
}
|
|
96975
97672
|
async function generateTestFromScenario(mcpClient, scenario, siteMap, baseUrl, credentials, aiClient, agentModel, logs2, onAICall, opts) {
|
|
@@ -96993,6 +97690,7 @@ ${tailBlock}`;
|
|
|
96993
97690
|
}
|
|
96994
97691
|
}
|
|
96995
97692
|
}
|
|
97693
|
+
let genInboxReader = null;
|
|
96996
97694
|
try {
|
|
96997
97695
|
const startTime = Date.now();
|
|
96998
97696
|
const groupId = `scenario-gen-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
@@ -97043,12 +97741,28 @@ ${tailBlock}`;
|
|
|
97043
97741
|
});
|
|
97044
97742
|
const baseSkeletonHadRegression = tags.includes("@regression");
|
|
97045
97743
|
const hasVerifiedRecipe = !!opts?.recipe && tags.includes("@needs-auth");
|
|
97046
|
-
const
|
|
97744
|
+
const projectHasRegisterIntent = credentials?.DISCOVERY_SHOULD_REGISTER === "true";
|
|
97745
|
+
const inboxGenConfig = (0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() && projectHasRegisterIntent && !hasVerifiedRecipe && scenarioLooksLikeRegistration(scenario) ? (0, verification_inbox_js_1.loadVerificationInboxConfig)() : null;
|
|
97746
|
+
let genInboxState = null;
|
|
97747
|
+
let genInboxCredentials = credentials;
|
|
97748
|
+
let genInboxSecret = null;
|
|
97749
|
+
if (inboxGenConfig) {
|
|
97750
|
+
const genTag = `vqag${(0, crypto_1.createHash)("sha256").update(`testgen|${(0, verification_inbox_js_1.seedToHostKey)(baseUrl)}|${groupId}`).digest("hex").slice(0, 10)}`;
|
|
97751
|
+
const genEmail = (0, verification_inbox_js_1.applyPlusTag)(inboxGenConfig.address, genTag);
|
|
97752
|
+
const genPassword = (0, verification_inbox_js_1.deriveRegistrationPassword)(genEmail);
|
|
97753
|
+
genInboxReader = new verification_inbox_js_1.VerificationInboxReader(inboxGenConfig);
|
|
97754
|
+
genInboxState = (0, inbox_tools_js_1.createInboxRunState)(inboxGenConfig, { tag: genTag, passwordKey: test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV });
|
|
97755
|
+
genInboxCredentials = { ...credentials, [test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV]: genEmail, [test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV]: genPassword };
|
|
97756
|
+
genInboxSecret = genPassword;
|
|
97757
|
+
openaiTools.push(...inbox_tools_js_1.INBOX_TOOLS);
|
|
97758
|
+
logs2.push(` \u{1F4EC} Email-verification generation ON for "${scenario.name}" \u2014 registering throwaway ${genEmail}`);
|
|
97759
|
+
}
|
|
97760
|
+
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]) : "");
|
|
97047
97761
|
const messages = [
|
|
97048
97762
|
{ role: "system", content: systemPrompt },
|
|
97049
97763
|
{
|
|
97050
97764
|
role: "user",
|
|
97051
|
-
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.`
|
|
97765
|
+
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.`
|
|
97052
97766
|
}
|
|
97053
97767
|
];
|
|
97054
97768
|
let iteration = 0;
|
|
@@ -97176,6 +97890,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97176
97890
|
const iterStart = Date.now();
|
|
97177
97891
|
const requestMessageCount = messages.length;
|
|
97178
97892
|
const requestMessages = messages.slice();
|
|
97893
|
+
const scrubAudit = (payload) => {
|
|
97894
|
+
if (!genInboxSecret)
|
|
97895
|
+
return payload;
|
|
97896
|
+
try {
|
|
97897
|
+
return JSON.parse(JSON.stringify(payload).split(genInboxSecret).join("[REDACTED:VQA_TEST_ACCOUNT_PASSWORD]"));
|
|
97898
|
+
} catch {
|
|
97899
|
+
return payload;
|
|
97900
|
+
}
|
|
97901
|
+
};
|
|
97179
97902
|
let response;
|
|
97180
97903
|
try {
|
|
97181
97904
|
response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
@@ -97217,7 +97940,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97217
97940
|
messages.push(assistantMessage);
|
|
97218
97941
|
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
|
97219
97942
|
try {
|
|
97220
|
-
onAICall?.({
|
|
97943
|
+
onAICall?.(scrubAudit({
|
|
97221
97944
|
iteration,
|
|
97222
97945
|
groupId,
|
|
97223
97946
|
callType: "scenarioGenerate",
|
|
@@ -97231,7 +97954,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97231
97954
|
requestMessageCount,
|
|
97232
97955
|
requestMessages,
|
|
97233
97956
|
ledger: actionLedger.slice()
|
|
97234
|
-
});
|
|
97957
|
+
}));
|
|
97235
97958
|
} catch {
|
|
97236
97959
|
}
|
|
97237
97960
|
logs2.push(` No tool calls \u2014 agent stopped`);
|
|
@@ -97258,7 +97981,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97258
97981
|
const suspectedBugDeclared = toolArgs.suspectedBug === true;
|
|
97259
97982
|
const bugDescription = String(toolArgs.bugDescription || "").slice(0, 500).replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/\s+/g, " ").trim();
|
|
97260
97983
|
const testObj = { name, description, playwrightCode: code, tags };
|
|
97261
|
-
const validated = validateTests([testObj], logs2, { sensitiveEnvMap:
|
|
97984
|
+
const validated = validateTests([testObj], logs2, { sensitiveEnvMap: genInboxCredentials, authRecipe: opts?.recipe ?? null, basePath: gotoBasePathFromBaseUrl(baseUrl) });
|
|
97262
97985
|
if (validated.length > 0) {
|
|
97263
97986
|
if (actionLedger.length > lastRenderedLedgerEntryCount && actionLedger.length > 0) {
|
|
97264
97987
|
const renderedLedger = renderSessionRecording(actionLedger, credentials);
|
|
@@ -97310,7 +98033,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97310
98033
|
});
|
|
97311
98034
|
continue;
|
|
97312
98035
|
}
|
|
97313
|
-
const actionCheck = validateFinalActionFidelity(code, actionLedger,
|
|
98036
|
+
const actionCheck = validateFinalActionFidelity(code, actionLedger, genInboxCredentials, { entryRoute: scenario.entryRoute });
|
|
97314
98037
|
if (actionCheck.offendingActions.length > 0 && !actionRevisionAttempted) {
|
|
97315
98038
|
actionRevisionAttempted = true;
|
|
97316
98039
|
const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
|
|
@@ -97623,6 +98346,15 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
97623
98346
|
});
|
|
97624
98347
|
continue;
|
|
97625
98348
|
}
|
|
98349
|
+
if (genInboxReader && genInboxState && inbox_tools_js_1.INBOX_TOOL_NAMES.has(toolName)) {
|
|
98350
|
+
const inboxResult = await (0, inbox_tools_js_1.dispatchInboxTool)(toolName, toolArgs, {
|
|
98351
|
+
reader: genInboxReader,
|
|
98352
|
+
state: genInboxState,
|
|
98353
|
+
mcpClient
|
|
98354
|
+
});
|
|
98355
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: inboxResult.content });
|
|
98356
|
+
continue;
|
|
98357
|
+
}
|
|
97626
98358
|
let toolResultContent = "";
|
|
97627
98359
|
try {
|
|
97628
98360
|
const mcpResult = await mcpClient.callTool({ name: toolName, arguments: toolArgs });
|
|
@@ -97785,7 +98517,7 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
|
|
|
97785
98517
|
tool: assistantMessage.tool_calls?.find((tc) => tc.id === tr.tool_call_id)?.function.name ?? "unknown",
|
|
97786
98518
|
result: typeof tr.content === "string" ? tr.content.slice(0, 5e4) : String(tr.content).slice(0, 5e4)
|
|
97787
98519
|
}));
|
|
97788
|
-
onAICall?.({
|
|
98520
|
+
onAICall?.(scrubAudit({
|
|
97789
98521
|
iteration,
|
|
97790
98522
|
groupId,
|
|
97791
98523
|
callType: "scenarioGenerate",
|
|
@@ -97801,7 +98533,7 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
|
|
|
97801
98533
|
requestMessageCount,
|
|
97802
98534
|
requestMessages,
|
|
97803
98535
|
ledger: actionLedger.slice()
|
|
97804
|
-
});
|
|
98536
|
+
}));
|
|
97805
98537
|
} catch {
|
|
97806
98538
|
}
|
|
97807
98539
|
messages.push(...toolResults);
|
|
@@ -97809,8 +98541,12 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
|
|
|
97809
98541
|
if (!result) {
|
|
97810
98542
|
logs2.push(` Scenario generation did not produce a test after ${iteration} iterations`);
|
|
97811
98543
|
}
|
|
98544
|
+
await genInboxReader?.close().catch(() => {
|
|
98545
|
+
});
|
|
97812
98546
|
return result;
|
|
97813
98547
|
} catch (err) {
|
|
98548
|
+
await genInboxReader?.close().catch(() => {
|
|
98549
|
+
});
|
|
97814
98550
|
const msg = err instanceof Error ? err.message : String(err);
|
|
97815
98551
|
logs2.push(` Scenario generation failed: ${msg}`);
|
|
97816
98552
|
if ((0, ai_retry_js_1.isAIProviderError)(err)) {
|
|
@@ -319095,7 +319831,10 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319095
319831
|
"../runner-core/dist/services/survey-link-frontier.js"(exports) {
|
|
319096
319832
|
"use strict";
|
|
319097
319833
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
319098
|
-
exports.FINISH_GATE_MIN_BUDGET_PCT = exports.MAX_FINISH_REDIRECTS = void 0;
|
|
319834
|
+
exports.FINISH_ESCAPE_BOUNCE_STREAK = exports.FINISH_GATE_MIN_BUDGET_PCT = exports.MAX_FINISH_REDIRECTS = void 0;
|
|
319835
|
+
exports.initialFinishBounceState = initialFinishBounceState;
|
|
319836
|
+
exports.evaluateFinishBounce = evaluateFinishBounce;
|
|
319837
|
+
exports.recordFinishBounce = recordFinishBounce;
|
|
319099
319838
|
exports.isSurveyLinkFrontierEnabled = isSurveyLinkFrontierEnabled;
|
|
319100
319839
|
exports.extractSnapshotLinks = extractSnapshotLinks;
|
|
319101
319840
|
exports.deriveLinkScope = deriveLinkScope;
|
|
@@ -319112,6 +319851,17 @@ var require_survey_link_frontier = __commonJS({
|
|
|
319112
319851
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
319113
319852
|
exports.MAX_FINISH_REDIRECTS = 2;
|
|
319114
319853
|
exports.FINISH_GATE_MIN_BUDGET_PCT = 0.2;
|
|
319854
|
+
exports.FINISH_ESCAPE_BOUNCE_STREAK = 3;
|
|
319855
|
+
function initialFinishBounceState() {
|
|
319856
|
+
return { streak: 0, pagesBaseline: 0 };
|
|
319857
|
+
}
|
|
319858
|
+
function evaluateFinishBounce(prev, pagesVisited, threshold = exports.FINISH_ESCAPE_BOUNCE_STREAK) {
|
|
319859
|
+
const base2 = pagesVisited > prev.pagesBaseline ? { streak: 0, pagesBaseline: pagesVisited } : prev;
|
|
319860
|
+
return { state: base2, escape: base2.streak >= threshold };
|
|
319861
|
+
}
|
|
319862
|
+
function recordFinishBounce(prev) {
|
|
319863
|
+
return { streak: prev.streak + 1, pagesBaseline: prev.pagesBaseline };
|
|
319864
|
+
}
|
|
319115
319865
|
function isSurveyLinkFrontierEnabled() {
|
|
319116
319866
|
const v = process.env.DISCOVERY_SURVEY_LINK_FRONTIER;
|
|
319117
319867
|
return v === "true" || v === "1";
|
|
@@ -323300,6 +324050,7 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
323300
324050
|
const linkScope = (0, survey_link_frontier_js_1.deriveLinkScope)(baseUrl);
|
|
323301
324051
|
const surveyNavScope = options?.mode === "survey" ? (0, survey_scope_js_1.deriveSurveyPathScope)(baseUrl) : null;
|
|
323302
324052
|
let finishRedirects = 0;
|
|
324053
|
+
let finishBounce = (0, survey_link_frontier_js_1.initialFinishBounceState)();
|
|
323303
324054
|
let lastLandedPageUrl = null;
|
|
323304
324055
|
const state2 = (0, exploration_state_js_1.createExplorationState)();
|
|
323305
324056
|
function ingestLinksFromText(rawText) {
|
|
@@ -323537,12 +324288,14 @@ ${inboxPromptBlock}`;
|
|
|
323537
324288
|
const item = state2.frontier.get(r);
|
|
323538
324289
|
return item?.source === "link-seen" && item.attempted === true;
|
|
323539
324290
|
};
|
|
323540
|
-
const
|
|
324291
|
+
const isSatisfied = (r) => (0, survey_link_frontier_js_1.visitedSatisfies)(state2, r);
|
|
324292
|
+
const ranked = (0, exploration_state_js_1.getRankedFrontier)(state2, { preferShallow: true }).filter((r) => r !== "/" && !isAttemptedLinkEntry(r) && !isSatisfied(r));
|
|
323541
324293
|
const rankedSet = new Set(ranked);
|
|
323542
324294
|
const navigatedUncaptured = [];
|
|
323543
324295
|
for (const r of state2.frontier.keys()) {
|
|
323544
|
-
if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r))
|
|
324296
|
+
if (r !== "/" && !rankedSet.has(r) && !isAttemptedLinkEntry(r) && !isSatisfied(r)) {
|
|
323545
324297
|
navigatedUncaptured.push(r);
|
|
324298
|
+
}
|
|
323546
324299
|
}
|
|
323547
324300
|
return ranked.concat(navigatedUncaptured);
|
|
323548
324301
|
};
|
|
@@ -324095,6 +324848,7 @@ ${currentSummary}`;
|
|
|
324095
324848
|
}
|
|
324096
324849
|
const toolResults = [];
|
|
324097
324850
|
let finishBouncedThisBatch = false;
|
|
324851
|
+
let finishStreakCountedThisBatch = false;
|
|
324098
324852
|
const fullVarMapForGuard = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
324099
324853
|
const { secrets: secretsOnlyForGuard } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMapForGuard);
|
|
324100
324854
|
const allowedCredentialValues = inboxState ? [inboxState.email] : [];
|
|
@@ -324273,25 +325027,41 @@ ${currentSummary}`;
|
|
|
324273
325027
|
}
|
|
324274
325028
|
logs2.push(` Captured page: ${normalized.route}, ${normalized.provenCommands.length} proven commands${normalized.alreadyCaptured ? " (RE-VISIT)" : ""}`);
|
|
324275
325029
|
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).` } : {};
|
|
324276
|
-
const
|
|
324277
|
-
|
|
324278
|
-
|
|
324279
|
-
{
|
|
324280
|
-
|
|
324281
|
-
|
|
324282
|
-
|
|
324283
|
-
|
|
324284
|
-
|
|
324285
|
-
|
|
324286
|
-
|
|
324287
|
-
|
|
324288
|
-
|
|
324289
|
-
|
|
324290
|
-
|
|
324291
|
-
|
|
324292
|
-
|
|
324293
|
-
|
|
324294
|
-
|
|
325030
|
+
const progressLine = `${state2.pagesVisited} pages, ${state2.transitionsFound} transitions, ${state2.recordedJourneys.length} tests | iteration ${iteration}/${maxIterations} (${maxIterations - iteration} remaining)`;
|
|
325031
|
+
if (isSurveyMode && normalized.alreadyCaptured && normalized.observations.length === 0 && (adaptiveActive || linkFrontierEnabled)) {
|
|
325032
|
+
const nextRoutes = (adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route)).slice(0, 6);
|
|
325033
|
+
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.";
|
|
325034
|
+
toolResults.push({
|
|
325035
|
+
role: "tool",
|
|
325036
|
+
tool_call_id: toolCall.id,
|
|
325037
|
+
content: JSON.stringify({
|
|
325038
|
+
acknowledged: true,
|
|
325039
|
+
alreadyCaptured: true,
|
|
325040
|
+
note: steer,
|
|
325041
|
+
progress: progressLine
|
|
325042
|
+
})
|
|
325043
|
+
});
|
|
325044
|
+
} else {
|
|
325045
|
+
const lastAction = normalized.observations.length > 0 ? normalized.observations[normalized.observations.length - 1].action : void 0;
|
|
325046
|
+
const snapshotBlock = (0, snapshot_observation_js_1.renderCompactSnapshotBlock)(
|
|
325047
|
+
normalized,
|
|
325048
|
+
{ lastAction },
|
|
325049
|
+
// G10: no explicit elementLimit — let the flag-aware deep default govern
|
|
325050
|
+
// (flags-off 6 = byte-identical; richRender 8, so demoted form controls fit).
|
|
325051
|
+
{ formLimit: 3, observationLimit: 3, mode: options?.mode ?? "full", recoveredClickables }
|
|
325052
|
+
);
|
|
325053
|
+
toolResults.push({
|
|
325054
|
+
role: "tool",
|
|
325055
|
+
tool_call_id: toolCall.id,
|
|
325056
|
+
content: JSON.stringify({
|
|
325057
|
+
acknowledged: true,
|
|
325058
|
+
...normalized.alreadyCaptured ? { note: "Already captured - move on to unvisited pages." } : {},
|
|
325059
|
+
...transitionHint,
|
|
325060
|
+
snapshot: snapshotBlock,
|
|
325061
|
+
progress: progressLine
|
|
325062
|
+
})
|
|
325063
|
+
});
|
|
325064
|
+
}
|
|
324295
325065
|
} catch (err) {
|
|
324296
325066
|
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
324297
325067
|
logs2.push(` Error persisting page: ${errMsg2}`);
|
|
@@ -324708,7 +325478,18 @@ ${currentSummary}`;
|
|
|
324708
325478
|
continue;
|
|
324709
325479
|
}
|
|
324710
325480
|
if (toolName === "finish_exploration") {
|
|
324711
|
-
|
|
325481
|
+
const distinctPathsVisited = new Set([...state2.visitedRoutes].map((r) => (0, exploration_parser_js_1.normalizeRouteForFocusScope)(r))).size;
|
|
325482
|
+
const fb = (0, survey_link_frontier_js_1.evaluateFinishBounce)(finishBounce, distinctPathsVisited);
|
|
325483
|
+
finishBounce = fb.state;
|
|
325484
|
+
const forceAcceptFinish = fb.escape && !finishStreakCountedThisBatch;
|
|
325485
|
+
if (forceAcceptFinish) {
|
|
325486
|
+
const stuck = adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route);
|
|
325487
|
+
const reasonText = String(toolArgs.summary || "").replace(/\s+/g, " ").trim().slice(0, 400);
|
|
325488
|
+
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" : ""}` : ""));
|
|
325489
|
+
if (reasonText)
|
|
325490
|
+
logs2.push(` [finish-escape] agent's stated reason: ${reasonText}`);
|
|
325491
|
+
}
|
|
325492
|
+
if (!forceAcceptFinish && linkFrontierEnabled) {
|
|
324712
325493
|
const blocking = (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2);
|
|
324713
325494
|
const budgetRemainingPct = maxIterations > 0 ? (maxIterations - iteration) / maxIterations : 0;
|
|
324714
325495
|
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)) {
|
|
@@ -324727,13 +325508,17 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324727
325508
|
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.`
|
|
324728
325509
|
})
|
|
324729
325510
|
});
|
|
325511
|
+
if (!finishStreakCountedThisBatch) {
|
|
325512
|
+
finishBounce = (0, survey_link_frontier_js_1.recordFinishBounce)(finishBounce);
|
|
325513
|
+
finishStreakCountedThisBatch = true;
|
|
325514
|
+
}
|
|
324730
325515
|
continue;
|
|
324731
325516
|
}
|
|
324732
325517
|
if (blocking.length > 0) {
|
|
324733
325518
|
logs2.push(` finish accepted with ${blocking.length} uncovered discovered route(s): ${(0, survey_link_frontier_js_1.describeBlockingRoutes)(blocking, 10)}`);
|
|
324734
325519
|
}
|
|
324735
325520
|
}
|
|
324736
|
-
if (adaptiveActive && !adaptiveComplete) {
|
|
325521
|
+
if (!forceAcceptFinish && adaptiveActive && !adaptiveComplete) {
|
|
324737
325522
|
refreshAdaptiveDiscovery();
|
|
324738
325523
|
const remainingList = adaptiveDebtRoutes();
|
|
324739
325524
|
if (remainingList.length > 0 && iteration < adaptiveHardCap) {
|
|
@@ -324741,10 +325526,10 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324741
325526
|
if (iteration >= adaptiveDecisionArmedAt) {
|
|
324742
325527
|
const { directive, keepGoing } = runBudgetController();
|
|
324743
325528
|
if (keepGoing) {
|
|
324744
|
-
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.`;
|
|
325529
|
+
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.`;
|
|
324745
325530
|
}
|
|
324746
325531
|
} else {
|
|
324747
|
-
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.`;
|
|
325532
|
+
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.`;
|
|
324748
325533
|
}
|
|
324749
325534
|
if (reason) {
|
|
324750
325535
|
toolResults.push({
|
|
@@ -324753,6 +325538,10 @@ finish_exploration bounced (${finishRedirects}/${survey_link_frontier_js_1.MAX_F
|
|
|
324753
325538
|
content: JSON.stringify({ acknowledged: false, keepExploring: true, reason })
|
|
324754
325539
|
});
|
|
324755
325540
|
logs2.push(` [adaptive-budget] finish bounced: ${remainingList.length} route(s) still un-captured`);
|
|
325541
|
+
if (!finishStreakCountedThisBatch) {
|
|
325542
|
+
finishBounce = (0, survey_link_frontier_js_1.recordFinishBounce)(finishBounce);
|
|
325543
|
+
finishStreakCountedThisBatch = true;
|
|
325544
|
+
}
|
|
324756
325545
|
continue;
|
|
324757
325546
|
}
|
|
324758
325547
|
}
|
|
@@ -324814,7 +325603,7 @@ Exploration complete: ${summary}`);
|
|
|
324814
325603
|
continue;
|
|
324815
325604
|
}
|
|
324816
325605
|
if (toolName === "fill_credential" || toolName === "fill_credential_form") {
|
|
324817
|
-
if (emailVerificationUnavailable) {
|
|
325606
|
+
if (emailVerificationUnavailable || inboxState?.authDead) {
|
|
324818
325607
|
const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
|
|
324819
325608
|
if (codeGuard.blocked) {
|
|
324820
325609
|
logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
|
|
@@ -324908,7 +325697,7 @@ Exploration complete: ${summary}`);
|
|
|
324908
325697
|
continue;
|
|
324909
325698
|
}
|
|
324910
325699
|
}
|
|
324911
|
-
if (emailVerificationUnavailable) {
|
|
325700
|
+
if (emailVerificationUnavailable || inboxState?.authDead) {
|
|
324912
325701
|
const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
|
|
324913
325702
|
if (codeGuard.blocked) {
|
|
324914
325703
|
logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
|
|
@@ -326323,7 +327112,10 @@ var require_auth_capture_run = __commonJS({
|
|
|
326323
327112
|
exports.getAuthCaptureTimeoutMs = getAuthCaptureTimeoutMs;
|
|
326324
327113
|
exports.getAuthCaptureRunRetryMaxAttempts = getAuthCaptureRunRetryMaxAttempts;
|
|
326325
327114
|
exports.classifyCaptureMiss = classifyCaptureMiss;
|
|
327115
|
+
exports.harvestCollapsedAuthProof = harvestCollapsedAuthProof;
|
|
327116
|
+
exports.selectAuthLandingRoute = selectAuthLandingRoute;
|
|
326326
327117
|
exports.harvestAuthProofLive = harvestAuthProofLive;
|
|
327118
|
+
exports.detectRedirectBackCollapse = detectRedirectBackCollapse;
|
|
326327
327119
|
exports.findCredentialLiteralLeaks = findCredentialLiteralLeaks;
|
|
326328
327120
|
exports.runLoginCaptureRunWithOutcome = runLoginCaptureRunWithOutcome;
|
|
326329
327121
|
exports.runLoginCaptureRun = runLoginCaptureRun;
|
|
@@ -326444,35 +327236,26 @@ ${keyLines || "- (none provided)"}
|
|
|
326444
327236
|
return false;
|
|
326445
327237
|
return true;
|
|
326446
327238
|
}
|
|
326447
|
-
|
|
326448
|
-
|
|
326449
|
-
|
|
326450
|
-
|
|
326451
|
-
|
|
326452
|
-
|
|
326453
|
-
|
|
326454
|
-
|
|
326455
|
-
|
|
326456
|
-
|
|
326457
|
-
|
|
326458
|
-
|
|
326459
|
-
|
|
326460
|
-
|
|
326461
|
-
|
|
326462
|
-
|
|
326463
|
-
continue;
|
|
326464
|
-
for (const el of p.elements ?? []) {
|
|
326465
|
-
const nm = (el.label ?? "").trim().toLowerCase();
|
|
326466
|
-
if (el.role && nm)
|
|
326467
|
-
loginRoleNames.add(`${el.role}|${nm}`);
|
|
326468
|
-
if (el.testId)
|
|
326469
|
-
loginTestIds.add(el.testId);
|
|
326470
|
-
}
|
|
326471
|
-
}
|
|
327239
|
+
var LOGOUT_RE = /\b(log\s?out|sign\s?out)\b/i;
|
|
327240
|
+
var ACCOUNT_RE = /\b(account|profile|settings|avatar|notifications?|dashboard|inbox|my\s+account|user\s+menu)\b/i;
|
|
327241
|
+
var GREETING_RE = /\b(welcome|hello|hey|good\s+(morning|afternoon|evening|day))\b/i;
|
|
327242
|
+
function describeAuthProof(proof) {
|
|
327243
|
+
switch (proof.kind) {
|
|
327244
|
+
case "role":
|
|
327245
|
+
return `role:${proof.role}${proof.name ? `(${proof.name})` : ""}`;
|
|
327246
|
+
case "testid":
|
|
327247
|
+
return `testid:${proof.testId}`;
|
|
327248
|
+
case "endpoint":
|
|
327249
|
+
return `endpoint:${proof.url}`;
|
|
327250
|
+
case "url":
|
|
327251
|
+
return `url:${proof.route}`;
|
|
327252
|
+
}
|
|
327253
|
+
}
|
|
327254
|
+
function rankDurableAuthProof(landingElements, loginRoleNames, loginTestIds, credentials, logs2) {
|
|
326472
327255
|
const haveLoginDiff = loginRoleNames.size > 0 || loginTestIds.size > 0;
|
|
326473
327256
|
const roleNameFreq = /* @__PURE__ */ new Map();
|
|
326474
327257
|
const testIdFreq = /* @__PURE__ */ new Map();
|
|
326475
|
-
for (const el of
|
|
327258
|
+
for (const el of landingElements) {
|
|
326476
327259
|
const nm = (el.label ?? "").trim().toLowerCase();
|
|
326477
327260
|
if (el.role && nm)
|
|
326478
327261
|
roleNameFreq.set(`${el.role}|${nm}`, (roleNameFreq.get(`${el.role}|${nm}`) ?? 0) + 1);
|
|
@@ -326512,7 +327295,7 @@ ${keyLines || "- (none provided)"}
|
|
|
326512
327295
|
return true;
|
|
326513
327296
|
};
|
|
326514
327297
|
const ranked = [];
|
|
326515
|
-
for (const el of
|
|
327298
|
+
for (const el of landingElements) {
|
|
326516
327299
|
const label = (el.label ?? "").trim();
|
|
326517
327300
|
const role = el.role ?? "";
|
|
326518
327301
|
const testId = el.testId ?? "";
|
|
@@ -326524,7 +327307,7 @@ ${keyLines || "- (none provided)"}
|
|
|
326524
327307
|
const r = role === "link" ? "link" : "button";
|
|
326525
327308
|
ranked.push({ tier: 1, proof: { kind: "role", role: r, name: label }, desc: `${r} "${label}" (logout control)` });
|
|
326526
327309
|
}
|
|
326527
|
-
if (label && ACCOUNT_RE.test(label) && !personalized && uniqueRoleName && (role === "button" || role === "link") && absentOnLogin(roleNameKey, "")) {
|
|
327310
|
+
if (label && ACCOUNT_RE.test(label) && !personalized && uniqueRoleName && haveLoginDiff && (role === "button" || role === "link") && absentOnLogin(roleNameKey, "")) {
|
|
326528
327311
|
ranked.push({ tier: 2, proof: { kind: "role", role, name: label }, desc: `${role} "${label}" (account control)` });
|
|
326529
327312
|
}
|
|
326530
327313
|
if (testId && isSemanticTestId(testId) && uniqueTestId && haveLoginDiff && absentOnLogin("", testId)) {
|
|
@@ -326538,25 +327321,110 @@ ${keyLines || "- (none provided)"}
|
|
|
326538
327321
|
}
|
|
326539
327322
|
}
|
|
326540
327323
|
if (ranked.length === 0) {
|
|
326541
|
-
|
|
326542
|
-
const toProofPathname = (r) => {
|
|
326543
|
-
if (!r)
|
|
326544
|
-
return null;
|
|
326545
|
-
const p = r.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
326546
|
-
return p && p !== "/" && !isLoginPage(p) && p !== loginPathname ? p : null;
|
|
326547
|
-
};
|
|
326548
|
-
const urlRoute = toProofPathname(redirectRoute) ?? toProofPathname(landing.route);
|
|
326549
|
-
if (urlRoute) {
|
|
326550
|
-
logs2(` \u2713 url auth-proof: authenticated landing route ${urlRoute} (no durable element proof on the default-state page)`);
|
|
326551
|
-
return { kind: "url", route: urlRoute };
|
|
326552
|
-
}
|
|
326553
|
-
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)");
|
|
327324
|
+
logs2(" \u26A0 auth-proof: no durable login-absent element proof on the authenticated page (no url fallback \u2014 coverage deferred to a positive proof)");
|
|
326554
327325
|
return null;
|
|
326555
327326
|
}
|
|
326556
327327
|
ranked.sort((a, b) => a.tier - b.tier);
|
|
326557
327328
|
logs2(` \u2713 live auth-proof: ${ranked[0].desc}`);
|
|
326558
327329
|
return ranked[0].proof;
|
|
326559
327330
|
}
|
|
327331
|
+
function harvestCollapsedAuthProof(landingElements, loginAbsentElements, credentials, logs2) {
|
|
327332
|
+
const loginRoleNames = /* @__PURE__ */ new Set();
|
|
327333
|
+
const loginTestIds = /* @__PURE__ */ new Set();
|
|
327334
|
+
for (const el of loginAbsentElements) {
|
|
327335
|
+
const nm = (el.label ?? "").trim().toLowerCase();
|
|
327336
|
+
if (el.role && nm)
|
|
327337
|
+
loginRoleNames.add(`${el.role}|${nm}`);
|
|
327338
|
+
if (el.testId)
|
|
327339
|
+
loginTestIds.add(el.testId);
|
|
327340
|
+
}
|
|
327341
|
+
return rankDurableAuthProof(landingElements, loginRoleNames, loginTestIds, credentials, logs2);
|
|
327342
|
+
}
|
|
327343
|
+
function pickAuthLandingPage(siteMap, redirectRoute, loginRoute) {
|
|
327344
|
+
const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
|
|
327345
|
+
const authedPages = siteMap.pages.filter((p) => (p.requiresAuth || redirectRoute !== null && p.route === redirectRoute) && !isLoginPage(p.route));
|
|
327346
|
+
return (redirectRoute !== null ? authedPages.find((p) => p.route === redirectRoute) : void 0) ?? authedPages[0] ?? null;
|
|
327347
|
+
}
|
|
327348
|
+
function selectAuthLandingRoute(siteMap, redirectRoute, loginRoute) {
|
|
327349
|
+
return pickAuthLandingPage(siteMap, redirectRoute, loginRoute)?.route ?? null;
|
|
327350
|
+
}
|
|
327351
|
+
function harvestAuthProofLive(siteMap, redirectRoute, loginRoute, credentials, logs2) {
|
|
327352
|
+
const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
|
|
327353
|
+
const landing = pickAuthLandingPage(siteMap, redirectRoute, loginRoute);
|
|
327354
|
+
if (!landing) {
|
|
327355
|
+
logs2(" \u26A0 auth-proof: no authenticated landing page captured \u2014 cannot derive a durable proof");
|
|
327356
|
+
return null;
|
|
327357
|
+
}
|
|
327358
|
+
const loginRoleNames = /* @__PURE__ */ new Set();
|
|
327359
|
+
const loginTestIds = /* @__PURE__ */ new Set();
|
|
327360
|
+
for (const p of siteMap.pages) {
|
|
327361
|
+
if (!isLoginPage(p.route) || p.route === landing.route)
|
|
327362
|
+
continue;
|
|
327363
|
+
for (const el of p.elements ?? []) {
|
|
327364
|
+
const nm = (el.label ?? "").trim().toLowerCase();
|
|
327365
|
+
if (el.role && nm)
|
|
327366
|
+
loginRoleNames.add(`${el.role}|${nm}`);
|
|
327367
|
+
if (el.testId)
|
|
327368
|
+
loginTestIds.add(el.testId);
|
|
327369
|
+
}
|
|
327370
|
+
}
|
|
327371
|
+
return rankDurableAuthProof(landing.elements ?? [], loginRoleNames, loginTestIds, credentials, logs2);
|
|
327372
|
+
}
|
|
327373
|
+
var AUTH_ROUTE_RE = /\/(log[\s_-]?in|sign[\s_-]?in|sign[\s_-]?up|register|auth|forgot|reset)\b/i;
|
|
327374
|
+
var AUTO_NAV_TRIGGERS = /* @__PURE__ */ new Set(["AUTO", "REDIRECT"]);
|
|
327375
|
+
var USER_ACTION_TRIGGERS = /* @__PURE__ */ new Set(["CLICK", "SUBMIT", "NAVIGATION"]);
|
|
327376
|
+
var stripToBarePath = (route2) => {
|
|
327377
|
+
if (!route2)
|
|
327378
|
+
return null;
|
|
327379
|
+
let path = route2;
|
|
327380
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(route2)) {
|
|
327381
|
+
try {
|
|
327382
|
+
path = new URL(route2).pathname;
|
|
327383
|
+
} catch {
|
|
327384
|
+
}
|
|
327385
|
+
}
|
|
327386
|
+
const p = path.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
327387
|
+
return p || "/";
|
|
327388
|
+
};
|
|
327389
|
+
function detectRedirectBackCollapse(baseUrl, transitions) {
|
|
327390
|
+
let basePath;
|
|
327391
|
+
try {
|
|
327392
|
+
basePath = stripToBarePath(new URL(baseUrl).pathname);
|
|
327393
|
+
} catch {
|
|
327394
|
+
return null;
|
|
327395
|
+
}
|
|
327396
|
+
if (!basePath || basePath === "/" || AUTH_ROUTE_RE.test(basePath))
|
|
327397
|
+
return null;
|
|
327398
|
+
const userActionEdges = new Set(transitions.filter((t) => USER_ACTION_TRIGGERS.has(t.triggerType)).map((t) => `${stripToBarePath(t.fromRoute) ?? ""} ${stripToBarePath(t.toRoute) ?? ""}`));
|
|
327399
|
+
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) ?? ""}`));
|
|
327400
|
+
if (bounceIdx < 0)
|
|
327401
|
+
return null;
|
|
327402
|
+
const authRoute = stripToBarePath(transitions[bounceIdx].toRoute);
|
|
327403
|
+
if (!authRoute)
|
|
327404
|
+
return null;
|
|
327405
|
+
const returned = transitions.slice(bounceIdx + 1).some((t) => AUTH_ROUTE_RE.test(stripToBarePath(t.fromRoute) ?? "") && stripToBarePath(t.toRoute) === basePath);
|
|
327406
|
+
if (!returned)
|
|
327407
|
+
return null;
|
|
327408
|
+
return { basePath, authRoute };
|
|
327409
|
+
}
|
|
327410
|
+
function pickRouteElements(routeElements, wantRoute) {
|
|
327411
|
+
const want = stripToBarePath(wantRoute);
|
|
327412
|
+
for (const [route2, els] of routeElements) {
|
|
327413
|
+
if (stripToBarePath(route2) === want && (els?.length ?? 0) > 0)
|
|
327414
|
+
return els;
|
|
327415
|
+
}
|
|
327416
|
+
return [];
|
|
327417
|
+
}
|
|
327418
|
+
function pickLoginFormElements(routeElements, authRoute) {
|
|
327419
|
+
const exact = pickRouteElements(routeElements, authRoute);
|
|
327420
|
+
if (exact.length > 0)
|
|
327421
|
+
return exact;
|
|
327422
|
+
for (const [route2, els] of routeElements) {
|
|
327423
|
+
if (AUTH_ROUTE_RE.test(stripToBarePath(route2) ?? "") && (els?.length ?? 0) > 0)
|
|
327424
|
+
return els;
|
|
327425
|
+
}
|
|
327426
|
+
return [];
|
|
327427
|
+
}
|
|
326560
327428
|
function findCredentialLiteralLeaks(rendered, credentials) {
|
|
326561
327429
|
const leaked = [];
|
|
326562
327430
|
for (const [key, value] of Object.entries(credentials)) {
|
|
@@ -326569,12 +327437,22 @@ ${keyLines || "- (none provided)"}
|
|
|
326569
327437
|
return leaked;
|
|
326570
327438
|
}
|
|
326571
327439
|
async function verifyAuthRecipe(recipe, baseUrl, credentials, logs2) {
|
|
327440
|
+
const baselineRoute = recipe.redirectRoute ?? baseUrl;
|
|
326572
327441
|
const spec = [
|
|
326573
327442
|
`import { test, expect } from '@playwright/test';`,
|
|
326574
327443
|
``,
|
|
326575
327444
|
(0, auth_recipe_js_1.renderAuthFn)(recipe),
|
|
326576
327445
|
``,
|
|
326577
327446
|
`test('__vqa_auth_verify', async ({ page }) => {`,
|
|
327447
|
+
` // DIFFERENTIAL BASELINE (logged out): the durable proof must NOT already hold here, else it`,
|
|
327448
|
+
` // does not discriminate an authenticated session from an anonymous one \u2014 reject rather than`,
|
|
327449
|
+
` // publish a proof that passes while logged out (the cardinal invariant). The goto is NOT`,
|
|
327450
|
+
` // .catch-swallowed: a genuine navigation failure must fail the verify (fail-closed) rather than`,
|
|
327451
|
+
` // leave the baseline running on about:blank where the proof is vacuously "absent". An auth-gated`,
|
|
327452
|
+
` // route resolves fine (it follows the redirect to /login).`,
|
|
327453
|
+
` await page.goto(${JSON.stringify(baselineRoute)}, { waitUntil: 'domcontentloaded' });`,
|
|
327454
|
+
` ${(0, auth_recipe_js_1.renderProofBaselineCheck)(recipe.authProof, " ")}`,
|
|
327455
|
+
` // AUTHED: real login + the durable proof assertion (inside __vqaLogin).`,
|
|
326578
327456
|
` await __vqaLogin(page);`,
|
|
326579
327457
|
`});`,
|
|
326580
327458
|
``
|
|
@@ -326676,21 +327554,50 @@ ${keyLines || "- (none provided)"}
|
|
|
326676
327554
|
cmds: login.cmds.map((c) => (0, scrub_credentials_js_1.scrubCredentials)(c, credentials)),
|
|
326677
327555
|
preNavCmds: login.preNavCmds?.map((c) => (0, scrub_credentials_js_1.scrubCredentials)(c, credentials))
|
|
326678
327556
|
};
|
|
326679
|
-
|
|
327557
|
+
let authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
|
|
327558
|
+
let effectiveLogin = scrubbedLogin;
|
|
327559
|
+
if (authProof && (authProof.kind === "role" || authProof.kind === "testid")) {
|
|
327560
|
+
const landingRoute = selectAuthLandingRoute(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route);
|
|
327561
|
+
if (landingRoute)
|
|
327562
|
+
effectiveLogin = { ...effectiveLogin, redirectRoute: landingRoute };
|
|
327563
|
+
}
|
|
326680
327564
|
if (!authProof) {
|
|
326681
|
-
|
|
326682
|
-
|
|
327565
|
+
const collapse = detectRedirectBackCollapse(baseUrl, exploreResult.collectedTransitions);
|
|
327566
|
+
if (collapse) {
|
|
327567
|
+
const landingEls = pickRouteElements(handle.routeElements, collapse.basePath);
|
|
327568
|
+
const loginEls = pickLoginFormElements(handle.routeElements, collapse.authRoute);
|
|
327569
|
+
const collapsedProof = harvestCollapsedAuthProof(landingEls, loginEls, credentials, logs2);
|
|
327570
|
+
if (collapsedProof) {
|
|
327571
|
+
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)`);
|
|
327572
|
+
authProof = collapsedProof;
|
|
327573
|
+
effectiveLogin = { ...effectiveLogin, redirectRoute: collapse.basePath };
|
|
327574
|
+
} else {
|
|
327575
|
+
logs2(" \u26A0 redirect-back-to-entry collapse detected but no durable authed-only element differenced (no url fallback) \u2014 deferring to model-authored login");
|
|
327576
|
+
}
|
|
327577
|
+
}
|
|
326683
327578
|
}
|
|
326684
|
-
|
|
326685
|
-
|
|
326686
|
-
|
|
326687
|
-
|
|
326688
|
-
|
|
326689
|
-
|
|
327579
|
+
let identityEndpointProof = null;
|
|
327580
|
+
try {
|
|
327581
|
+
const cookies = await handle.page.context().cookies();
|
|
327582
|
+
if (cookies.some((c) => /(next-auth|authjs)\.session-token/.test(c.name))) {
|
|
327583
|
+
identityEndpointProof = { kind: "endpoint", url: "/api/auth/session" };
|
|
327584
|
+
logs2(" \u2139 NextAuth/Auth.js session cookie detected \u2014 trying an identity-endpoint proof (/api/auth/session) before the element proof");
|
|
327585
|
+
}
|
|
327586
|
+
} catch {
|
|
326690
327587
|
}
|
|
326691
|
-
const
|
|
326692
|
-
if (
|
|
326693
|
-
|
|
327588
|
+
const proofCandidates = [];
|
|
327589
|
+
if (identityEndpointProof)
|
|
327590
|
+
proofCandidates.push(identityEndpointProof);
|
|
327591
|
+
if (authProof)
|
|
327592
|
+
proofCandidates.push(authProof);
|
|
327593
|
+
if (proofCandidates.length === 0) {
|
|
327594
|
+
logs2(" \u26A0 auth unverified (no positive proof \u2014 no identity endpoint, no login-absent element) \u2014 falling back to model-authored login");
|
|
327595
|
+
return { recipe: null, outcome: exploreMiss("verify_failed") };
|
|
327596
|
+
}
|
|
327597
|
+
const loginText = [...effectiveLogin.cmds, ...effectiveLogin.preNavCmds ?? []].join("\n") + "\n" + JSON.stringify(effectiveLogin.replayPlan ?? []);
|
|
327598
|
+
const loginLeaks = findCredentialLiteralLeaks(loginText, credentials);
|
|
327599
|
+
if (loginLeaks.length > 0) {
|
|
327600
|
+
logs2(` \u26A0 auth recipe rejected: login sequence contains literal credential value(s) for ${loginLeaks.join(", ")} \u2014 falling back to model-authored login`);
|
|
326694
327601
|
return { recipe: null, outcome: "credential_leak" };
|
|
326695
327602
|
}
|
|
326696
327603
|
try {
|
|
@@ -326698,14 +327605,25 @@ ${keyLines || "- (none provided)"}
|
|
|
326698
327605
|
} catch {
|
|
326699
327606
|
}
|
|
326700
327607
|
handle = null;
|
|
326701
|
-
|
|
326702
|
-
|
|
326703
|
-
|
|
326704
|
-
|
|
327608
|
+
let lastReject = "no candidate verified";
|
|
327609
|
+
for (const proof of proofCandidates) {
|
|
327610
|
+
const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(effectiveLogin, proof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
|
|
327611
|
+
const renderLeaks = findCredentialLiteralLeaks((0, auth_recipe_js_1.renderAuthFn)(candidate), credentials);
|
|
327612
|
+
if (renderLeaks.length > 0) {
|
|
327613
|
+
lastReject = `${describeAuthProof(proof)} render coincidentally matched credential value(s) ${renderLeaks.join(", ")}`;
|
|
327614
|
+
logs2(` \u26A0 auth-proof candidate skipped (${lastReject}) \u2014 trying the next candidate`);
|
|
327615
|
+
continue;
|
|
327616
|
+
}
|
|
327617
|
+
const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
|
|
327618
|
+
if (ok) {
|
|
327619
|
+
logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${describeAuthProof(proof)}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
|
|
327620
|
+
return { recipe: candidate, outcome: "captured" };
|
|
327621
|
+
}
|
|
327622
|
+
lastReject = `${describeAuthProof(proof)} failed the differential verify`;
|
|
327623
|
+
logs2(` \u26A0 auth-proof candidate rejected (${lastReject}) \u2014 trying the next candidate`);
|
|
326705
327624
|
}
|
|
326706
|
-
|
|
326707
|
-
|
|
326708
|
-
return { recipe: candidate, outcome: "captured" };
|
|
327625
|
+
logs2(` \u26A0 auth unverified (all proof candidates failed: ${lastReject}) \u2014 falling back to model-authored login`);
|
|
327626
|
+
return { recipe: null, outcome: exploreMiss("verify_failed") };
|
|
326709
327627
|
} catch (err) {
|
|
326710
327628
|
logs2(` \u26A0 auth-capture run failed: ${err instanceof Error ? err.message : String(err)} \u2014 falling back to model-authored login`);
|
|
326711
327629
|
return { recipe: null, outcome: (0, ai_retry_js_1.isTransientRateError)(err) ? "rate_limited" : "error" };
|
|
@@ -333832,8 +334750,13 @@ Respond with ONLY valid JSON:
|
|
|
333832
334750
|
if (!(0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && ctx.mode !== "capture")
|
|
333833
334751
|
return null;
|
|
333834
334752
|
if (ctx.authRecipe) {
|
|
333835
|
-
|
|
333836
|
-
|
|
334753
|
+
if (ctx.authRecipe.authProof?.kind === "url") {
|
|
334754
|
+
pushLog(`auth-prologue: persisted recipe ${ctx.authRecipe.recipeHash} uses a demoted url proof \u2014 re-capturing with the differential pipeline (legacy invalidation)`);
|
|
334755
|
+
ctx.authRecipe = void 0;
|
|
334756
|
+
} else {
|
|
334757
|
+
pushLog(`auth-prologue: reusing persisted login recipe ${ctx.authRecipe.recipeHash} \u2014 skipping capture`);
|
|
334758
|
+
return Promise.resolve(ctx.authRecipe);
|
|
334759
|
+
}
|
|
333837
334760
|
}
|
|
333838
334761
|
const exploreModel = ctx.exploreModel ?? "";
|
|
333839
334762
|
const authCaptureModel = ctx.authCaptureModel ?? "";
|
|
@@ -349380,7 +350303,7 @@ var require_package4 = __commonJS({
|
|
|
349380
350303
|
"package.json"(exports, module) {
|
|
349381
350304
|
module.exports = {
|
|
349382
350305
|
name: "@validate.qa/runner",
|
|
349383
|
-
version: "1.0.
|
|
350306
|
+
version: "1.0.57",
|
|
349384
350307
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
349385
350308
|
bin: {
|
|
349386
350309
|
"validate-runner": "dist/cli.js"
|
|
@@ -350994,6 +351917,17 @@ async function submitCssKeyboardInCurrentContext(sessionPath, css) {
|
|
|
350994
351917
|
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) {
|
|
350995
351918
|
throw new Error(`WEBVIEW element not found for css=${css}`);
|
|
350996
351919
|
}
|
|
351920
|
+
try {
|
|
351921
|
+
const scriptResult = await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
351922
|
+
method: "POST",
|
|
351923
|
+
body: JSON.stringify({
|
|
351924
|
+
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';",
|
|
351925
|
+
args: [{ ELEMENT: elementId, "element-6066-11e4-a52e-4f735466cecf": elementId }]
|
|
351926
|
+
})
|
|
351927
|
+
});
|
|
351928
|
+
if (readValue(scriptResult) === "request-submit") return;
|
|
351929
|
+
} catch {
|
|
351930
|
+
}
|
|
350997
351931
|
await appiumRequest(`${sessionPath}/element/${elementId}/click`, {
|
|
350998
351932
|
method: "POST",
|
|
350999
351933
|
body: "{}"
|
|
@@ -351003,6 +351937,14 @@ async function submitCssKeyboardInCurrentContext(sessionPath, css) {
|
|
|
351003
351937
|
body: JSON.stringify({ text: WEBDRIVER_ENTER_KEY, value: [WEBDRIVER_ENTER_KEY] })
|
|
351004
351938
|
});
|
|
351005
351939
|
}
|
|
351940
|
+
async function hideKeyboardIfShown(sessionPath) {
|
|
351941
|
+
try {
|
|
351942
|
+
const shown = await appiumRequest(`${sessionPath}/appium/device/is_keyboard_shown`, { method: "GET" });
|
|
351943
|
+
if (readValue(shown) !== true) return;
|
|
351944
|
+
await appiumRequest(`${sessionPath}/appium/device/hide_keyboard`, { method: "POST", body: "{}" });
|
|
351945
|
+
} catch {
|
|
351946
|
+
}
|
|
351947
|
+
}
|
|
351006
351948
|
async function withWebViewContext(sessionPath, webviewName, body, options = {}) {
|
|
351007
351949
|
const previous = await getAppContext(sessionPath) ?? "NATIVE_APP";
|
|
351008
351950
|
let bodyCompleted = false;
|
|
@@ -355807,6 +356749,7 @@ var MobileBridgeDriver = class {
|
|
|
355807
356749
|
() => typeCssInCurrentContext(this.sessionPath, css, value),
|
|
355808
356750
|
this.webViewEntryOptions(webviewContext)
|
|
355809
356751
|
);
|
|
356752
|
+
await hideKeyboardIfShown(this.sessionPath);
|
|
355810
356753
|
});
|
|
355811
356754
|
}
|
|
355812
356755
|
async webSubmitKeyboard(webviewContext, css) {
|
|
@@ -355817,6 +356760,7 @@ var MobileBridgeDriver = class {
|
|
|
355817
356760
|
() => submitCssKeyboardInCurrentContext(this.sessionPath, css),
|
|
355818
356761
|
this.webViewEntryOptions(webviewContext)
|
|
355819
356762
|
);
|
|
356763
|
+
await hideKeyboardIfShown(this.sessionPath);
|
|
355820
356764
|
});
|
|
355821
356765
|
}
|
|
355822
356766
|
};
|
|
@@ -356256,7 +357200,7 @@ var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bea
|
|
|
356256
357200
|
var IS_CLOUD = parseBoolFlag(process.env.RUNNER_CLOUD);
|
|
356257
357201
|
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
356258
357202
|
var RUNNER_VERSION = runnerPackage.version ?? "0.0.0";
|
|
356259
|
-
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;
|
|
357203
|
+
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);
|
|
356260
357204
|
var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
|
|
356261
357205
|
var DEFAULT_TYPE_LIMITS = {
|
|
356262
357206
|
discovery: IS_CLOUD ? 2 : 15,
|
|
@@ -356907,7 +357851,9 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
356907
357851
|
}, assignedDeviceId, mobileLeaseError) {
|
|
356908
357852
|
const runMode = run2.mode ?? "run";
|
|
356909
357853
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
356910
|
-
|
|
357854
|
+
const runLabel = run2.testCaseId ?? run2.testName ?? (runMode === "run" ? "auth-setup" : runMode);
|
|
357855
|
+
const modeLabel = runMode === "run" ? RUNNER_MODE : runMode;
|
|
357856
|
+
log.info(`Running test: ${runLabel} (run ${run2.runId}) [mode: ${modeLabel}]`);
|
|
356911
357857
|
const liveBrowserHandle = import_runner_core11.liveBrowserRegistry.register({
|
|
356912
357858
|
sessionId: `${run2.runId}`,
|
|
356913
357859
|
mode: toLiveBrowserMode(runMode),
|
|
@@ -358741,9 +359687,15 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
358741
359687
|
}
|
|
358742
359688
|
const reproduceError = reproduce.error;
|
|
358743
359689
|
const modelOverride = run2.healerModel ?? void 0;
|
|
359690
|
+
const healDeadlineAt = Date.now() + Math.max(6e4, envInt("MOBILE_HEAL_DEADLINE_MS", 20 * 6e4));
|
|
358744
359691
|
const healerOptions = {
|
|
358745
359692
|
...modelOverride ? { model: modelOverride } : {},
|
|
359693
|
+
deadlineAt: healDeadlineAt,
|
|
358746
359694
|
runNativeTest,
|
|
359695
|
+
// Surface heal liveness: a Phase-1/Phase-2 heal can legitimately run
|
|
359696
|
+
// for many minutes of model calls with zero device traffic, which
|
|
359697
|
+
// previously printed nothing between the start banner and the verdict.
|
|
359698
|
+
onProgress: (message) => log.info(`\u{1FA7A} ${message}`),
|
|
358747
359699
|
selectorRegistryContext: run2.selectorRegistryContext ?? null,
|
|
358748
359700
|
runLiveHeal: async (a) => {
|
|
358749
359701
|
let liveSession = null;
|
|
@@ -358786,6 +359738,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
358786
359738
|
}
|
|
358787
359739
|
return bridgeDriver;
|
|
358788
359740
|
};
|
|
359741
|
+
if (Date.now() >= healDeadlineAt) {
|
|
359742
|
+
return {
|
|
359743
|
+
passed: false,
|
|
359744
|
+
verdict: "config_issue",
|
|
359745
|
+
healReason: "mobile heal deadline exceeded before opening the live Phase-2 session"
|
|
359746
|
+
};
|
|
359747
|
+
}
|
|
359748
|
+
log.info("\u{1FA7A} Phase 2 (live device) \u2014 opening the live observation session...");
|
|
358789
359749
|
try {
|
|
358790
359750
|
await openLiveSession();
|
|
358791
359751
|
const liveDriver = {
|
|
@@ -359631,12 +360591,29 @@ async function startRunner(opts) {
|
|
|
359631
360591
|
}
|
|
359632
360592
|
}, PIDS_WATCHDOG_INTERVAL_MS);
|
|
359633
360593
|
pidsWatchdog.unref();
|
|
360594
|
+
const deferralLogged = /* @__PURE__ */ new Map();
|
|
360595
|
+
const logDeferredOnce = (runId, line) => {
|
|
360596
|
+
if (deferralLogged.get(runId) === line) return;
|
|
360597
|
+
deferralLogged.set(runId, line);
|
|
360598
|
+
log.dim(line);
|
|
360599
|
+
};
|
|
360600
|
+
let lastPendingSummary = "";
|
|
359634
360601
|
while (!shuttingDown) {
|
|
359635
360602
|
try {
|
|
359636
360603
|
if (activeRuns < MAX_CONCURRENT) {
|
|
359637
360604
|
const pending = await fetchPendingRuns(serverUrl, headers);
|
|
360605
|
+
{
|
|
360606
|
+
const pendingIds = new Set(pending.map((r) => r.runId));
|
|
360607
|
+
for (const id of [...deferralLogged.keys()]) {
|
|
360608
|
+
if (!pendingIds.has(id)) deferralLogged.delete(id);
|
|
360609
|
+
}
|
|
360610
|
+
}
|
|
359638
360611
|
if (pending.length > 0) {
|
|
359639
|
-
|
|
360612
|
+
const pendingSummary = `${pending.length} pending run(s) found (${activeRuns}/${MAX_CONCURRENT} slots used)`;
|
|
360613
|
+
if (pendingSummary !== lastPendingSummary) {
|
|
360614
|
+
lastPendingSummary = pendingSummary;
|
|
360615
|
+
log.info(pendingSummary);
|
|
360616
|
+
}
|
|
359640
360617
|
const staggerMs = envInt("RUN_STAGGER_MS", DEFAULT_RUN_STAGGER_MS);
|
|
359641
360618
|
let claimedThisPoll = 0;
|
|
359642
360619
|
for (const run2 of pending) {
|
|
@@ -359649,7 +360626,7 @@ async function startRunner(opts) {
|
|
|
359649
360626
|
const runProject = projectKey(run2);
|
|
359650
360627
|
const projectLimit = projectCapacityFor(runProject, pending);
|
|
359651
360628
|
if (getActiveForProject(runProject) >= projectLimit) {
|
|
359652
|
-
|
|
360629
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
359653
360630
|
continue;
|
|
359654
360631
|
}
|
|
359655
360632
|
const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
@@ -359659,27 +360636,27 @@ async function startRunner(opts) {
|
|
|
359659
360636
|
browserPool: browserPool.stats()
|
|
359660
360637
|
});
|
|
359661
360638
|
if (!admission.allowed) {
|
|
359662
|
-
|
|
360639
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (${admission.reason})`);
|
|
359663
360640
|
continue;
|
|
359664
360641
|
}
|
|
359665
360642
|
if (enableMobile && run2.framework === "APPIUM" && getBridgeState().recordingActive) {
|
|
359666
|
-
|
|
360643
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active)`);
|
|
359667
360644
|
continue;
|
|
359668
360645
|
}
|
|
359669
360646
|
const discoveryMedium = runMode === "discovery" ? run2.discoveryContext?.medium : void 0;
|
|
359670
360647
|
if (discoveryMedium === "IOS_NATIVE" || discoveryMedium === "ANDROID_NATIVE") {
|
|
359671
360648
|
if (!enableMobile) {
|
|
359672
|
-
|
|
360649
|
+
logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (native mobile discovery needs a runner started with --mobile)`);
|
|
359673
360650
|
continue;
|
|
359674
360651
|
}
|
|
359675
360652
|
const caps = getRunnerCapabilities();
|
|
359676
360653
|
const hasDeviceCap = discoveryMedium === "IOS_NATIVE" ? caps.ios : caps.android;
|
|
359677
360654
|
if (!hasDeviceCap) {
|
|
359678
|
-
|
|
360655
|
+
logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (no booted ${discoveryMedium === "IOS_NATIVE" ? "iOS" : "Android"} device for native mobile discovery)`);
|
|
359679
360656
|
continue;
|
|
359680
360657
|
}
|
|
359681
360658
|
if (getBridgeState().hasActiveSession) {
|
|
359682
|
-
|
|
360659
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active on shared device)`);
|
|
359683
360660
|
continue;
|
|
359684
360661
|
}
|
|
359685
360662
|
}
|
|
@@ -359724,7 +360701,7 @@ async function startRunner(opts) {
|
|
|
359724
360701
|
activeMobileDiscoveries
|
|
359725
360702
|
});
|
|
359726
360703
|
if (!decision.claim) {
|
|
359727
|
-
|
|
360704
|
+
logDeferredOnce(run2.runId, ` ${decision.disposition === "pending" ? "Left PENDING" : "Deferred"} ${run2.runId} (${decision.reason})`);
|
|
359728
360705
|
continue;
|
|
359729
360706
|
}
|
|
359730
360707
|
leasedDeviceId = freeDeviceId;
|