@validate.qa/runner 1.0.56 → 1.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1255 -535
- package/dist/cli.mjs +1255 -535
- 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
|
|
@@ -3869,8 +3874,9 @@ function inlineLocatorBindings(stmts) {
|
|
|
3869
3874
|
function stripLoginScaffolding(code) {
|
|
3870
3875
|
const out = [];
|
|
3871
3876
|
let inAuth = false;
|
|
3877
|
+
let inInbox = false;
|
|
3872
3878
|
for (const ln of code.split("\n")) {
|
|
3873
|
-
if (!inAuth && AUTH_START.test(ln)) {
|
|
3879
|
+
if (!inAuth && !inInbox && AUTH_START.test(ln)) {
|
|
3874
3880
|
inAuth = true;
|
|
3875
3881
|
continue;
|
|
3876
3882
|
}
|
|
@@ -3879,6 +3885,15 @@ function stripLoginScaffolding(code) {
|
|
|
3879
3885
|
inAuth = false;
|
|
3880
3886
|
continue;
|
|
3881
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
|
+
}
|
|
3882
3897
|
if (LOGIN_CALL_LINE.test(ln))
|
|
3883
3898
|
continue;
|
|
3884
3899
|
out.push(ln);
|
|
@@ -4267,7 +4282,7 @@ function validateAssertionStrength(code, opts = {}) {
|
|
|
4267
4282
|
signals
|
|
4268
4283
|
};
|
|
4269
4284
|
}
|
|
4270
|
-
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;
|
|
4271
4286
|
var init_assertion_strength = __esm({
|
|
4272
4287
|
"../shared/dist/assertion-strength.js"() {
|
|
4273
4288
|
"use strict";
|
|
@@ -4317,6 +4332,8 @@ var init_assertion_strength = __esm({
|
|
|
4317
4332
|
AUTH_START = /^\s*\/\/\s*>>>\s*VALIDATE_QA_AUTH/;
|
|
4318
4333
|
AUTH_END = /^\s*\/\/\s*<<<\s*VALIDATE_QA_AUTH/;
|
|
4319
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/;
|
|
4320
4337
|
NEGATED_ASSERT = /\.not\.(toBeVisible|toContainText|toHaveText|toBeChecked)\b|\.toBeHidden\s*\(/;
|
|
4321
4338
|
TYPED_INPUT = /\.(fill|type|pressSequentially|setInputFiles)\s*\(/;
|
|
4322
4339
|
TITLE_LINE = /^\s*(?:test|describe)(?:\.\w+)?\s*\(/;
|
|
@@ -4359,10 +4376,7 @@ function isEnvironmentalError(run2) {
|
|
|
4359
4376
|
return false;
|
|
4360
4377
|
if (run2.status !== "ERROR")
|
|
4361
4378
|
return false;
|
|
4362
|
-
|
|
4363
|
-
if (typeof err !== "string")
|
|
4364
|
-
return false;
|
|
4365
|
-
return ENVIRONMENTAL_ERROR_PREFIXES.some((p) => err.startsWith(p));
|
|
4379
|
+
return classifyErrorMessage(run2.error) !== null;
|
|
4366
4380
|
}
|
|
4367
4381
|
function isCountedFailure(run2) {
|
|
4368
4382
|
if (!run2)
|
|
@@ -4381,9 +4395,10 @@ function environmentalReason(run2) {
|
|
|
4381
4395
|
function classifyErrorMessage(error2) {
|
|
4382
4396
|
if (typeof error2 !== "string")
|
|
4383
4397
|
return null;
|
|
4384
|
-
|
|
4398
|
+
const normalized = error2.replace(/^\s+/, "").replace(/^Error:\s*/i, "");
|
|
4399
|
+
if (normalized.startsWith("RATE_LIMIT_BLOCKED:"))
|
|
4385
4400
|
return "rate_limit";
|
|
4386
|
-
if (
|
|
4401
|
+
if (normalized.startsWith("CONFIG_ISSUE:"))
|
|
4387
4402
|
return "config_issue";
|
|
4388
4403
|
return null;
|
|
4389
4404
|
}
|
|
@@ -5266,6 +5281,12 @@ var init_auth_state_freshness = __esm({
|
|
|
5266
5281
|
function detectSkippedApiCoverage(steps) {
|
|
5267
5282
|
const allSkipped = steps.length > 0 && steps.every((s) => s.status === "skipped");
|
|
5268
5283
|
if (allSkipped) {
|
|
5284
|
+
if (steps.every((s) => s.skipKind === "fixme")) {
|
|
5285
|
+
return {
|
|
5286
|
+
detected: false,
|
|
5287
|
+
logLine: "All @api tests skipped, but EVERY skip is a deliberate fixme (intentional N/A \u2014 e.g. a UI-only redirect login) \u2014 NOT relabelling CONFIG_ISSUE"
|
|
5288
|
+
};
|
|
5289
|
+
}
|
|
5269
5290
|
return {
|
|
5270
5291
|
detected: true,
|
|
5271
5292
|
error: (
|
|
@@ -72605,7 +72626,7 @@ var require_verification_inbox = __commonJS({
|
|
|
72605
72626
|
};
|
|
72606
72627
|
})();
|
|
72607
72628
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
72608
|
-
exports.VerificationInboxReader = void 0;
|
|
72629
|
+
exports.VerificationInboxReader = exports.InboxAuthError = void 0;
|
|
72609
72630
|
exports.isVerificationInboxConfigured = isVerificationInboxConfigured;
|
|
72610
72631
|
exports.describeVerificationInboxConfigGap = describeVerificationInboxConfigGap;
|
|
72611
72632
|
exports.loadVerificationInboxConfig = loadVerificationInboxConfig;
|
|
@@ -72617,8 +72638,10 @@ var require_verification_inbox = __commonJS({
|
|
|
72617
72638
|
exports.applyPlusTag = applyPlusTag;
|
|
72618
72639
|
exports.deriveRegistrationPassword = deriveRegistrationPassword;
|
|
72619
72640
|
exports.extractVerificationCode = extractVerificationCode;
|
|
72641
|
+
exports.extractVerificationCodeDetailed = extractVerificationCodeDetailed;
|
|
72620
72642
|
exports.extractVerificationLink = extractVerificationLink;
|
|
72621
72643
|
exports.collectMessageUrls = collectMessageUrls;
|
|
72644
|
+
exports.isInboxAuthFailure = isInboxAuthFailure;
|
|
72622
72645
|
var node_crypto_1 = __require("crypto");
|
|
72623
72646
|
var DEFAULT_MAILBOX = "INBOX";
|
|
72624
72647
|
var DEFAULT_GMAIL_SPAM_MAILBOX = "[Gmail]/Spam";
|
|
@@ -72791,6 +72814,9 @@ var require_verification_inbox = __commonJS({
|
|
|
72791
72814
|
var CODE_TOKEN = new RegExp(`\\b(${CODE_ALT})\\b`, "gi");
|
|
72792
72815
|
var YEAR_CONTEXT = /(©|\(c\)|copyright|all rights reserved|®)/i;
|
|
72793
72816
|
function extractVerificationCode(text, html, subject) {
|
|
72817
|
+
return extractVerificationCodeDetailed(text, html, subject).code;
|
|
72818
|
+
}
|
|
72819
|
+
function extractVerificationCodeDetailed(text, html, subject) {
|
|
72794
72820
|
const subj = collapseSpacedDigits(subject || "");
|
|
72795
72821
|
const bodies = [text || "", stripHtml(html || "")].map(collapseSpacedDigits);
|
|
72796
72822
|
const withSubject = [subj, ...bodies];
|
|
@@ -72799,7 +72825,7 @@ var require_verification_inbox = __commonJS({
|
|
|
72799
72825
|
if (lone) {
|
|
72800
72826
|
const c = cleanCode(lone[1]);
|
|
72801
72827
|
if (c)
|
|
72802
|
-
return c;
|
|
72828
|
+
return { code: c, confidence: "subject" };
|
|
72803
72829
|
}
|
|
72804
72830
|
}
|
|
72805
72831
|
for (const body of withSubject) {
|
|
@@ -72811,26 +72837,28 @@ var require_verification_inbox = __commonJS({
|
|
|
72811
72837
|
continue;
|
|
72812
72838
|
const m = matchBestCode(line);
|
|
72813
72839
|
if (m)
|
|
72814
|
-
return m;
|
|
72840
|
+
return { code: m, confidence: "keyword" };
|
|
72815
72841
|
}
|
|
72816
72842
|
const windowed = body.match(new RegExp(`${CODE_KEYWORDS.source}[\\s\\S]{0,40}?\\b(${CODE_ALT})\\b`, "i"));
|
|
72817
72843
|
if (windowed && windowed[1]) {
|
|
72818
72844
|
const cleaned = cleanCode(windowed[1]);
|
|
72819
72845
|
if (cleaned && !looksLikeYearOrDate(cleaned))
|
|
72820
|
-
return cleaned;
|
|
72846
|
+
return { code: cleaned, confidence: "keyword" };
|
|
72821
72847
|
}
|
|
72822
72848
|
}
|
|
72849
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
72823
72850
|
for (const body of bodies) {
|
|
72824
72851
|
if (!body)
|
|
72825
72852
|
continue;
|
|
72826
|
-
const
|
|
72827
|
-
|
|
72828
|
-
|
|
72829
|
-
|
|
72830
|
-
if (real)
|
|
72831
|
-
return real;
|
|
72853
|
+
for (const n of body.match(/\b[0-9]{4,8}\b/g) ?? []) {
|
|
72854
|
+
if (!looksLikeYearOrDate(n))
|
|
72855
|
+
distinct.add(n);
|
|
72856
|
+
}
|
|
72832
72857
|
}
|
|
72833
|
-
|
|
72858
|
+
if (distinct.size === 1) {
|
|
72859
|
+
return { code: distinct.values().next().value ?? null, confidence: "fallback" };
|
|
72860
|
+
}
|
|
72861
|
+
return { code: null, confidence: null };
|
|
72834
72862
|
}
|
|
72835
72863
|
function matchBestCode(line) {
|
|
72836
72864
|
const hasYearContext = YEAR_CONTEXT.test(line);
|
|
@@ -72952,6 +72980,28 @@ var require_verification_inbox = __commonJS({
|
|
|
72952
72980
|
}
|
|
72953
72981
|
var MAX_WAIT_CEILING_MS = 18e4;
|
|
72954
72982
|
var MIN_POLL_MS = 2e3;
|
|
72983
|
+
var InboxAuthError = class extends Error {
|
|
72984
|
+
constructor(message) {
|
|
72985
|
+
super(message);
|
|
72986
|
+
this.name = "InboxAuthError";
|
|
72987
|
+
}
|
|
72988
|
+
};
|
|
72989
|
+
exports.InboxAuthError = InboxAuthError;
|
|
72990
|
+
function isInboxAuthFailure(error2) {
|
|
72991
|
+
if (!error2)
|
|
72992
|
+
return false;
|
|
72993
|
+
if (error2 instanceof InboxAuthError)
|
|
72994
|
+
return true;
|
|
72995
|
+
const e = error2;
|
|
72996
|
+
if (e.authenticationFailed === true)
|
|
72997
|
+
return true;
|
|
72998
|
+
const hay = `${e.responseText ?? ""} ${e.message ?? ""}`;
|
|
72999
|
+
return /\bAUTHENTICATIONFAILED\b|\bInvalid credentials\b/i.test(hay);
|
|
73000
|
+
}
|
|
73001
|
+
function isPlusTagged(address) {
|
|
73002
|
+
const at = address.indexOf("@");
|
|
73003
|
+
return at > 0 && address.slice(0, at).includes("+");
|
|
73004
|
+
}
|
|
72955
73005
|
var VerificationInboxReader = class {
|
|
72956
73006
|
config;
|
|
72957
73007
|
client = null;
|
|
@@ -72981,7 +73031,14 @@ var require_verification_inbox = __commonJS({
|
|
|
72981
73031
|
});
|
|
72982
73032
|
client.on("error", () => {
|
|
72983
73033
|
});
|
|
72984
|
-
|
|
73034
|
+
try {
|
|
73035
|
+
await client.connect();
|
|
73036
|
+
} catch (error2) {
|
|
73037
|
+
if (isInboxAuthFailure(error2)) {
|
|
73038
|
+
throw new InboxAuthError(`IMAP authentication rejected for ${this.config.user} \u2014 the inbox app password is revoked or wrong`);
|
|
73039
|
+
}
|
|
73040
|
+
throw error2;
|
|
73041
|
+
}
|
|
72985
73042
|
this.client = client;
|
|
72986
73043
|
return client;
|
|
72987
73044
|
})();
|
|
@@ -73026,7 +73083,10 @@ var require_verification_inbox = __commonJS({
|
|
|
73026
73083
|
} finally {
|
|
73027
73084
|
lock.release();
|
|
73028
73085
|
}
|
|
73029
|
-
} catch {
|
|
73086
|
+
} catch (error2) {
|
|
73087
|
+
if (isInboxAuthFailure(error2)) {
|
|
73088
|
+
throw error2 instanceof InboxAuthError ? error2 : new InboxAuthError(`IMAP authentication rejected for ${this.config.user} \u2014 the inbox app password is revoked or wrong`);
|
|
73089
|
+
}
|
|
73030
73090
|
return { uid: 0, uidValidity: 0 };
|
|
73031
73091
|
}
|
|
73032
73092
|
}
|
|
@@ -73077,23 +73137,52 @@ var require_verification_inbox = __commonJS({
|
|
|
73077
73137
|
}
|
|
73078
73138
|
}
|
|
73079
73139
|
/**
|
|
73080
|
-
* Poll until a
|
|
73081
|
-
*
|
|
73140
|
+
* Poll until a message with a STRONG verification signal (subject/keyword
|
|
73141
|
+
* code or a positive-scoring link) arrives, or the deadline passes.
|
|
73142
|
+
*
|
|
73143
|
+
* Signal-less mail addressed to the same recipient (a welcome email, a
|
|
73144
|
+
* receipt) is remembered but polled PAST — returning it immediately used to
|
|
73145
|
+
* hand welcome-mail order numbers to the caller as "the code" whenever the
|
|
73146
|
+
* welcome email out-raced the verification email. Only at the deadline is
|
|
73147
|
+
* the newest signal-less message surfaced (better than pretending the
|
|
73148
|
+
* mailbox is empty: the caller can still read it and decide).
|
|
73082
73149
|
*/
|
|
73083
73150
|
async waitForMessage(opts) {
|
|
73084
73151
|
const maxWaitMs = Math.min(opts.maxWaitMs ?? 9e4, MAX_WAIT_CEILING_MS);
|
|
73085
73152
|
const pollIntervalMs = Math.max(opts.pollIntervalMs ?? 5e3, MIN_POLL_MS);
|
|
73086
73153
|
const deadline = startTime() + maxWaitMs;
|
|
73154
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
|
|
73155
|
+
const satisfies = (m) => {
|
|
73156
|
+
if (opts.require === "code")
|
|
73157
|
+
return m.code !== null && m.codeConfidence !== "fallback";
|
|
73158
|
+
if (opts.require === "link")
|
|
73159
|
+
return m.link !== null;
|
|
73160
|
+
return m.hasVerificationSignal;
|
|
73161
|
+
};
|
|
73162
|
+
let weakest = null;
|
|
73163
|
+
const check2 = async () => {
|
|
73164
|
+
if (opts.stats)
|
|
73165
|
+
opts.stats.filteredOut = 0;
|
|
73166
|
+
const messages = await this.fetchMatching({ ...opts, limit }, true);
|
|
73167
|
+
if (!opts.require)
|
|
73168
|
+
return messages[0] ?? null;
|
|
73169
|
+
const strong = messages.find((m) => satisfies(m) && !m.senderMismatch) ?? messages.find(satisfies);
|
|
73170
|
+
if (strong)
|
|
73171
|
+
return strong;
|
|
73172
|
+
if (messages[0])
|
|
73173
|
+
weakest = messages[0];
|
|
73174
|
+
return null;
|
|
73175
|
+
};
|
|
73087
73176
|
do {
|
|
73088
|
-
const
|
|
73089
|
-
if (
|
|
73090
|
-
return
|
|
73177
|
+
const found = await check2();
|
|
73178
|
+
if (found)
|
|
73179
|
+
return found;
|
|
73091
73180
|
const remaining = deadline - startTime();
|
|
73092
73181
|
if (remaining <= 0)
|
|
73093
73182
|
break;
|
|
73094
73183
|
await sleep(Math.min(pollIntervalMs, remaining));
|
|
73095
73184
|
} while (startTime() < deadline);
|
|
73096
|
-
return
|
|
73185
|
+
return await check2() ?? weakest;
|
|
73097
73186
|
}
|
|
73098
73187
|
async fetchMatching(opts, withBody) {
|
|
73099
73188
|
const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
|
|
@@ -73107,7 +73196,7 @@ var require_verification_inbox = __commonJS({
|
|
|
73107
73196
|
throw e;
|
|
73108
73197
|
}
|
|
73109
73198
|
}
|
|
73110
|
-
results.sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
73199
|
+
results.sort((a, b) => Number(a.senderMismatch ?? false) - Number(b.senderMismatch ?? false) || Date.parse(b.date) - Date.parse(a.date));
|
|
73111
73200
|
return results.slice(0, limit);
|
|
73112
73201
|
}
|
|
73113
73202
|
async fetchMatchingInMailbox(opts, withBody, mailbox, limit) {
|
|
@@ -73115,6 +73204,7 @@ var require_verification_inbox = __commonJS({
|
|
|
73115
73204
|
const since = new Date(Math.max(0, opts.sinceMs));
|
|
73116
73205
|
const lock = await client.getMailboxLock(mailbox);
|
|
73117
73206
|
const results = [];
|
|
73207
|
+
const taggedRecipient = isPlusTagged(opts.toAddress);
|
|
73118
73208
|
try {
|
|
73119
73209
|
try {
|
|
73120
73210
|
await client.noop();
|
|
@@ -73158,17 +73248,23 @@ var require_verification_inbox = __commonJS({
|
|
|
73158
73248
|
const receivedMs = (parsed.date ?? msg.envelope?.date ?? /* @__PURE__ */ new Date(0)).getTime();
|
|
73159
73249
|
if (!useUid && receivedMs < opts.sinceMs - 6e4)
|
|
73160
73250
|
continue;
|
|
73161
|
-
|
|
73251
|
+
const filterActive = Boolean(opts.fromMatch || opts.subjectMatch);
|
|
73252
|
+
const filterMatched = (!opts.fromMatch || matchField(parsed.from?.text, opts.fromMatch)) && (!opts.subjectMatch || matchField(parsed.subject, opts.subjectMatch));
|
|
73253
|
+
if (filterActive && !filterMatched && !taggedRecipient) {
|
|
73254
|
+
if (opts.stats)
|
|
73255
|
+
opts.stats.filteredOut += 1;
|
|
73162
73256
|
continue;
|
|
73163
|
-
|
|
73164
|
-
|
|
73165
|
-
results.push({ uid: msg.uid, parsed, receivedMs });
|
|
73257
|
+
}
|
|
73258
|
+
results.push({ uid: msg.uid, parsed, receivedMs, senderMismatch: filterActive && !filterMatched });
|
|
73166
73259
|
}
|
|
73167
73260
|
} finally {
|
|
73168
73261
|
lock.release();
|
|
73169
73262
|
}
|
|
73170
|
-
results.sort((a, b) => b.receivedMs - a.receivedMs);
|
|
73171
|
-
return results.slice(0, limit).map((r) =>
|
|
73263
|
+
results.sort((a, b) => Number(a.senderMismatch) - Number(b.senderMismatch) || b.receivedMs - a.receivedMs);
|
|
73264
|
+
return results.slice(0, limit).map((r) => ({
|
|
73265
|
+
...toMessage(messageId(mailbox, String(r.uid), this.config.mailbox), r.parsed, withBody, mailbox),
|
|
73266
|
+
...r.senderMismatch ? { senderMismatch: true } : {}
|
|
73267
|
+
}));
|
|
73172
73268
|
}
|
|
73173
73269
|
async close() {
|
|
73174
73270
|
const client = this.client;
|
|
@@ -73199,12 +73295,18 @@ var require_verification_inbox = __commonJS({
|
|
|
73199
73295
|
const text = parsed.text || "";
|
|
73200
73296
|
const html = typeof parsed.html === "string" ? parsed.html : "";
|
|
73201
73297
|
const summary = toSummary({ uid, parsed, mailbox });
|
|
73298
|
+
const { code, confidence } = extractVerificationCodeDetailed(text, html, parsed.subject || "");
|
|
73299
|
+
const link = extractVerificationLink(text, html);
|
|
73202
73300
|
return {
|
|
73203
73301
|
...summary,
|
|
73204
73302
|
text: withBody ? text : "",
|
|
73205
73303
|
html: withBody ? html : "",
|
|
73206
|
-
code
|
|
73207
|
-
link
|
|
73304
|
+
code,
|
|
73305
|
+
link,
|
|
73306
|
+
codeConfidence: confidence,
|
|
73307
|
+
// A fallback-confidence code is NOT a signal: it is exactly the shape a
|
|
73308
|
+
// welcome/receipt email produces (one stray number, no keyword context).
|
|
73309
|
+
hasVerificationSignal: code !== null && confidence !== "fallback" || link !== null
|
|
73208
73310
|
};
|
|
73209
73311
|
}
|
|
73210
73312
|
function toSummary(input) {
|
|
@@ -73402,7 +73504,11 @@ var require_inbox_tools = __commonJS({
|
|
|
73402
73504
|
function err(message) {
|
|
73403
73505
|
return { content: JSON.stringify({ error: message }), didMutate: false };
|
|
73404
73506
|
}
|
|
73507
|
+
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.";
|
|
73405
73508
|
async function dispatchInboxTool(toolName, toolArgs, ctx) {
|
|
73509
|
+
if (ctx.state.authDead) {
|
|
73510
|
+
return err(`${toolName} skipped: ${INBOX_AUTH_DEAD_GUIDANCE}`);
|
|
73511
|
+
}
|
|
73406
73512
|
try {
|
|
73407
73513
|
switch (toolName) {
|
|
73408
73514
|
case "provision_identity":
|
|
@@ -73424,6 +73530,10 @@ var require_inbox_tools = __commonJS({
|
|
|
73424
73530
|
}
|
|
73425
73531
|
} catch (e) {
|
|
73426
73532
|
const message = e instanceof Error ? e.message : String(e);
|
|
73533
|
+
if ((0, verification_inbox_js_1.isInboxAuthFailure)(e)) {
|
|
73534
|
+
ctx.state.authDead = true;
|
|
73535
|
+
return err(`${toolName} failed: ${INBOX_AUTH_DEAD_GUIDANCE}`);
|
|
73536
|
+
}
|
|
73427
73537
|
return err(`${toolName} failed: ${message}`);
|
|
73428
73538
|
}
|
|
73429
73539
|
}
|
|
@@ -73481,6 +73591,7 @@ var require_inbox_tools = __commonJS({
|
|
|
73481
73591
|
return notReady;
|
|
73482
73592
|
const maxWaitSeconds = clampNumber(args.maxWaitSeconds, 90, 5, 180);
|
|
73483
73593
|
const { toAddress, altAddress } = inboxRecipients(ctx.state);
|
|
73594
|
+
const stats = { filteredOut: 0 };
|
|
73484
73595
|
const msg = await ctx.reader.waitForMessage({
|
|
73485
73596
|
toAddress,
|
|
73486
73597
|
altAddress,
|
|
@@ -73489,13 +73600,16 @@ var require_inbox_tools = __commonJS({
|
|
|
73489
73600
|
sinceUidValidity: ctx.state.sinceUidValidity,
|
|
73490
73601
|
fromMatch: asStr(args.senderMatch),
|
|
73491
73602
|
subjectMatch: asStr(args.subjectMatch),
|
|
73492
|
-
maxWaitMs: maxWaitSeconds * 1e3
|
|
73603
|
+
maxWaitMs: maxWaitSeconds * 1e3,
|
|
73604
|
+
stats
|
|
73493
73605
|
});
|
|
73494
73606
|
if (!msg) {
|
|
73607
|
+
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.` : "";
|
|
73495
73608
|
return ok({
|
|
73496
73609
|
found: false,
|
|
73497
73610
|
waitedSeconds: maxWaitSeconds,
|
|
73498
|
-
|
|
73611
|
+
...stats.filteredOut > 0 ? { filteredOut: stats.filteredOut } : {},
|
|
73612
|
+
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.`
|
|
73499
73613
|
}, ctx.maxResultChars);
|
|
73500
73614
|
}
|
|
73501
73615
|
rememberLink(ctx.state, msg);
|
|
@@ -73507,6 +73621,8 @@ var require_inbox_tools = __commonJS({
|
|
|
73507
73621
|
subject: msg.subject,
|
|
73508
73622
|
from: msg.from,
|
|
73509
73623
|
code: msg.code,
|
|
73624
|
+
...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." } : {},
|
|
73625
|
+
...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." } : {},
|
|
73510
73626
|
link,
|
|
73511
73627
|
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."
|
|
73512
73628
|
}, ctx.maxResultChars);
|
|
@@ -73640,20 +73756,42 @@ var require_inbox_tools = __commonJS({
|
|
|
73640
73756
|
return false;
|
|
73641
73757
|
if (host === "::1" || host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd"))
|
|
73642
73758
|
return false;
|
|
73643
|
-
|
|
73644
|
-
|
|
73645
|
-
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
73646
|
-
if (a === 127 || a === 0 || a === 10)
|
|
73647
|
-
return false;
|
|
73648
|
-
if (a === 169 && b === 254)
|
|
73649
|
-
return false;
|
|
73650
|
-
if (a === 192 && b === 168)
|
|
73651
|
-
return false;
|
|
73652
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
73759
|
+
if (host.includes(":")) {
|
|
73760
|
+
if (host === "::" || host === "::ffff:0:0")
|
|
73653
73761
|
return false;
|
|
73654
|
-
|
|
73762
|
+
const dotted = host.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
73763
|
+
if (dotted && !ipv4IsPublic(dotted[1]))
|
|
73655
73764
|
return false;
|
|
73765
|
+
const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
73766
|
+
if (mapped) {
|
|
73767
|
+
const hi = parseInt(mapped[1], 16);
|
|
73768
|
+
const lo = parseInt(mapped[2], 16);
|
|
73769
|
+
const ipv4 = `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
73770
|
+
if (!ipv4IsPublic(ipv4))
|
|
73771
|
+
return false;
|
|
73772
|
+
}
|
|
73656
73773
|
}
|
|
73774
|
+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) && !ipv4IsPublic(host))
|
|
73775
|
+
return false;
|
|
73776
|
+
return true;
|
|
73777
|
+
}
|
|
73778
|
+
function ipv4IsPublic(ipv4) {
|
|
73779
|
+
const m = ipv4.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
73780
|
+
if (!m)
|
|
73781
|
+
return true;
|
|
73782
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
73783
|
+
if (a > 255 || b > 255 || Number(m[3]) > 255 || Number(m[4]) > 255)
|
|
73784
|
+
return false;
|
|
73785
|
+
if (a === 127 || a === 0 || a === 10)
|
|
73786
|
+
return false;
|
|
73787
|
+
if (a === 169 && b === 254)
|
|
73788
|
+
return false;
|
|
73789
|
+
if (a === 192 && b === 168)
|
|
73790
|
+
return false;
|
|
73791
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
73792
|
+
return false;
|
|
73793
|
+
if (a >= 224)
|
|
73794
|
+
return false;
|
|
73657
73795
|
return true;
|
|
73658
73796
|
}
|
|
73659
73797
|
function clampNumber(v, dflt, min, max) {
|
|
@@ -75387,12 +75525,11 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
75387
75525
|
keyboardRecoveryScreenId = currentScreenId ?? null;
|
|
75388
75526
|
keyboardRecoverySameScreenTurns = 0;
|
|
75389
75527
|
}
|
|
75390
|
-
|
|
75391
|
-
if (keyboardRecoverySameScreenTurns >= 4 && keyboardLikelyOpen && iteration - lastKeyboardRecoveryIteration > 4) {
|
|
75528
|
+
if (keyboardRecoverySameScreenTurns >= 4 && iteration - lastKeyboardRecoveryIteration > 4) {
|
|
75392
75529
|
lastKeyboardRecoveryIteration = iteration;
|
|
75393
75530
|
try {
|
|
75394
75531
|
await (0, mobile_keyboard_js_1.dismissKeyboardForRetry)(driver);
|
|
75395
|
-
log2(" Auto-dismissed the on-screen keyboard (several turns stalled on this screen
|
|
75532
|
+
log2(" Auto-dismissed the on-screen keyboard (several turns stalled on this screen).");
|
|
75396
75533
|
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.");
|
|
75397
75534
|
} catch (error2) {
|
|
75398
75535
|
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(error2);
|
|
@@ -84168,6 +84305,7 @@ ${args.directive}`);
|
|
|
84168
84305
|
aiResponseCount += 1;
|
|
84169
84306
|
const parsed = parseHealVerdict(content);
|
|
84170
84307
|
if (!parsed) {
|
|
84308
|
+
options.onProgress?.(`Phase 1 attempt ${attempt}: response was not a valid verdict \u2014 granting a recovery turn`);
|
|
84171
84309
|
markRecoverableTurn();
|
|
84172
84310
|
pendingDirective = "Your last response was not valid JSON matching the verdict schema. Return exactly one JSON verdict object.";
|
|
84173
84311
|
continue;
|
|
@@ -85722,12 +85860,14 @@ var require_run_callback_proxy = __commonJS({
|
|
|
85722
85860
|
}
|
|
85723
85861
|
const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
|
|
85724
85862
|
const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
|
|
85863
|
+
const localPaths = new Map(Object.entries(input.localRoutes ?? {}).map(([suffix, handler]) => [`${runPath}/${suffix}`, handler]));
|
|
85725
85864
|
const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
|
|
85726
85865
|
const authHeader = `Bearer ${token}`;
|
|
85727
85866
|
const server3 = (0, node_http_1.createServer)(async (req, res) => {
|
|
85728
85867
|
try {
|
|
85729
85868
|
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
85730
|
-
|
|
85869
|
+
const localHandler = localPaths.get(requestUrl.pathname);
|
|
85870
|
+
if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) && !localHandler || requestUrl.search) {
|
|
85731
85871
|
sendJson(res, 404, { error: "Callback route not available" });
|
|
85732
85872
|
return;
|
|
85733
85873
|
}
|
|
@@ -85746,6 +85886,11 @@ var require_run_callback_proxy = __commonJS({
|
|
|
85746
85886
|
sendJson(res, 400, { error: "Callback body must be a JSON object" });
|
|
85747
85887
|
return;
|
|
85748
85888
|
}
|
|
85889
|
+
if (localHandler) {
|
|
85890
|
+
const out = await localHandler(parsedBody);
|
|
85891
|
+
sendJson(res, out.status, out.body);
|
|
85892
|
+
return;
|
|
85893
|
+
}
|
|
85749
85894
|
const upstreamBody = JSON.stringify({
|
|
85750
85895
|
...parsedBody,
|
|
85751
85896
|
...input.taskArn ? { taskArn: input.taskArn } : {},
|
|
@@ -85804,6 +85949,668 @@ var require_run_callback_proxy = __commonJS({
|
|
|
85804
85949
|
}
|
|
85805
85950
|
});
|
|
85806
85951
|
|
|
85952
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
85953
|
+
var require_discovery_lint_flags = __commonJS({
|
|
85954
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
85955
|
+
"use strict";
|
|
85956
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
85957
|
+
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
85958
|
+
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
85959
|
+
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
85960
|
+
exports.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
85961
|
+
function strictOn(value) {
|
|
85962
|
+
return value === "true" || value === "1";
|
|
85963
|
+
}
|
|
85964
|
+
function structuralLocatorLintMode() {
|
|
85965
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
85966
|
+
if (v === "warn")
|
|
85967
|
+
return "warn";
|
|
85968
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
85969
|
+
return "enforce";
|
|
85970
|
+
return "off";
|
|
85971
|
+
}
|
|
85972
|
+
function isPwMethodPreflightEnabled() {
|
|
85973
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
85974
|
+
}
|
|
85975
|
+
function isVqaLoginDedupeEnabled() {
|
|
85976
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
85977
|
+
}
|
|
85978
|
+
function isTestgenEmailVerifyEnabled() {
|
|
85979
|
+
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
85980
|
+
}
|
|
85981
|
+
}
|
|
85982
|
+
});
|
|
85983
|
+
|
|
85984
|
+
// ../runner-core/dist/services/auth-recipe.js
|
|
85985
|
+
var require_auth_recipe = __commonJS({
|
|
85986
|
+
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
85987
|
+
"use strict";
|
|
85988
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
85989
|
+
exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = void 0;
|
|
85990
|
+
exports.computeRecipeHash = computeRecipeHash;
|
|
85991
|
+
exports.buildAuthRecipe = buildAuthRecipe;
|
|
85992
|
+
exports.authProofLocator = authProofLocator;
|
|
85993
|
+
exports.urlProofPredicateExpr = urlProofPredicateExpr;
|
|
85994
|
+
exports.renderAuthProofAssertion = renderAuthProofAssertion;
|
|
85995
|
+
exports.renderProofBaselineCheck = renderProofBaselineCheck;
|
|
85996
|
+
exports.renderAuthFn = renderAuthFn;
|
|
85997
|
+
exports.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
|
|
85998
|
+
exports.absoluteRoute = absoluteRoute;
|
|
85999
|
+
exports.renderReplayClickArrow = renderReplayClickArrow;
|
|
86000
|
+
exports.renderReplaySettleArrow = renderReplaySettleArrow;
|
|
86001
|
+
exports.renderAuthUrlWaitLine = renderAuthUrlWaitLine;
|
|
86002
|
+
exports.commandToReplayStep = commandToReplayStep;
|
|
86003
|
+
exports.buildReplayPlan = buildReplayPlan;
|
|
86004
|
+
exports.extractAuthRegion = extractAuthRegion;
|
|
86005
|
+
exports.stripAuthRegions = stripAuthRegions;
|
|
86006
|
+
exports.ensureExpectImported = ensureExpectImported;
|
|
86007
|
+
exports.insertAfterPwImport = insertAfterPwImport;
|
|
86008
|
+
exports.ensureLoginCall = ensureLoginCall;
|
|
86009
|
+
exports.upsertAuthBlock = upsertAuthBlock;
|
|
86010
|
+
exports.buildHealAuthStamp = buildHealAuthStamp;
|
|
86011
|
+
var crypto_1 = __require("crypto");
|
|
86012
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
86013
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
86014
|
+
exports.AUTH_MARKER_VERSION = "v3";
|
|
86015
|
+
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
86016
|
+
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
|
|
86017
|
+
var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
|
|
86018
|
+
var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
|
|
86019
|
+
function computeRecipeHash(input) {
|
|
86020
|
+
const canonical = JSON.stringify({
|
|
86021
|
+
preNavRoute: input.preNavRoute,
|
|
86022
|
+
preNavCmds: input.preNavCmds,
|
|
86023
|
+
route: input.route,
|
|
86024
|
+
cmds: input.cmds,
|
|
86025
|
+
redirectRoute: input.redirectRoute,
|
|
86026
|
+
authProof: input.authProof
|
|
86027
|
+
});
|
|
86028
|
+
return (0, crypto_1.createHash)("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
86029
|
+
}
|
|
86030
|
+
function buildAuthRecipe(login, authProof, verifiedAt) {
|
|
86031
|
+
const preNavRoute = login.preNavRoute ?? "/";
|
|
86032
|
+
const preNavCmds = login.preNavCmds ?? [];
|
|
86033
|
+
const recipeHash = computeRecipeHash({
|
|
86034
|
+
preNavRoute,
|
|
86035
|
+
preNavCmds,
|
|
86036
|
+
route: login.route,
|
|
86037
|
+
cmds: login.cmds,
|
|
86038
|
+
redirectRoute: login.redirectRoute,
|
|
86039
|
+
authProof
|
|
86040
|
+
});
|
|
86041
|
+
return {
|
|
86042
|
+
version: 1,
|
|
86043
|
+
recipeHash,
|
|
86044
|
+
preNavRoute,
|
|
86045
|
+
preNavCmds,
|
|
86046
|
+
route: login.route,
|
|
86047
|
+
cmds: login.cmds,
|
|
86048
|
+
redirectRoute: login.redirectRoute,
|
|
86049
|
+
authProof,
|
|
86050
|
+
replayPlan: login.replayPlan ?? [],
|
|
86051
|
+
verifiedAt
|
|
86052
|
+
};
|
|
86053
|
+
}
|
|
86054
|
+
function authProofLocator(proof) {
|
|
86055
|
+
if (proof.kind === "url" || proof.kind === "endpoint") {
|
|
86056
|
+
throw new Error(`authProofLocator: ${proof.kind} proof has no element locator; callers must branch on proof.kind`);
|
|
86057
|
+
}
|
|
86058
|
+
if (proof.kind === "testid") {
|
|
86059
|
+
return `page.getByTestId(${JSON.stringify(proof.testId)})`;
|
|
86060
|
+
}
|
|
86061
|
+
if (proof.name) {
|
|
86062
|
+
return `page.getByRole(${JSON.stringify(proof.role)}, { name: ${JSON.stringify(proof.name)} })`;
|
|
86063
|
+
}
|
|
86064
|
+
return `page.getByRole(${JSON.stringify(proof.role)}).first()`;
|
|
86065
|
+
}
|
|
86066
|
+
function stripToPathname(route2) {
|
|
86067
|
+
return route2.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/";
|
|
86068
|
+
}
|
|
86069
|
+
function urlProofPredicateExpr(route2, exact = false) {
|
|
86070
|
+
const p = stripToPathname(route2);
|
|
86071
|
+
if (p === "/")
|
|
86072
|
+
return `() => false`;
|
|
86073
|
+
const r = JSON.stringify(p);
|
|
86074
|
+
if (exact)
|
|
86075
|
+
return `(u) => u.pathname === ${r} || u.pathname === ${r} + '/'`;
|
|
86076
|
+
return `(u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/')`;
|
|
86077
|
+
}
|
|
86078
|
+
function renderStableExactUrlProof(pred, tabs) {
|
|
86079
|
+
return [
|
|
86080
|
+
`await page.waitForURL(${pred}, { timeout: 15000 });`,
|
|
86081
|
+
`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)');`,
|
|
86082
|
+
`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');`
|
|
86083
|
+
].join(`
|
|
86084
|
+
${tabs}`);
|
|
86085
|
+
}
|
|
86086
|
+
var ENDPOINT_ORIGIN_EXPR = `(page.url().match(/^https?:\\/\\/[^/]+/) || [''])[0]`;
|
|
86087
|
+
function renderEndpointProbe(url, tabs, mode, originMode) {
|
|
86088
|
+
const u = JSON.stringify(url);
|
|
86089
|
+
const target = originMode === "pageurl" ? `${ENDPOINT_ORIGIN_EXPR} + ${u}` : u;
|
|
86090
|
+
const lines = [
|
|
86091
|
+
`const __vqaResp = await page.request.get(${target});`,
|
|
86092
|
+
`const __vqaJson = __vqaResp.ok() ? await __vqaResp.json().catch(() => null) : null;`,
|
|
86093
|
+
`const __vqaUser = __vqaJson && (__vqaJson.user ?? __vqaJson.data?.user ?? __vqaJson);`,
|
|
86094
|
+
`const __vqaAuthed = !!(__vqaUser && (__vqaUser.id || __vqaUser.email || __vqaUser.sub));`,
|
|
86095
|
+
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');`
|
|
86096
|
+
];
|
|
86097
|
+
return lines.join(`
|
|
86098
|
+
${tabs}`);
|
|
86099
|
+
}
|
|
86100
|
+
function renderAuthProofAssertion(proof) {
|
|
86101
|
+
if (proof.kind === "endpoint") {
|
|
86102
|
+
return renderEndpointProbe(proof.url, " ", "authed", "baseurl");
|
|
86103
|
+
}
|
|
86104
|
+
if (proof.kind === "url") {
|
|
86105
|
+
const pred = urlProofPredicateExpr(proof.route, proof.exact);
|
|
86106
|
+
if (proof.exact)
|
|
86107
|
+
return renderStableExactUrlProof(pred, " ");
|
|
86108
|
+
return `await page.waitForURL(${pred}, { timeout: 15000 });`;
|
|
86109
|
+
}
|
|
86110
|
+
return `await expect(${authProofLocator(proof)}).toBeVisible({ timeout: 15000 });`;
|
|
86111
|
+
}
|
|
86112
|
+
var BASELINE_VISIBLE_TIMEOUT_MS = 15e3;
|
|
86113
|
+
function renderProofBaselineCheck(proof, tabs) {
|
|
86114
|
+
if (proof.kind === "endpoint") {
|
|
86115
|
+
return renderEndpointProbe(proof.url, tabs, "anon", "baseurl");
|
|
86116
|
+
}
|
|
86117
|
+
if (proof.kind === "url") {
|
|
86118
|
+
const pred = urlProofPredicateExpr(proof.route, proof.exact);
|
|
86119
|
+
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');`;
|
|
86120
|
+
}
|
|
86121
|
+
const loc = authProofLocator(proof);
|
|
86122
|
+
return [
|
|
86123
|
+
`if (await ${loc}.waitFor({ state: 'visible', timeout: ${BASELINE_VISIBLE_TIMEOUT_MS} }).then(() => true, () => false)) {`,
|
|
86124
|
+
` throw new Error('__vqa auth-baseline: authed-only proof visible while logged out \u2014 not a discriminating proof');`,
|
|
86125
|
+
`}`
|
|
86126
|
+
].join(`
|
|
86127
|
+
${tabs}`);
|
|
86128
|
+
}
|
|
86129
|
+
function normalizeCmd(cmd) {
|
|
86130
|
+
let c = cmd.trim();
|
|
86131
|
+
if (c.endsWith(";"))
|
|
86132
|
+
c = c.slice(0, -1);
|
|
86133
|
+
return ` ${c};`;
|
|
86134
|
+
}
|
|
86135
|
+
function renderAuthFn(recipe, opts) {
|
|
86136
|
+
const lines = [];
|
|
86137
|
+
lines.push(START_MARKER);
|
|
86138
|
+
lines.push(`async function __vqaLogin(page) {`);
|
|
86139
|
+
if (opts?.warmGuard) {
|
|
86140
|
+
const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
|
|
86141
|
+
const loginRoute = recipe.route ?? recipe.preNavRoute;
|
|
86142
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
|
|
86143
|
+
lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
|
|
86144
|
+
if (recipe.authProof.kind === "endpoint") {
|
|
86145
|
+
lines.push(` try {`);
|
|
86146
|
+
lines.push(` ${renderEndpointProbe(recipe.authProof.url, " ", "authed", "baseurl")}`);
|
|
86147
|
+
lines.push(` return;`);
|
|
86148
|
+
lines.push(` } catch {}`);
|
|
86149
|
+
} else if (recipe.authProof.kind === "url") {
|
|
86150
|
+
const pred = urlProofPredicateExpr(recipe.authProof.route, recipe.authProof.exact);
|
|
86151
|
+
lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
|
|
86152
|
+
lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
|
|
86153
|
+
} else if (loginRoute) {
|
|
86154
|
+
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
|
|
86155
|
+
} else {
|
|
86156
|
+
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
|
|
86157
|
+
}
|
|
86158
|
+
lines.push(` }`);
|
|
86159
|
+
}
|
|
86160
|
+
lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
|
|
86161
|
+
for (const cmd of recipe.preNavCmds)
|
|
86162
|
+
lines.push(normalizeCmd(cmd));
|
|
86163
|
+
if (recipe.preNavCmds.length === 0 && recipe.route && recipe.route !== recipe.preNavRoute) {
|
|
86164
|
+
lines.push(` await page.goto(${JSON.stringify(recipe.route)}, { waitUntil: 'domcontentloaded' });`);
|
|
86165
|
+
}
|
|
86166
|
+
for (const cmd of recipe.cmds)
|
|
86167
|
+
lines.push(normalizeCmd(cmd));
|
|
86168
|
+
lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
|
|
86169
|
+
lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
|
|
86170
|
+
if (opts?.warmGuard) {
|
|
86171
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
|
|
86172
|
+
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) {`);
|
|
86173
|
+
lines.push(` try {`);
|
|
86174
|
+
lines.push(` const __vqaState = await page.context().storageState();`);
|
|
86175
|
+
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 });`);
|
|
86176
|
+
lines.push(` } catch {}`);
|
|
86177
|
+
lines.push(` }`);
|
|
86178
|
+
}
|
|
86179
|
+
lines.push(`}`);
|
|
86180
|
+
lines.push(END_MARKER);
|
|
86181
|
+
return lines.join("\n");
|
|
86182
|
+
}
|
|
86183
|
+
exports.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
|
|
86184
|
+
"AUTH_FILE",
|
|
86185
|
+
"SHARED_AUTH_INJECT",
|
|
86186
|
+
"VALIDATE_RUN_ID",
|
|
86187
|
+
"VALIDATE_CLAIM_EPOCH",
|
|
86188
|
+
"VALIDATE_AUTH_CONFIG_HASH",
|
|
86189
|
+
"VALIDATE_TASK_ARN",
|
|
86190
|
+
"VALIDATE_RUNNER_AUTH",
|
|
86191
|
+
"VALIDATE_SERVER_URL",
|
|
86192
|
+
// Register-and-verify tests (VALIDATE_QA_INBOX region, inbox-stamp.ts): the
|
|
86193
|
+
// per-run minted registration address + its DERIVED throwaway-app password,
|
|
86194
|
+
// injected by the parent's test-inbox-broker — never project variables.
|
|
86195
|
+
"VQA_TEST_INBOX_EMAIL",
|
|
86196
|
+
"VQA_TEST_ACCOUNT_PASSWORD"
|
|
86197
|
+
]);
|
|
86198
|
+
function isReservedRunnerEnvVar(key) {
|
|
86199
|
+
return exports.RESERVED_RUNNER_ENV_VARS.has(key);
|
|
86200
|
+
}
|
|
86201
|
+
function absoluteRoute(route2, baseUrl) {
|
|
86202
|
+
try {
|
|
86203
|
+
return new URL(route2, baseUrl).href;
|
|
86204
|
+
} catch {
|
|
86205
|
+
return route2;
|
|
86206
|
+
}
|
|
86207
|
+
}
|
|
86208
|
+
function renderReplayClickArrow(step) {
|
|
86209
|
+
const loc = step.locator.trim();
|
|
86210
|
+
if (step.kind === "wait") {
|
|
86211
|
+
return `async (page) => {
|
|
86212
|
+
await ${loc}.waitFor({ state: 'visible', timeout: 15000 });
|
|
86213
|
+
}`;
|
|
86214
|
+
}
|
|
86215
|
+
if (step.kind === "press") {
|
|
86216
|
+
return `async (page) => {
|
|
86217
|
+
await ${loc}.press(${JSON.stringify(step.key ?? "Enter")});
|
|
86218
|
+
}`;
|
|
86219
|
+
}
|
|
86220
|
+
return `async (page) => {
|
|
86221
|
+
await ${loc}.click();
|
|
86222
|
+
}`;
|
|
86223
|
+
}
|
|
86224
|
+
function renderReplaySettleArrow(proof) {
|
|
86225
|
+
if (proof.kind === "endpoint") {
|
|
86226
|
+
return `async (page) => {
|
|
86227
|
+
${renderEndpointProbe(proof.url, " ", "authed", "pageurl")}
|
|
86228
|
+
}`;
|
|
86229
|
+
}
|
|
86230
|
+
if (proof.kind === "url") {
|
|
86231
|
+
const pred = urlProofPredicateExpr(proof.route, proof.exact);
|
|
86232
|
+
const body = proof.exact ? renderStableExactUrlProof(pred, " ") : `await page.waitForURL(${pred}, { timeout: 15000 });`;
|
|
86233
|
+
return `async (page) => {
|
|
86234
|
+
${body}
|
|
86235
|
+
}`;
|
|
86236
|
+
}
|
|
86237
|
+
return `async (page) => {
|
|
86238
|
+
await ${authProofLocator(proof)}.waitFor({ state: 'visible', timeout: 15000 });
|
|
86239
|
+
}`;
|
|
86240
|
+
}
|
|
86241
|
+
function renderAuthUrlWaitLine(redirectRoute) {
|
|
86242
|
+
const opts = `{ waitUntil: 'domcontentloaded', timeout: 8000 }`;
|
|
86243
|
+
const tail = `.catch(() => { /* URL drift \u2014 the auth-proof assertion below is the gate */ });`;
|
|
86244
|
+
const rr = redirectRoute ? stripToPathname(redirectRoute) : null;
|
|
86245
|
+
if (rr && rr !== "/") {
|
|
86246
|
+
const r = JSON.stringify(rr);
|
|
86247
|
+
return ` await page.waitForURL((u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/'), ${opts})${tail}`;
|
|
86248
|
+
}
|
|
86249
|
+
if (rr === "/") {
|
|
86250
|
+
return ` await page.waitForURL((u) => !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
|
|
86251
|
+
}
|
|
86252
|
+
return ` await page.waitForURL((u) => u.pathname !== '/' && !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
|
|
86253
|
+
}
|
|
86254
|
+
function locatorChainOf(cmd) {
|
|
86255
|
+
const m = cmd.match(/\bpage\.[\s\S]*?(?=\.(?:fill|click|check|press|selectOption|setChecked|waitFor)\s*\()/);
|
|
86256
|
+
return m ? m[0].trim() : null;
|
|
86257
|
+
}
|
|
86258
|
+
function parseRoleName(locator) {
|
|
86259
|
+
const role = locator.match(/getByRole\(\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
86260
|
+
const name = locator.match(/getByRole\([^)]*name:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByLabel\(\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByPlaceholder\(\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
86261
|
+
return { role, name };
|
|
86262
|
+
}
|
|
86263
|
+
function credentialKeyOf(cmd) {
|
|
86264
|
+
return cmd.match(/\.fill\(\s*process\.env\.([A-Za-z_$][\w$]*)!?\s*\)/)?.[1] ?? null;
|
|
86265
|
+
}
|
|
86266
|
+
function commandToReplayStep(cmd) {
|
|
86267
|
+
const chain = locatorChainOf(cmd);
|
|
86268
|
+
if (!chain)
|
|
86269
|
+
return null;
|
|
86270
|
+
const credKey = credentialKeyOf(cmd);
|
|
86271
|
+
if (cmd.includes(".fill(")) {
|
|
86272
|
+
if (!credKey)
|
|
86273
|
+
return null;
|
|
86274
|
+
const { role, name } = parseRoleName(chain);
|
|
86275
|
+
return { kind: "fill", credentialKey: credKey, locator: chain, role, name, element: name ?? role ?? "credential field" };
|
|
86276
|
+
}
|
|
86277
|
+
if (/\.waitFor\s*\(/.test(cmd))
|
|
86278
|
+
return { kind: "wait", locator: chain };
|
|
86279
|
+
const pressKey = cmd.match(/\.press\(\s*['"`]([^'"`]+)['"`]\s*\)/)?.[1];
|
|
86280
|
+
if (pressKey)
|
|
86281
|
+
return { kind: "press", locator: chain, key: pressKey };
|
|
86282
|
+
if (/\.(?:click|check|setChecked|selectOption)\s*\(/.test(cmd))
|
|
86283
|
+
return { kind: "click", locator: chain };
|
|
86284
|
+
return null;
|
|
86285
|
+
}
|
|
86286
|
+
function buildReplayPlan(login) {
|
|
86287
|
+
const plan = [];
|
|
86288
|
+
const preNavRoute = login.preNavRoute ?? "/";
|
|
86289
|
+
plan.push({ kind: "nav", route: preNavRoute });
|
|
86290
|
+
for (const c of login.preNavCmds ?? []) {
|
|
86291
|
+
const step = commandToReplayStep(c);
|
|
86292
|
+
if (step)
|
|
86293
|
+
plan.push(step);
|
|
86294
|
+
}
|
|
86295
|
+
if ((login.preNavCmds ?? []).length === 0 && login.route && login.route !== preNavRoute) {
|
|
86296
|
+
plan.push({ kind: "nav", route: login.route });
|
|
86297
|
+
}
|
|
86298
|
+
for (const c of login.cmds) {
|
|
86299
|
+
const step = commandToReplayStep(c);
|
|
86300
|
+
if (step)
|
|
86301
|
+
plan.push(step);
|
|
86302
|
+
}
|
|
86303
|
+
return plan;
|
|
86304
|
+
}
|
|
86305
|
+
function extractAuthRegion(code) {
|
|
86306
|
+
const m = code.match(new RegExp(AUTH_REGION_RE.source));
|
|
86307
|
+
return m ? m[0] : null;
|
|
86308
|
+
}
|
|
86309
|
+
function countAuthRegions(code) {
|
|
86310
|
+
const m = code.match(AUTH_REGION_RE);
|
|
86311
|
+
return m ? m.length : 0;
|
|
86312
|
+
}
|
|
86313
|
+
function stripAuthRegions(code) {
|
|
86314
|
+
return code.replace(AUTH_REGION_RE, "");
|
|
86315
|
+
}
|
|
86316
|
+
var PW_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?/;
|
|
86317
|
+
function ensureExpectImported(code) {
|
|
86318
|
+
const m = code.match(PW_IMPORT_RE);
|
|
86319
|
+
if (!m)
|
|
86320
|
+
return code;
|
|
86321
|
+
const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
86322
|
+
if (names.includes("expect"))
|
|
86323
|
+
return code;
|
|
86324
|
+
const next = [...names, "expect"];
|
|
86325
|
+
return code.replace(PW_IMPORT_RE, `import { ${next.join(", ")} } from '@playwright/test';`);
|
|
86326
|
+
}
|
|
86327
|
+
function insertAfterPwImport(code, block) {
|
|
86328
|
+
const m = code.match(PW_IMPORT_RE);
|
|
86329
|
+
if (!m || m.index === void 0) {
|
|
86330
|
+
return `${block}
|
|
86331
|
+
|
|
86332
|
+
${code.replace(/^\r?\n+/, "")}`;
|
|
86333
|
+
}
|
|
86334
|
+
const end = m.index + m[0].length;
|
|
86335
|
+
const head = code.slice(0, end);
|
|
86336
|
+
const tail = code.slice(end).replace(/^\r?\n+/, "");
|
|
86337
|
+
return `${head}
|
|
86338
|
+
|
|
86339
|
+
${block}
|
|
86340
|
+
|
|
86341
|
+
${tail}`;
|
|
86342
|
+
}
|
|
86343
|
+
var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
|
|
86344
|
+
var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
|
|
86345
|
+
function ensureLoginCall(code) {
|
|
86346
|
+
if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
|
|
86347
|
+
return code;
|
|
86348
|
+
let result = code;
|
|
86349
|
+
if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
|
|
86350
|
+
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
86351
|
+
}
|
|
86352
|
+
const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
|
|
86353
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
86354
|
+
const m = matches[i];
|
|
86355
|
+
if (m.index === void 0)
|
|
86356
|
+
continue;
|
|
86357
|
+
const insertAt = m.index + m[0].length;
|
|
86358
|
+
const following = result.slice(insertAt, insertAt + 60);
|
|
86359
|
+
if (/^\s*await\s+__vqaLogin\s*\(\s*page\s*\)/.test(following))
|
|
86360
|
+
continue;
|
|
86361
|
+
result = `${result.slice(0, insertAt)}
|
|
86362
|
+
await __vqaLogin(page);${result.slice(insertAt)}`;
|
|
86363
|
+
}
|
|
86364
|
+
return result;
|
|
86365
|
+
}
|
|
86366
|
+
function upsertAuthBlock(code, ctx) {
|
|
86367
|
+
if (ctx.mode === "generate") {
|
|
86368
|
+
if (!ctx.recipe || !ctx.isAuthNeeded)
|
|
86369
|
+
return code;
|
|
86370
|
+
let result = stripAuthRegions(code);
|
|
86371
|
+
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
86372
|
+
result = ensureExpectImported(result);
|
|
86373
|
+
const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
|
|
86374
|
+
result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
|
|
86375
|
+
result = ensureLoginCall(result);
|
|
86376
|
+
const n = countAuthRegions(result);
|
|
86377
|
+
if (n !== 1) {
|
|
86378
|
+
throw new Error(`upsertAuthBlock(generate): expected exactly 1 auth region after stamp, got ${n}`);
|
|
86379
|
+
}
|
|
86380
|
+
if (!LOGIN_CALL_RE.test(result)) {
|
|
86381
|
+
throw new Error("upsertAuthBlock(generate): stamped __vqaLogin but found no test() opener to inject the call");
|
|
86382
|
+
}
|
|
86383
|
+
return result;
|
|
86384
|
+
}
|
|
86385
|
+
try {
|
|
86386
|
+
if (!ctx.preservedAuthRegion)
|
|
86387
|
+
return code;
|
|
86388
|
+
let result = ensureExpectImported(code);
|
|
86389
|
+
if (countAuthRegions(result) >= 1) {
|
|
86390
|
+
result = result.replace(AUTH_REGION_RE, "");
|
|
86391
|
+
result = insertAfterPwImport(result, ctx.preservedAuthRegion);
|
|
86392
|
+
} else {
|
|
86393
|
+
result = insertAfterPwImport(result, ctx.preservedAuthRegion);
|
|
86394
|
+
}
|
|
86395
|
+
result = ensureLoginCall(result);
|
|
86396
|
+
if (countAuthRegions(result) !== 1) {
|
|
86397
|
+
throw new Error("heal re-stamp produced non-unique auth region");
|
|
86398
|
+
}
|
|
86399
|
+
return result;
|
|
86400
|
+
} catch (err) {
|
|
86401
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
86402
|
+
ctx.onSignal?.({ type: "auth-stamp-degraded", reason });
|
|
86403
|
+
return code;
|
|
86404
|
+
}
|
|
86405
|
+
}
|
|
86406
|
+
function buildHealAuthStamp(originalCode, failureError, logs2) {
|
|
86407
|
+
const region = extractAuthRegion(originalCode);
|
|
86408
|
+
const loginFailed = !!failureError && /__vqaLogin/.test(failureError);
|
|
86409
|
+
return {
|
|
86410
|
+
mode: "heal",
|
|
86411
|
+
preservedAuthRegion: region && !loginFailed ? region : null,
|
|
86412
|
+
onSignal: logs2 ? (s) => logs2.push(`\u26A0 auth-stamp ${s.type}: ${s.reason}`) : void 0
|
|
86413
|
+
};
|
|
86414
|
+
}
|
|
86415
|
+
}
|
|
86416
|
+
});
|
|
86417
|
+
|
|
86418
|
+
// ../runner-core/dist/services/inbox-stamp.js
|
|
86419
|
+
var require_inbox_stamp = __commonJS({
|
|
86420
|
+
"../runner-core/dist/services/inbox-stamp.js"(exports) {
|
|
86421
|
+
"use strict";
|
|
86422
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86423
|
+
exports.INBOX_HELPER_TOKEN_RE = exports.INBOX_HELPER_NAME = exports.INBOX_REGION_RE = exports.INBOX_MARKER_VERSION = void 0;
|
|
86424
|
+
exports.codeUsesInboxHelper = codeUsesInboxHelper;
|
|
86425
|
+
exports.stripInboxRegions = stripInboxRegions;
|
|
86426
|
+
exports.renderInboxFn = renderInboxFn;
|
|
86427
|
+
exports.upsertInboxBlock = upsertInboxBlock;
|
|
86428
|
+
var auth_recipe_js_1 = require_auth_recipe();
|
|
86429
|
+
exports.INBOX_MARKER_VERSION = "v1";
|
|
86430
|
+
var START_MARKER = `// >>> VALIDATE_QA_INBOX ${exports.INBOX_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
86431
|
+
var END_MARKER = `// <<< VALIDATE_QA_INBOX ${exports.INBOX_MARKER_VERSION} <<<`;
|
|
86432
|
+
exports.INBOX_REGION_RE = /\/\/ >>> VALIDATE_QA_INBOX[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_INBOX[^\n]*\r?\n?/g;
|
|
86433
|
+
exports.INBOX_HELPER_NAME = "__vqaWaitForEmail";
|
|
86434
|
+
exports.INBOX_HELPER_TOKEN_RE = /__vqaWaitForEmail\s*\(/;
|
|
86435
|
+
function codeUsesInboxHelper(code) {
|
|
86436
|
+
if (!code)
|
|
86437
|
+
return false;
|
|
86438
|
+
return exports.INBOX_HELPER_TOKEN_RE.test(code) || /VALIDATE_QA_INBOX/.test(code);
|
|
86439
|
+
}
|
|
86440
|
+
function stripInboxRegions(code) {
|
|
86441
|
+
return code.replace(exports.INBOX_REGION_RE, "");
|
|
86442
|
+
}
|
|
86443
|
+
function countInboxRegions(code) {
|
|
86444
|
+
const m = code.match(exports.INBOX_REGION_RE);
|
|
86445
|
+
return m ? m.length : 0;
|
|
86446
|
+
}
|
|
86447
|
+
function renderInboxFn() {
|
|
86448
|
+
const lines = [
|
|
86449
|
+
START_MARKER,
|
|
86450
|
+
`// Reads THIS run's verification email via the runner's inbox broker. The`,
|
|
86451
|
+
`// IMAP credential stays in the runner parent; this helper can only see mail`,
|
|
86452
|
+
`// addressed to the run's own registration address (VQA_TEST_INBOX_EMAIL).`,
|
|
86453
|
+
`// kind: 'code' \u2192 the one-time code, 'link' \u2192 the verification URL,`,
|
|
86454
|
+
`// 'either' / omitted \u2192 { code, link, subject, from } for flexible flows.`,
|
|
86455
|
+
`async function __vqaWaitForEmail(kind, opts) {`,
|
|
86456
|
+
` const want = kind === 'code' || kind === 'link' ? kind : 'either';`,
|
|
86457
|
+
` if (!process.env.VQA_TEST_INBOX_EMAIL || !process.env.VALIDATE_SERVER_URL || !process.env.VALIDATE_RUN_ID) {`,
|
|
86458
|
+
` throw new Error('CONFIG_ISSUE: email verification unavailable - this runner has no verification inbox, so a register-and-verify test cannot run here');`,
|
|
86459
|
+
` }`,
|
|
86460
|
+
` const timeoutMs = Math.min(Math.max((opts && opts.timeoutMs) || 150000, 5000), 170000);`,
|
|
86461
|
+
` const res = await fetch(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + encodeURIComponent(process.env.VALIDATE_RUN_ID) + '/inbox-wait', {`,
|
|
86462
|
+
` method: 'POST',`,
|
|
86463
|
+
` headers: { authorization: process.env.VALIDATE_RUNNER_AUTH || '', 'content-type': 'application/json' },`,
|
|
86464
|
+
` body: JSON.stringify({ kind: want, timeoutMs: timeoutMs }),`,
|
|
86465
|
+
` });`,
|
|
86466
|
+
` if (res.status === 404) {`,
|
|
86467
|
+
` throw new Error('CONFIG_ISSUE: email verification unavailable - the runner inbox broker is not active for this run');`,
|
|
86468
|
+
` }`,
|
|
86469
|
+
` let data = null;`,
|
|
86470
|
+
` try { data = await res.json(); } catch (e) { data = null; }`,
|
|
86471
|
+
` if (!res.ok || !data) { throw new Error('inbox broker request failed (status ' + res.status + ')'); }`,
|
|
86472
|
+
` if (!data.found) {`,
|
|
86473
|
+
` if (data.error) { throw new Error('CONFIG_ISSUE: verification inbox failed on this runner - ' + data.error); }`,
|
|
86474
|
+
` 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');`,
|
|
86475
|
+
` }`,
|
|
86476
|
+
` if (want === 'code' && !data.code) {`,
|
|
86477
|
+
` throw new Error('VERIFICATION_EMAIL_HAS_NO_CODE: an email arrived (subject: ' + JSON.stringify(data.subject || '') + ') but no one-time code could be extracted');`,
|
|
86478
|
+
` }`,
|
|
86479
|
+
` if (want === 'link' && !data.link) {`,
|
|
86480
|
+
` throw new Error('VERIFICATION_EMAIL_HAS_NO_LINK: an email arrived (subject: ' + JSON.stringify(data.subject || '') + ') but no verification link could be extracted');`,
|
|
86481
|
+
` }`,
|
|
86482
|
+
` return want === 'code' ? data.code : want === 'link' ? data.link : data;`,
|
|
86483
|
+
`}`,
|
|
86484
|
+
END_MARKER
|
|
86485
|
+
];
|
|
86486
|
+
return lines.join("\n");
|
|
86487
|
+
}
|
|
86488
|
+
function upsertInboxBlock(code) {
|
|
86489
|
+
const stripped = stripInboxRegions(code);
|
|
86490
|
+
if (!exports.INBOX_HELPER_TOKEN_RE.test(stripped))
|
|
86491
|
+
return stripped;
|
|
86492
|
+
const result = (0, auth_recipe_js_1.insertAfterPwImport)(stripped, renderInboxFn());
|
|
86493
|
+
const n = countInboxRegions(result);
|
|
86494
|
+
if (n !== 1) {
|
|
86495
|
+
throw new Error(`upsertInboxBlock: expected exactly 1 inbox region after stamp, got ${n}`);
|
|
86496
|
+
}
|
|
86497
|
+
return result;
|
|
86498
|
+
}
|
|
86499
|
+
}
|
|
86500
|
+
});
|
|
86501
|
+
|
|
86502
|
+
// ../runner-core/dist/services/test-inbox-broker.js
|
|
86503
|
+
var require_test_inbox_broker = __commonJS({
|
|
86504
|
+
"../runner-core/dist/services/test-inbox-broker.js"(exports) {
|
|
86505
|
+
"use strict";
|
|
86506
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86507
|
+
exports.INBOX_WAIT_ROUTE = exports.TEST_INBOX_PASSWORD_ENV = exports.TEST_INBOX_EMAIL_ENV = void 0;
|
|
86508
|
+
exports.deriveTestRunInboxTag = deriveTestRunInboxTag;
|
|
86509
|
+
exports.mintTestRunInboxIdentity = mintTestRunInboxIdentity;
|
|
86510
|
+
exports.decideInboxWaitResponse = decideInboxWaitResponse;
|
|
86511
|
+
exports.startTestInboxBroker = startTestInboxBroker;
|
|
86512
|
+
var node_crypto_1 = __require("crypto");
|
|
86513
|
+
var verification_inbox_js_1 = require_verification_inbox();
|
|
86514
|
+
var inbox_tools_js_1 = require_inbox_tools();
|
|
86515
|
+
exports.TEST_INBOX_EMAIL_ENV = "VQA_TEST_INBOX_EMAIL";
|
|
86516
|
+
exports.TEST_INBOX_PASSWORD_ENV = "VQA_TEST_ACCOUNT_PASSWORD";
|
|
86517
|
+
exports.INBOX_WAIT_ROUTE = "inbox-wait";
|
|
86518
|
+
var MIN_WAIT_MS = 5e3;
|
|
86519
|
+
var MAX_WAIT_MS = 17e4;
|
|
86520
|
+
var DEFAULT_WAIT_MS = 15e4;
|
|
86521
|
+
function deriveTestRunInboxTag(seed, projectId, runId) {
|
|
86522
|
+
const hostKey = (0, verification_inbox_js_1.seedToHostKey)(seed || "");
|
|
86523
|
+
const hash = (0, node_crypto_1.createHash)("sha256").update(`testrun|${hostKey}|${projectId ?? ""}|${runId}`).digest("hex").slice(0, 10);
|
|
86524
|
+
return `vqat${hash}`;
|
|
86525
|
+
}
|
|
86526
|
+
function mintTestRunInboxIdentity(config, input) {
|
|
86527
|
+
const tag = deriveTestRunInboxTag(input.baseUrl, input.projectId, input.runId);
|
|
86528
|
+
const email = (0, verification_inbox_js_1.applyPlusTag)(config.address, tag);
|
|
86529
|
+
return { email, password: (0, verification_inbox_js_1.deriveRegistrationPassword)(email), tag };
|
|
86530
|
+
}
|
|
86531
|
+
function decideInboxWaitResponse(msg, publicLink) {
|
|
86532
|
+
const code = msg && msg.code && msg.codeConfidence !== "fallback" ? msg.code : null;
|
|
86533
|
+
const link = publicLink;
|
|
86534
|
+
if (!msg || !code && !link)
|
|
86535
|
+
return { found: false };
|
|
86536
|
+
return { found: true, code, link, subject: msg.subject, from: msg.from };
|
|
86537
|
+
}
|
|
86538
|
+
async function startTestInboxBroker(opts) {
|
|
86539
|
+
const identity = mintTestRunInboxIdentity(opts.config, opts);
|
|
86540
|
+
const reader = new verification_inbox_js_1.VerificationInboxReader(opts.config);
|
|
86541
|
+
const sinceMs = Date.now();
|
|
86542
|
+
let sinceUid = 0;
|
|
86543
|
+
let sinceUidValidity = 0;
|
|
86544
|
+
let authDeadReason = null;
|
|
86545
|
+
try {
|
|
86546
|
+
const wm = await reader.getUidWatermark();
|
|
86547
|
+
sinceUid = wm.uid;
|
|
86548
|
+
sinceUidValidity = wm.uidValidity;
|
|
86549
|
+
} catch (error2) {
|
|
86550
|
+
if ((0, verification_inbox_js_1.isInboxAuthFailure)(error2)) {
|
|
86551
|
+
authDeadReason = error2 instanceof Error ? error2.message : String(error2);
|
|
86552
|
+
opts.log?.(`\u26A0 Inbox broker: IMAP authentication rejected at watermark probe \u2014 email waits will fail as CONFIG_ISSUE (${authDeadReason})`);
|
|
86553
|
+
} else {
|
|
86554
|
+
opts.log?.("\u26A0 Inbox broker: UID watermark unavailable, falling back to the time floor");
|
|
86555
|
+
}
|
|
86556
|
+
}
|
|
86557
|
+
opts.log?.(`\u{1F4EC} Inbox broker active for ${identity.email} (uid watermark ${sinceUid || "none"})`);
|
|
86558
|
+
let waiting = false;
|
|
86559
|
+
const handleWait = async (body) => {
|
|
86560
|
+
if (authDeadReason) {
|
|
86561
|
+
return { status: 200, body: { found: false, error: "inbox authentication failed (revoked/wrong app password)" } };
|
|
86562
|
+
}
|
|
86563
|
+
if (waiting) {
|
|
86564
|
+
return { status: 200, body: { found: false, error: "another verification wait is already in progress for this run" } };
|
|
86565
|
+
}
|
|
86566
|
+
waiting = true;
|
|
86567
|
+
const rawTimeout = typeof body.timeoutMs === "number" && Number.isFinite(body.timeoutMs) ? body.timeoutMs : DEFAULT_WAIT_MS;
|
|
86568
|
+
const timeoutMs = Math.min(Math.max(rawTimeout, MIN_WAIT_MS), MAX_WAIT_MS);
|
|
86569
|
+
const require2 = body.kind === "code" ? "code" : body.kind === "link" ? "link" : "signal";
|
|
86570
|
+
try {
|
|
86571
|
+
const stats = { filteredOut: 0 };
|
|
86572
|
+
const msg = await reader.waitForMessage({
|
|
86573
|
+
toAddress: identity.email,
|
|
86574
|
+
sinceMs,
|
|
86575
|
+
sinceUid,
|
|
86576
|
+
sinceUidValidity,
|
|
86577
|
+
maxWaitMs: timeoutMs,
|
|
86578
|
+
require: require2,
|
|
86579
|
+
stats
|
|
86580
|
+
});
|
|
86581
|
+
const publicLink = msg && msg.link && (0, inbox_tools_js_1.isPublicHttpUrl)(msg.link) ? msg.link : null;
|
|
86582
|
+
const decision = decideInboxWaitResponse(msg, publicLink);
|
|
86583
|
+
if (!decision.found) {
|
|
86584
|
+
opts.log?.(`\u{1F4ED} Inbox broker: no usable verification email for ${identity.tag} within ${Math.round(timeoutMs / 1e3)}s`);
|
|
86585
|
+
return { status: 200, body: { found: false } };
|
|
86586
|
+
}
|
|
86587
|
+
opts.log?.(`\u{1F4EC} Inbox broker: email for ${identity.tag} (code=${decision.code ? "yes" : "no"}, link=${decision.link ? "yes" : "no"})`);
|
|
86588
|
+
return { status: 200, body: { ...decision } };
|
|
86589
|
+
} catch (error2) {
|
|
86590
|
+
if ((0, verification_inbox_js_1.isInboxAuthFailure)(error2)) {
|
|
86591
|
+
authDeadReason = error2 instanceof Error ? error2.message : String(error2);
|
|
86592
|
+
return { status: 200, body: { found: false, error: "inbox authentication failed (revoked/wrong app password)" } };
|
|
86593
|
+
}
|
|
86594
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
86595
|
+
opts.log?.(`\u26A0 Inbox broker wait failed: ${message}`);
|
|
86596
|
+
return { status: 200, body: { found: false, error: `inbox read failed: ${message}` } };
|
|
86597
|
+
} finally {
|
|
86598
|
+
waiting = false;
|
|
86599
|
+
}
|
|
86600
|
+
};
|
|
86601
|
+
return {
|
|
86602
|
+
identity,
|
|
86603
|
+
childEnv: () => ({
|
|
86604
|
+
[exports.TEST_INBOX_EMAIL_ENV]: identity.email,
|
|
86605
|
+
[exports.TEST_INBOX_PASSWORD_ENV]: identity.password
|
|
86606
|
+
}),
|
|
86607
|
+
handleWait,
|
|
86608
|
+
close: () => reader.close()
|
|
86609
|
+
};
|
|
86610
|
+
}
|
|
86611
|
+
}
|
|
86612
|
+
});
|
|
86613
|
+
|
|
85807
86614
|
// ../runner-core/dist/services/playwright-native.js
|
|
85808
86615
|
var require_playwright_native = __commonJS({
|
|
85809
86616
|
"../runner-core/dist/services/playwright-native.js"(exports) {
|
|
@@ -85861,6 +86668,9 @@ var require_playwright_native = __commonJS({
|
|
|
85861
86668
|
var har_stream_js_1 = require_har_stream();
|
|
85862
86669
|
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
85863
86670
|
var run_callback_proxy_js_1 = require_run_callback_proxy();
|
|
86671
|
+
var inbox_stamp_js_1 = require_inbox_stamp();
|
|
86672
|
+
var test_inbox_broker_js_1 = require_test_inbox_broker();
|
|
86673
|
+
var verification_inbox_js_1 = require_verification_inbox();
|
|
85864
86674
|
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
85865
86675
|
Object.defineProperty(exports, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
85866
86676
|
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
@@ -85895,17 +86705,20 @@ var require_playwright_native = __commonJS({
|
|
|
85895
86705
|
let runDir = null;
|
|
85896
86706
|
let standingNeedleMatcher = null;
|
|
85897
86707
|
let callbackProxy;
|
|
86708
|
+
let inboxBroker;
|
|
85898
86709
|
try {
|
|
85899
86710
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
85900
86711
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
85901
86712
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
86713
|
+
const usesInboxHelper = !isApiTest2 && (0, inbox_stamp_js_1.codeUsesInboxHelper)(playwrightCode);
|
|
85902
86714
|
const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
|
|
85903
86715
|
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
85904
86716
|
explicit: extraHttpHeaders,
|
|
85905
86717
|
standingHeadersMode: effectiveStandingMode,
|
|
85906
86718
|
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
85907
86719
|
});
|
|
85908
|
-
|
|
86720
|
+
const needleValues = resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : [];
|
|
86721
|
+
standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(needleValues);
|
|
85909
86722
|
const cssDisableSetup = `
|
|
85910
86723
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
85911
86724
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -86144,7 +86957,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
86144
86957
|
module.exports = {
|
|
86145
86958
|
testDir: ${JSON.stringify(absTestDir)},
|
|
86146
86959
|
outputDir: ${JSON.stringify(absOutputDir)},
|
|
86147
|
-
timeout:
|
|
86960
|
+
timeout: ${usesInboxHelper ? 24e4 : 6e4},
|
|
86148
86961
|
retries: 0,
|
|
86149
86962
|
use: {
|
|
86150
86963
|
baseURL: ${JSON.stringify(baseUrl)},
|
|
@@ -86170,6 +86983,27 @@ module.exports = {
|
|
|
86170
86983
|
const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
|
|
86171
86984
|
const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
|
|
86172
86985
|
const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
|
|
86986
|
+
if (usesInboxHelper && validateContext?.runId) {
|
|
86987
|
+
const inboxConfig = (0, verification_inbox_js_1.loadVerificationInboxConfig)();
|
|
86988
|
+
if (!inboxConfig) {
|
|
86989
|
+
logs2.push("\u{1F4EA} Test uses __vqaWaitForEmail but this runner has no verification inbox configured \u2014 the helper will fail as CONFIG_ISSUE");
|
|
86990
|
+
} else {
|
|
86991
|
+
try {
|
|
86992
|
+
inboxBroker = await (0, test_inbox_broker_js_1.startTestInboxBroker)({
|
|
86993
|
+
config: inboxConfig,
|
|
86994
|
+
baseUrl,
|
|
86995
|
+
projectId: options.projectId ?? null,
|
|
86996
|
+
runId: validateContext.runId,
|
|
86997
|
+
log: (line) => logs2.push(line)
|
|
86998
|
+
});
|
|
86999
|
+
standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)([...needleValues, inboxBroker.identity.password]);
|
|
87000
|
+
} catch (error3) {
|
|
87001
|
+
logs2.push(`\u26A0 Inbox broker failed to start: ${error3 instanceof Error ? error3.message : String(error3)}`);
|
|
87002
|
+
}
|
|
87003
|
+
}
|
|
87004
|
+
} else if (usesInboxHelper) {
|
|
87005
|
+
logs2.push("\u{1F4EA} Test uses __vqaWaitForEmail but no run callback context is available \u2014 the helper will fail as CONFIG_ISSUE");
|
|
87006
|
+
}
|
|
86173
87007
|
if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
|
|
86174
87008
|
try {
|
|
86175
87009
|
callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
|
|
@@ -86178,7 +87012,8 @@ module.exports = {
|
|
|
86178
87012
|
upstreamAuthHeader: validateContext.authHeader,
|
|
86179
87013
|
taskArn: process.env.VALIDATE_TASK_ARN,
|
|
86180
87014
|
claimEpoch: callbackClaimEpoch,
|
|
86181
|
-
authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
|
|
87015
|
+
authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH,
|
|
87016
|
+
...inboxBroker ? { localRoutes: { [test_inbox_broker_js_1.INBOX_WAIT_ROUTE]: inboxBroker.handleWait } } : {}
|
|
86182
87017
|
});
|
|
86183
87018
|
} catch (error3) {
|
|
86184
87019
|
logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
|
|
@@ -86205,14 +87040,19 @@ module.exports = {
|
|
|
86205
87040
|
// Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
|
|
86206
87041
|
// both parent and generated __vqaLogin code; "false" must still cross this process
|
|
86207
87042
|
// boundary to disable the early publish POST. It remains a per-run toggle.
|
|
86208
|
-
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
87043
|
+
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {},
|
|
87044
|
+
// Register-and-verify tests: the run's minted registration identity. An
|
|
87045
|
+
// address + a DERIVED throwaway-app password — never the IMAP secret,
|
|
87046
|
+
// which stays in this parent (test-inbox-broker.ts). Useless without the
|
|
87047
|
+
// loopback proxy the helper reads mail through, hence the guard.
|
|
87048
|
+
...inboxBroker && callbackProxy ? inboxBroker.childEnv() : {}
|
|
86209
87049
|
};
|
|
86210
87050
|
delete childTestVariables.API_AUTH_HEADERS;
|
|
86211
87051
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
86212
87052
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
86213
87053
|
let playResult;
|
|
86214
87054
|
try {
|
|
86215
|
-
playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
|
|
87055
|
+
playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv, usesInboxHelper ? 3e5 : void 0);
|
|
86216
87056
|
} finally {
|
|
86217
87057
|
await stopLiveBrowserPolling?.();
|
|
86218
87058
|
}
|
|
@@ -86303,6 +87143,8 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
86303
87143
|
} finally {
|
|
86304
87144
|
await callbackProxy?.close().catch(() => {
|
|
86305
87145
|
});
|
|
87146
|
+
await inboxBroker?.close().catch(() => {
|
|
87147
|
+
});
|
|
86306
87148
|
if (runDir) {
|
|
86307
87149
|
await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
|
|
86308
87150
|
});
|
|
@@ -86354,7 +87196,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
86354
87196
|
stopped = true;
|
|
86355
87197
|
};
|
|
86356
87198
|
}
|
|
86357
|
-
function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
|
|
87199
|
+
function runPlaywright(configPath, testVariables, trustedRuntimeEnv, killAfterMs) {
|
|
86358
87200
|
return new Promise((resolvePromise) => {
|
|
86359
87201
|
const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
|
|
86360
87202
|
const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
|
|
@@ -86372,11 +87214,13 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
86372
87214
|
proc.stderr?.on("data", (chunk) => {
|
|
86373
87215
|
stderr += chunk.toString();
|
|
86374
87216
|
});
|
|
86375
|
-
const
|
|
87217
|
+
const killMs = killAfterMs ?? 12e4;
|
|
87218
|
+
const cancelKill = (0, spawn_with_tree_kill_js_1.scheduleTreeKill)(proc, killMs, {
|
|
86376
87219
|
graceMs: 1e4,
|
|
86377
87220
|
onTimeout: () => {
|
|
86378
87221
|
timedOut = true;
|
|
86379
|
-
stderr +=
|
|
87222
|
+
stderr += `
|
|
87223
|
+
[playwright killed after ${Math.round(killMs / 1e3)}s safety timeout]`;
|
|
86380
87224
|
}
|
|
86381
87225
|
});
|
|
86382
87226
|
const settle = (result) => {
|
|
@@ -90075,402 +90919,6 @@ var require_anchor_harvester = __commonJS({
|
|
|
90075
90919
|
}
|
|
90076
90920
|
});
|
|
90077
90921
|
|
|
90078
|
-
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
90079
|
-
var require_discovery_lint_flags = __commonJS({
|
|
90080
|
-
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
90081
|
-
"use strict";
|
|
90082
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
90083
|
-
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
90084
|
-
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
90085
|
-
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
90086
|
-
function strictOn(value) {
|
|
90087
|
-
return value === "true" || value === "1";
|
|
90088
|
-
}
|
|
90089
|
-
function structuralLocatorLintMode() {
|
|
90090
|
-
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
90091
|
-
if (v === "warn")
|
|
90092
|
-
return "warn";
|
|
90093
|
-
if (v === "true" || v === "1" || v === "enforce")
|
|
90094
|
-
return "enforce";
|
|
90095
|
-
return "off";
|
|
90096
|
-
}
|
|
90097
|
-
function isPwMethodPreflightEnabled() {
|
|
90098
|
-
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
90099
|
-
}
|
|
90100
|
-
function isVqaLoginDedupeEnabled() {
|
|
90101
|
-
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
90102
|
-
}
|
|
90103
|
-
}
|
|
90104
|
-
});
|
|
90105
|
-
|
|
90106
|
-
// ../runner-core/dist/services/auth-recipe.js
|
|
90107
|
-
var require_auth_recipe = __commonJS({
|
|
90108
|
-
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
90109
|
-
"use strict";
|
|
90110
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
90111
|
-
exports.RESERVED_RUNNER_ENV_VARS = exports.AUTH_MARKER_VERSION = void 0;
|
|
90112
|
-
exports.computeRecipeHash = computeRecipeHash;
|
|
90113
|
-
exports.buildAuthRecipe = buildAuthRecipe;
|
|
90114
|
-
exports.authProofLocator = authProofLocator;
|
|
90115
|
-
exports.urlProofPredicateExpr = urlProofPredicateExpr;
|
|
90116
|
-
exports.renderAuthProofAssertion = renderAuthProofAssertion;
|
|
90117
|
-
exports.renderAuthFn = renderAuthFn;
|
|
90118
|
-
exports.isReservedRunnerEnvVar = isReservedRunnerEnvVar;
|
|
90119
|
-
exports.absoluteRoute = absoluteRoute;
|
|
90120
|
-
exports.renderReplayClickArrow = renderReplayClickArrow;
|
|
90121
|
-
exports.renderReplaySettleArrow = renderReplaySettleArrow;
|
|
90122
|
-
exports.renderAuthUrlWaitLine = renderAuthUrlWaitLine;
|
|
90123
|
-
exports.commandToReplayStep = commandToReplayStep;
|
|
90124
|
-
exports.buildReplayPlan = buildReplayPlan;
|
|
90125
|
-
exports.extractAuthRegion = extractAuthRegion;
|
|
90126
|
-
exports.stripAuthRegions = stripAuthRegions;
|
|
90127
|
-
exports.ensureExpectImported = ensureExpectImported;
|
|
90128
|
-
exports.ensureLoginCall = ensureLoginCall;
|
|
90129
|
-
exports.upsertAuthBlock = upsertAuthBlock;
|
|
90130
|
-
exports.buildHealAuthStamp = buildHealAuthStamp;
|
|
90131
|
-
var crypto_1 = __require("crypto");
|
|
90132
|
-
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
90133
|
-
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
90134
|
-
exports.AUTH_MARKER_VERSION = "v3";
|
|
90135
|
-
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
90136
|
-
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
|
|
90137
|
-
var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
|
|
90138
|
-
var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
|
|
90139
|
-
function computeRecipeHash(input) {
|
|
90140
|
-
const canonical = JSON.stringify({
|
|
90141
|
-
preNavRoute: input.preNavRoute,
|
|
90142
|
-
preNavCmds: input.preNavCmds,
|
|
90143
|
-
route: input.route,
|
|
90144
|
-
cmds: input.cmds,
|
|
90145
|
-
redirectRoute: input.redirectRoute,
|
|
90146
|
-
authProof: input.authProof
|
|
90147
|
-
});
|
|
90148
|
-
return (0, crypto_1.createHash)("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
90149
|
-
}
|
|
90150
|
-
function buildAuthRecipe(login, authProof, verifiedAt) {
|
|
90151
|
-
const preNavRoute = login.preNavRoute ?? "/";
|
|
90152
|
-
const preNavCmds = login.preNavCmds ?? [];
|
|
90153
|
-
const recipeHash = computeRecipeHash({
|
|
90154
|
-
preNavRoute,
|
|
90155
|
-
preNavCmds,
|
|
90156
|
-
route: login.route,
|
|
90157
|
-
cmds: login.cmds,
|
|
90158
|
-
redirectRoute: login.redirectRoute,
|
|
90159
|
-
authProof
|
|
90160
|
-
});
|
|
90161
|
-
return {
|
|
90162
|
-
version: 1,
|
|
90163
|
-
recipeHash,
|
|
90164
|
-
preNavRoute,
|
|
90165
|
-
preNavCmds,
|
|
90166
|
-
route: login.route,
|
|
90167
|
-
cmds: login.cmds,
|
|
90168
|
-
redirectRoute: login.redirectRoute,
|
|
90169
|
-
authProof,
|
|
90170
|
-
replayPlan: login.replayPlan ?? [],
|
|
90171
|
-
verifiedAt
|
|
90172
|
-
};
|
|
90173
|
-
}
|
|
90174
|
-
function authProofLocator(proof) {
|
|
90175
|
-
if (proof.kind === "url") {
|
|
90176
|
-
throw new Error("authProofLocator: url proof has no element locator; callers must branch on proof.kind");
|
|
90177
|
-
}
|
|
90178
|
-
if (proof.kind === "testid") {
|
|
90179
|
-
return `page.getByTestId(${JSON.stringify(proof.testId)})`;
|
|
90180
|
-
}
|
|
90181
|
-
if (proof.name) {
|
|
90182
|
-
return `page.getByRole(${JSON.stringify(proof.role)}, { name: ${JSON.stringify(proof.name)} })`;
|
|
90183
|
-
}
|
|
90184
|
-
return `page.getByRole(${JSON.stringify(proof.role)}).first()`;
|
|
90185
|
-
}
|
|
90186
|
-
function stripToPathname(route2) {
|
|
90187
|
-
return route2.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/";
|
|
90188
|
-
}
|
|
90189
|
-
function urlProofPredicateExpr(route2) {
|
|
90190
|
-
const p = stripToPathname(route2);
|
|
90191
|
-
if (p === "/")
|
|
90192
|
-
return `() => false`;
|
|
90193
|
-
const r = JSON.stringify(p);
|
|
90194
|
-
return `(u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/')`;
|
|
90195
|
-
}
|
|
90196
|
-
function renderAuthProofAssertion(proof) {
|
|
90197
|
-
if (proof.kind === "url") {
|
|
90198
|
-
return `await page.waitForURL(${urlProofPredicateExpr(proof.route)}, { timeout: 15000 });`;
|
|
90199
|
-
}
|
|
90200
|
-
return `await expect(${authProofLocator(proof)}).toBeVisible({ timeout: 15000 });`;
|
|
90201
|
-
}
|
|
90202
|
-
function normalizeCmd(cmd) {
|
|
90203
|
-
let c = cmd.trim();
|
|
90204
|
-
if (c.endsWith(";"))
|
|
90205
|
-
c = c.slice(0, -1);
|
|
90206
|
-
return ` ${c};`;
|
|
90207
|
-
}
|
|
90208
|
-
function renderAuthFn(recipe, opts) {
|
|
90209
|
-
const lines = [];
|
|
90210
|
-
lines.push(START_MARKER);
|
|
90211
|
-
lines.push(`async function __vqaLogin(page) {`);
|
|
90212
|
-
if (opts?.warmGuard) {
|
|
90213
|
-
const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
|
|
90214
|
-
const loginRoute = recipe.route ?? recipe.preNavRoute;
|
|
90215
|
-
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
|
|
90216
|
-
lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
|
|
90217
|
-
if (recipe.authProof.kind === "url") {
|
|
90218
|
-
const pred = urlProofPredicateExpr(recipe.authProof.route);
|
|
90219
|
-
lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
|
|
90220
|
-
lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
|
|
90221
|
-
} else if (loginRoute) {
|
|
90222
|
-
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
|
|
90223
|
-
} else {
|
|
90224
|
-
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
|
|
90225
|
-
}
|
|
90226
|
-
lines.push(` }`);
|
|
90227
|
-
}
|
|
90228
|
-
lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
|
|
90229
|
-
for (const cmd of recipe.preNavCmds)
|
|
90230
|
-
lines.push(normalizeCmd(cmd));
|
|
90231
|
-
if (recipe.preNavCmds.length === 0 && recipe.route && recipe.route !== recipe.preNavRoute) {
|
|
90232
|
-
lines.push(` await page.goto(${JSON.stringify(recipe.route)}, { waitUntil: 'domcontentloaded' });`);
|
|
90233
|
-
}
|
|
90234
|
-
for (const cmd of recipe.cmds)
|
|
90235
|
-
lines.push(normalizeCmd(cmd));
|
|
90236
|
-
lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
|
|
90237
|
-
lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
|
|
90238
|
-
if (opts?.warmGuard) {
|
|
90239
|
-
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
|
|
90240
|
-
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) {`);
|
|
90241
|
-
lines.push(` try {`);
|
|
90242
|
-
lines.push(` const __vqaState = await page.context().storageState();`);
|
|
90243
|
-
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 });`);
|
|
90244
|
-
lines.push(` } catch {}`);
|
|
90245
|
-
lines.push(` }`);
|
|
90246
|
-
}
|
|
90247
|
-
lines.push(`}`);
|
|
90248
|
-
lines.push(END_MARKER);
|
|
90249
|
-
return lines.join("\n");
|
|
90250
|
-
}
|
|
90251
|
-
exports.RESERVED_RUNNER_ENV_VARS = /* @__PURE__ */ new Set([
|
|
90252
|
-
"AUTH_FILE",
|
|
90253
|
-
"SHARED_AUTH_INJECT",
|
|
90254
|
-
"VALIDATE_RUN_ID",
|
|
90255
|
-
"VALIDATE_CLAIM_EPOCH",
|
|
90256
|
-
"VALIDATE_AUTH_CONFIG_HASH",
|
|
90257
|
-
"VALIDATE_TASK_ARN",
|
|
90258
|
-
"VALIDATE_RUNNER_AUTH",
|
|
90259
|
-
"VALIDATE_SERVER_URL"
|
|
90260
|
-
]);
|
|
90261
|
-
function isReservedRunnerEnvVar(key) {
|
|
90262
|
-
return exports.RESERVED_RUNNER_ENV_VARS.has(key);
|
|
90263
|
-
}
|
|
90264
|
-
function absoluteRoute(route2, baseUrl) {
|
|
90265
|
-
try {
|
|
90266
|
-
return new URL(route2, baseUrl).href;
|
|
90267
|
-
} catch {
|
|
90268
|
-
return route2;
|
|
90269
|
-
}
|
|
90270
|
-
}
|
|
90271
|
-
function renderReplayClickArrow(step) {
|
|
90272
|
-
const loc = step.locator.trim();
|
|
90273
|
-
if (step.kind === "wait") {
|
|
90274
|
-
return `async (page) => {
|
|
90275
|
-
await ${loc}.waitFor({ state: 'visible', timeout: 15000 });
|
|
90276
|
-
}`;
|
|
90277
|
-
}
|
|
90278
|
-
if (step.kind === "press") {
|
|
90279
|
-
return `async (page) => {
|
|
90280
|
-
await ${loc}.press(${JSON.stringify(step.key ?? "Enter")});
|
|
90281
|
-
}`;
|
|
90282
|
-
}
|
|
90283
|
-
return `async (page) => {
|
|
90284
|
-
await ${loc}.click();
|
|
90285
|
-
}`;
|
|
90286
|
-
}
|
|
90287
|
-
function renderReplaySettleArrow(proof) {
|
|
90288
|
-
if (proof.kind === "url") {
|
|
90289
|
-
return `async (page) => {
|
|
90290
|
-
await page.waitForURL(${urlProofPredicateExpr(proof.route)}, { timeout: 15000 });
|
|
90291
|
-
}`;
|
|
90292
|
-
}
|
|
90293
|
-
return `async (page) => {
|
|
90294
|
-
await ${authProofLocator(proof)}.waitFor({ state: 'visible', timeout: 15000 });
|
|
90295
|
-
}`;
|
|
90296
|
-
}
|
|
90297
|
-
function renderAuthUrlWaitLine(redirectRoute) {
|
|
90298
|
-
const opts = `{ waitUntil: 'domcontentloaded', timeout: 8000 }`;
|
|
90299
|
-
const tail = `.catch(() => { /* URL drift \u2014 the auth-proof assertion below is the gate */ });`;
|
|
90300
|
-
const rr = redirectRoute ? stripToPathname(redirectRoute) : null;
|
|
90301
|
-
if (rr && rr !== "/") {
|
|
90302
|
-
const r = JSON.stringify(rr);
|
|
90303
|
-
return ` await page.waitForURL((u) => u.pathname === ${r} || u.pathname.startsWith(${r} + '/'), ${opts})${tail}`;
|
|
90304
|
-
}
|
|
90305
|
-
if (rr === "/") {
|
|
90306
|
-
return ` await page.waitForURL((u) => !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
|
|
90307
|
-
}
|
|
90308
|
-
return ` await page.waitForURL((u) => u.pathname !== '/' && !/\\/(login|sign-?in)/i.test(u.pathname), ${opts})${tail}`;
|
|
90309
|
-
}
|
|
90310
|
-
function locatorChainOf(cmd) {
|
|
90311
|
-
const m = cmd.match(/\bpage\.[\s\S]*?(?=\.(?:fill|click|check|press|selectOption|setChecked|waitFor)\s*\()/);
|
|
90312
|
-
return m ? m[0].trim() : null;
|
|
90313
|
-
}
|
|
90314
|
-
function parseRoleName(locator) {
|
|
90315
|
-
const role = locator.match(/getByRole\(\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
90316
|
-
const name = locator.match(/getByRole\([^)]*name:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByLabel\(\s*['"`]([^'"`]+)['"`]/)?.[1] ?? locator.match(/getByPlaceholder\(\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
90317
|
-
return { role, name };
|
|
90318
|
-
}
|
|
90319
|
-
function credentialKeyOf(cmd) {
|
|
90320
|
-
return cmd.match(/\.fill\(\s*process\.env\.([A-Za-z_$][\w$]*)!?\s*\)/)?.[1] ?? null;
|
|
90321
|
-
}
|
|
90322
|
-
function commandToReplayStep(cmd) {
|
|
90323
|
-
const chain = locatorChainOf(cmd);
|
|
90324
|
-
if (!chain)
|
|
90325
|
-
return null;
|
|
90326
|
-
const credKey = credentialKeyOf(cmd);
|
|
90327
|
-
if (cmd.includes(".fill(")) {
|
|
90328
|
-
if (!credKey)
|
|
90329
|
-
return null;
|
|
90330
|
-
const { role, name } = parseRoleName(chain);
|
|
90331
|
-
return { kind: "fill", credentialKey: credKey, locator: chain, role, name, element: name ?? role ?? "credential field" };
|
|
90332
|
-
}
|
|
90333
|
-
if (/\.waitFor\s*\(/.test(cmd))
|
|
90334
|
-
return { kind: "wait", locator: chain };
|
|
90335
|
-
const pressKey = cmd.match(/\.press\(\s*['"`]([^'"`]+)['"`]\s*\)/)?.[1];
|
|
90336
|
-
if (pressKey)
|
|
90337
|
-
return { kind: "press", locator: chain, key: pressKey };
|
|
90338
|
-
if (/\.(?:click|check|setChecked|selectOption)\s*\(/.test(cmd))
|
|
90339
|
-
return { kind: "click", locator: chain };
|
|
90340
|
-
return null;
|
|
90341
|
-
}
|
|
90342
|
-
function buildReplayPlan(login) {
|
|
90343
|
-
const plan = [];
|
|
90344
|
-
const preNavRoute = login.preNavRoute ?? "/";
|
|
90345
|
-
plan.push({ kind: "nav", route: preNavRoute });
|
|
90346
|
-
for (const c of login.preNavCmds ?? []) {
|
|
90347
|
-
const step = commandToReplayStep(c);
|
|
90348
|
-
if (step)
|
|
90349
|
-
plan.push(step);
|
|
90350
|
-
}
|
|
90351
|
-
if ((login.preNavCmds ?? []).length === 0 && login.route && login.route !== preNavRoute) {
|
|
90352
|
-
plan.push({ kind: "nav", route: login.route });
|
|
90353
|
-
}
|
|
90354
|
-
for (const c of login.cmds) {
|
|
90355
|
-
const step = commandToReplayStep(c);
|
|
90356
|
-
if (step)
|
|
90357
|
-
plan.push(step);
|
|
90358
|
-
}
|
|
90359
|
-
return plan;
|
|
90360
|
-
}
|
|
90361
|
-
function extractAuthRegion(code) {
|
|
90362
|
-
const m = code.match(new RegExp(AUTH_REGION_RE.source));
|
|
90363
|
-
return m ? m[0] : null;
|
|
90364
|
-
}
|
|
90365
|
-
function countAuthRegions(code) {
|
|
90366
|
-
const m = code.match(AUTH_REGION_RE);
|
|
90367
|
-
return m ? m.length : 0;
|
|
90368
|
-
}
|
|
90369
|
-
function stripAuthRegions(code) {
|
|
90370
|
-
return code.replace(AUTH_REGION_RE, "");
|
|
90371
|
-
}
|
|
90372
|
-
var PW_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?/;
|
|
90373
|
-
function ensureExpectImported(code) {
|
|
90374
|
-
const m = code.match(PW_IMPORT_RE);
|
|
90375
|
-
if (!m)
|
|
90376
|
-
return code;
|
|
90377
|
-
const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
90378
|
-
if (names.includes("expect"))
|
|
90379
|
-
return code;
|
|
90380
|
-
const next = [...names, "expect"];
|
|
90381
|
-
return code.replace(PW_IMPORT_RE, `import { ${next.join(", ")} } from '@playwright/test';`);
|
|
90382
|
-
}
|
|
90383
|
-
function insertAfterPwImport(code, block) {
|
|
90384
|
-
const m = code.match(PW_IMPORT_RE);
|
|
90385
|
-
if (!m || m.index === void 0) {
|
|
90386
|
-
return `${block}
|
|
90387
|
-
|
|
90388
|
-
${code.replace(/^\r?\n+/, "")}`;
|
|
90389
|
-
}
|
|
90390
|
-
const end = m.index + m[0].length;
|
|
90391
|
-
const head = code.slice(0, end);
|
|
90392
|
-
const tail = code.slice(end).replace(/^\r?\n+/, "");
|
|
90393
|
-
return `${head}
|
|
90394
|
-
|
|
90395
|
-
${block}
|
|
90396
|
-
|
|
90397
|
-
${tail}`;
|
|
90398
|
-
}
|
|
90399
|
-
var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
|
|
90400
|
-
var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
|
|
90401
|
-
function ensureLoginCall(code) {
|
|
90402
|
-
if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
|
|
90403
|
-
return code;
|
|
90404
|
-
let result = code;
|
|
90405
|
-
if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
|
|
90406
|
-
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
90407
|
-
}
|
|
90408
|
-
const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
|
|
90409
|
-
for (let i = matches.length - 1; i >= 0; i--) {
|
|
90410
|
-
const m = matches[i];
|
|
90411
|
-
if (m.index === void 0)
|
|
90412
|
-
continue;
|
|
90413
|
-
const insertAt = m.index + m[0].length;
|
|
90414
|
-
const following = result.slice(insertAt, insertAt + 60);
|
|
90415
|
-
if (/^\s*await\s+__vqaLogin\s*\(\s*page\s*\)/.test(following))
|
|
90416
|
-
continue;
|
|
90417
|
-
result = `${result.slice(0, insertAt)}
|
|
90418
|
-
await __vqaLogin(page);${result.slice(insertAt)}`;
|
|
90419
|
-
}
|
|
90420
|
-
return result;
|
|
90421
|
-
}
|
|
90422
|
-
function upsertAuthBlock(code, ctx) {
|
|
90423
|
-
if (ctx.mode === "generate") {
|
|
90424
|
-
if (!ctx.recipe || !ctx.isAuthNeeded)
|
|
90425
|
-
return code;
|
|
90426
|
-
let result = stripAuthRegions(code);
|
|
90427
|
-
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
90428
|
-
result = ensureExpectImported(result);
|
|
90429
|
-
const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
|
|
90430
|
-
result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
|
|
90431
|
-
result = ensureLoginCall(result);
|
|
90432
|
-
const n = countAuthRegions(result);
|
|
90433
|
-
if (n !== 1) {
|
|
90434
|
-
throw new Error(`upsertAuthBlock(generate): expected exactly 1 auth region after stamp, got ${n}`);
|
|
90435
|
-
}
|
|
90436
|
-
if (!LOGIN_CALL_RE.test(result)) {
|
|
90437
|
-
throw new Error("upsertAuthBlock(generate): stamped __vqaLogin but found no test() opener to inject the call");
|
|
90438
|
-
}
|
|
90439
|
-
return result;
|
|
90440
|
-
}
|
|
90441
|
-
try {
|
|
90442
|
-
if (!ctx.preservedAuthRegion)
|
|
90443
|
-
return code;
|
|
90444
|
-
let result = ensureExpectImported(code);
|
|
90445
|
-
if (countAuthRegions(result) >= 1) {
|
|
90446
|
-
result = result.replace(AUTH_REGION_RE, "");
|
|
90447
|
-
result = insertAfterPwImport(result, ctx.preservedAuthRegion);
|
|
90448
|
-
} else {
|
|
90449
|
-
result = insertAfterPwImport(result, ctx.preservedAuthRegion);
|
|
90450
|
-
}
|
|
90451
|
-
result = ensureLoginCall(result);
|
|
90452
|
-
if (countAuthRegions(result) !== 1) {
|
|
90453
|
-
throw new Error("heal re-stamp produced non-unique auth region");
|
|
90454
|
-
}
|
|
90455
|
-
return result;
|
|
90456
|
-
} catch (err) {
|
|
90457
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
90458
|
-
ctx.onSignal?.({ type: "auth-stamp-degraded", reason });
|
|
90459
|
-
return code;
|
|
90460
|
-
}
|
|
90461
|
-
}
|
|
90462
|
-
function buildHealAuthStamp(originalCode, failureError, logs2) {
|
|
90463
|
-
const region = extractAuthRegion(originalCode);
|
|
90464
|
-
const loginFailed = !!failureError && /__vqaLogin/.test(failureError);
|
|
90465
|
-
return {
|
|
90466
|
-
mode: "heal",
|
|
90467
|
-
preservedAuthRegion: region && !loginFailed ? region : null,
|
|
90468
|
-
onSignal: logs2 ? (s) => logs2.push(`\u26A0 auth-stamp ${s.type}: ${s.reason}`) : void 0
|
|
90469
|
-
};
|
|
90470
|
-
}
|
|
90471
|
-
}
|
|
90472
|
-
});
|
|
90473
|
-
|
|
90474
90922
|
// ../runner-core/dist/services/auth-replay.js
|
|
90475
90923
|
var require_auth_replay = __commonJS({
|
|
90476
90924
|
"../runner-core/dist/services/auth-replay.js"(exports) {
|
|
@@ -94347,9 +94795,41 @@ var require_discover_generator = __commonJS({
|
|
|
94347
94795
|
var reveal_diff_js_1 = require_reveal_diff();
|
|
94348
94796
|
var anchor_harvester_js_1 = require_anchor_harvester();
|
|
94349
94797
|
var auth_recipe_js_1 = require_auth_recipe();
|
|
94798
|
+
var inbox_stamp_js_1 = require_inbox_stamp();
|
|
94350
94799
|
var auth_replay_js_1 = require_auth_replay();
|
|
94351
94800
|
var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
|
|
94352
94801
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
94802
|
+
var inbox_tools_js_1 = require_inbox_tools();
|
|
94803
|
+
var verification_inbox_js_1 = require_verification_inbox();
|
|
94804
|
+
var test_inbox_broker_js_1 = require_test_inbox_broker();
|
|
94805
|
+
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;
|
|
94806
|
+
function scenarioLooksLikeRegistration(scenario) {
|
|
94807
|
+
const hay = `${scenario.name ?? ""} ${scenario.description ?? ""} ${scenario.entryRoute ?? ""} ${(scenario.targetPages ?? []).join(" ")}`;
|
|
94808
|
+
return REGISTER_SCENARIO_RE.test(hay);
|
|
94809
|
+
}
|
|
94810
|
+
function buildEmailVerifyGenBlock(genEmail, genPassword) {
|
|
94811
|
+
return `
|
|
94812
|
+
|
|
94813
|
+
## EMAIL VERIFICATION TEST \u2014 register a fresh account and verify it (LIVE, then EMIT)
|
|
94814
|
+
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.
|
|
94815
|
+
|
|
94816
|
+
LIVE (to prove the flow while generating):
|
|
94817
|
+
- Register on the app's signup form using this throwaway account:
|
|
94818
|
+
- email: \`${genEmail}\`
|
|
94819
|
+
- password: \`${genPassword}\`
|
|
94820
|
+
Type them with browser_type and submit the form ONCE.
|
|
94821
|
+
- 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\`.
|
|
94822
|
+
- Reach the verified / signed-in state and confirm it with a browser_verify_* call, so your final assertion is proven.
|
|
94823
|
+
|
|
94824
|
+
EMIT (the code you put in finish_generate) \u2014 do NOT hardcode the throwaway values above:
|
|
94825
|
+
- 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.
|
|
94826
|
+
- Read the verification email at runtime with the injected helper (it is provided for you \u2014 do NOT define it, do NOT import it):
|
|
94827
|
+
- code flow: \`const code = await __vqaWaitForEmail('code');\` then type \`code\` into the verification field.
|
|
94828
|
+
- link flow: \`const link = await __vqaWaitForEmail('link');\` then \`await page.goto(link);\`.
|
|
94829
|
+
- if unsure: \`const email = await __vqaWaitForEmail();\` returns \`{ code, link, subject, from }\`.
|
|
94830
|
+
- Tag the test \`@email-verify\`. Emit EXACTLY ONE test() in this file (one registration per run).
|
|
94831
|
+
- 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.`;
|
|
94832
|
+
}
|
|
94353
94833
|
exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
|
|
94354
94834
|
"browser_install",
|
|
94355
94835
|
"browser_close",
|
|
@@ -95536,6 +96016,11 @@ ${testOpen}`;
|
|
|
95536
96016
|
if (options?.authStamp) {
|
|
95537
96017
|
result = (0, auth_recipe_js_1.upsertAuthBlock)(result, options.authStamp);
|
|
95538
96018
|
}
|
|
96019
|
+
try {
|
|
96020
|
+
result = (0, inbox_stamp_js_1.upsertInboxBlock)(result);
|
|
96021
|
+
} catch {
|
|
96022
|
+
result = (0, inbox_stamp_js_1.stripInboxRegions)(result);
|
|
96023
|
+
}
|
|
95539
96024
|
return result;
|
|
95540
96025
|
}
|
|
95541
96026
|
function ensureDiscoveredTag(tags) {
|
|
@@ -95622,6 +96107,16 @@ ${testOpen}`;
|
|
|
95622
96107
|
logs2?.push(` \u2717 Rejected "${t.name}": JS parse error \u2014 ${msg}`);
|
|
95623
96108
|
return false;
|
|
95624
96109
|
}
|
|
96110
|
+
{
|
|
96111
|
+
const modelBody = (0, inbox_stamp_js_1.stripInboxRegions)(code);
|
|
96112
|
+
if (inbox_stamp_js_1.INBOX_HELPER_TOKEN_RE.test(modelBody)) {
|
|
96113
|
+
const openerCount = (stripStringsAndComments(modelBody).match(/\btest(?:\.(?:only|skip|fixme))?\s*\(/g) ?? []).length;
|
|
96114
|
+
if (openerCount > 1) {
|
|
96115
|
+
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`);
|
|
96116
|
+
return false;
|
|
96117
|
+
}
|
|
96118
|
+
}
|
|
96119
|
+
}
|
|
95625
96120
|
const codeForArgOrderScan = stripStringsAndComments(code);
|
|
95626
96121
|
if (/test\s*\(\s*,\s*(?:async\s*)?(?:\([^)]*\)|function\s*\([^)]*\))\s*(?:=>)?\s*\{[\s\S]*?\}\s*,\s*\{\s*(?:tags?|annotations)\s*:/.test(codeForArgOrderScan)) {
|
|
95627
96122
|
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)`);
|
|
@@ -96442,6 +96937,23 @@ ${testOpen}`;
|
|
|
96442
96937
|
return { offendingLines: offending };
|
|
96443
96938
|
}
|
|
96444
96939
|
var ACTION_METHODS = ["click", "dblclick", "tap", "fill", "type", "pressSequentially", "press", "selectOption", "hover", "check", "uncheck", "setInputFiles", "dragTo", "focus", "goto", "waitForURL"];
|
|
96940
|
+
var DYNAMIC_VERIFICATION_ARG = "<verification-dynamic>";
|
|
96941
|
+
function collectVerificationHelperVars(code) {
|
|
96942
|
+
const vars = /* @__PURE__ */ new Set();
|
|
96943
|
+
const assignRe = /(?:(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*await\s+__vqaWaitForEmail\s*\(/g;
|
|
96944
|
+
let m;
|
|
96945
|
+
while ((m = assignRe.exec(code)) !== null)
|
|
96946
|
+
vars.add(m[1]);
|
|
96947
|
+
const destructRe = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*await\s+__vqaWaitForEmail\s*\(/g;
|
|
96948
|
+
while ((m = destructRe.exec(code)) !== null) {
|
|
96949
|
+
for (const part of m[1].split(",")) {
|
|
96950
|
+
const name = part.split(":").pop()?.trim().replace(/\s.*$/, "");
|
|
96951
|
+
if (name && /^[A-Za-z_$][\w$]*$/.test(name))
|
|
96952
|
+
vars.add(name);
|
|
96953
|
+
}
|
|
96954
|
+
}
|
|
96955
|
+
return vars;
|
|
96956
|
+
}
|
|
96445
96957
|
function extractStringArg(text) {
|
|
96446
96958
|
const m = text.match(/^\s*(['"`])((?:\\.|(?!\1).)*)\1/);
|
|
96447
96959
|
if (!m)
|
|
@@ -96711,6 +97223,8 @@ ${testOpen}`;
|
|
|
96711
97223
|
if (!locatorsMatch(ledger.locator, submitted.locator))
|
|
96712
97224
|
return false;
|
|
96713
97225
|
if (ledger.method === "fill" || ledger.method === "type") {
|
|
97226
|
+
if (submitted.arg === DYNAMIC_VERIFICATION_ARG)
|
|
97227
|
+
return true;
|
|
96714
97228
|
if (ledger.arg === "<env>" || ledger.arg === "<test-value>") {
|
|
96715
97229
|
return submitted.arg === "<env>" || submitted.arg === "<test-value>";
|
|
96716
97230
|
}
|
|
@@ -96805,6 +97319,25 @@ ${testOpen}`;
|
|
|
96805
97319
|
}
|
|
96806
97320
|
}
|
|
96807
97321
|
const submittedSigs = extractActionSignatures(playwrightCode, credentials);
|
|
97322
|
+
const helperVars = collectVerificationHelperVars(playwrightCode);
|
|
97323
|
+
if (helperVars.size > 0) {
|
|
97324
|
+
const usesHelperVar = (text) => {
|
|
97325
|
+
for (const v of helperVars) {
|
|
97326
|
+
if (new RegExp(`\\b${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text))
|
|
97327
|
+
return true;
|
|
97328
|
+
}
|
|
97329
|
+
return false;
|
|
97330
|
+
};
|
|
97331
|
+
for (const sub of submittedSigs) {
|
|
97332
|
+
if ((sub.method === "fill" || sub.method === "type") && sub.arg && usesHelperVar(sub.arg)) {
|
|
97333
|
+
sub.arg = DYNAMIC_VERIFICATION_ARG;
|
|
97334
|
+
} else if (sub.method === "goto") {
|
|
97335
|
+
const gotoArg = sub.raw.match(/\.goto\s*\(\s*([^),]+)/);
|
|
97336
|
+
if (gotoArg && usesHelperVar(gotoArg[1]))
|
|
97337
|
+
sub.arg = DYNAMIC_VERIFICATION_ARG;
|
|
97338
|
+
}
|
|
97339
|
+
}
|
|
97340
|
+
}
|
|
96808
97341
|
const offending = [];
|
|
96809
97342
|
let firstGotoConsumed = false;
|
|
96810
97343
|
let cursor = 0;
|
|
@@ -96812,6 +97345,8 @@ ${testOpen}`;
|
|
|
96812
97345
|
for (const sub of submittedSigs) {
|
|
96813
97346
|
if (sub.method === "waitForURL")
|
|
96814
97347
|
continue;
|
|
97348
|
+
if (sub.method === "goto" && sub.arg === DYNAMIC_VERIFICATION_ARG)
|
|
97349
|
+
continue;
|
|
96815
97350
|
if (sub.method === "goto" && !firstGotoConsumed) {
|
|
96816
97351
|
firstGotoConsumed = true;
|
|
96817
97352
|
if (expectedEntry !== null && sub.arg !== expectedEntry) {
|
|
@@ -97137,7 +97672,7 @@ expect(secondTitle).toBeTruthy();
|
|
|
97137
97672
|
${tailBlock}`;
|
|
97138
97673
|
}
|
|
97139
97674
|
function evaluateAmbiguityGate(finalCode, alreadyAttempted, hasRevisionBudget) {
|
|
97140
|
-
const findings = (0, agentic_discovery_prompts_js_1.scanAmbiguousLocators)((0, auth_recipe_js_1.stripAuthRegions)(finalCode));
|
|
97675
|
+
const findings = (0, agentic_discovery_prompts_js_1.scanAmbiguousLocators)((0, auth_recipe_js_1.stripAuthRegions)((0, inbox_stamp_js_1.stripInboxRegions)(finalCode)));
|
|
97141
97676
|
return { fire: findings.length > 0 && !alreadyAttempted && hasRevisionBudget, findings };
|
|
97142
97677
|
}
|
|
97143
97678
|
async function generateTestFromScenario(mcpClient, scenario, siteMap, baseUrl, credentials, aiClient, agentModel, logs2, onAICall, opts) {
|
|
@@ -97161,6 +97696,7 @@ ${tailBlock}`;
|
|
|
97161
97696
|
}
|
|
97162
97697
|
}
|
|
97163
97698
|
}
|
|
97699
|
+
let genInboxReader = null;
|
|
97164
97700
|
try {
|
|
97165
97701
|
const startTime = Date.now();
|
|
97166
97702
|
const groupId = `scenario-gen-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
@@ -97211,12 +97747,28 @@ ${tailBlock}`;
|
|
|
97211
97747
|
});
|
|
97212
97748
|
const baseSkeletonHadRegression = tags.includes("@regression");
|
|
97213
97749
|
const hasVerifiedRecipe = !!opts?.recipe && tags.includes("@needs-auth");
|
|
97214
|
-
const
|
|
97750
|
+
const projectHasRegisterIntent = credentials?.DISCOVERY_SHOULD_REGISTER === "true";
|
|
97751
|
+
const inboxGenConfig = (0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() && projectHasRegisterIntent && !hasVerifiedRecipe && scenarioLooksLikeRegistration(scenario) ? (0, verification_inbox_js_1.loadVerificationInboxConfig)() : null;
|
|
97752
|
+
let genInboxState = null;
|
|
97753
|
+
let genInboxCredentials = credentials;
|
|
97754
|
+
let genInboxSecret = null;
|
|
97755
|
+
if (inboxGenConfig) {
|
|
97756
|
+
const genTag = `vqag${(0, crypto_1.createHash)("sha256").update(`testgen|${(0, verification_inbox_js_1.seedToHostKey)(baseUrl)}|${groupId}`).digest("hex").slice(0, 10)}`;
|
|
97757
|
+
const genEmail = (0, verification_inbox_js_1.applyPlusTag)(inboxGenConfig.address, genTag);
|
|
97758
|
+
const genPassword = (0, verification_inbox_js_1.deriveRegistrationPassword)(genEmail);
|
|
97759
|
+
genInboxReader = new verification_inbox_js_1.VerificationInboxReader(inboxGenConfig);
|
|
97760
|
+
genInboxState = (0, inbox_tools_js_1.createInboxRunState)(inboxGenConfig, { tag: genTag, passwordKey: test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV });
|
|
97761
|
+
genInboxCredentials = { ...credentials, [test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV]: genEmail, [test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV]: genPassword };
|
|
97762
|
+
genInboxSecret = genPassword;
|
|
97763
|
+
openaiTools.push(...inbox_tools_js_1.INBOX_TOOLS);
|
|
97764
|
+
logs2.push(` \u{1F4EC} Email-verification generation ON for "${scenario.name}" \u2014 registering throwaway ${genEmail}`);
|
|
97765
|
+
}
|
|
97766
|
+
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]) : "");
|
|
97215
97767
|
const messages = [
|
|
97216
97768
|
{ role: "system", content: systemPrompt },
|
|
97217
97769
|
{
|
|
97218
97770
|
role: "user",
|
|
97219
|
-
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.`
|
|
97771
|
+
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.`
|
|
97220
97772
|
}
|
|
97221
97773
|
];
|
|
97222
97774
|
let iteration = 0;
|
|
@@ -97344,6 +97896,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97344
97896
|
const iterStart = Date.now();
|
|
97345
97897
|
const requestMessageCount = messages.length;
|
|
97346
97898
|
const requestMessages = messages.slice();
|
|
97899
|
+
const scrubAudit = (payload) => {
|
|
97900
|
+
if (!genInboxSecret)
|
|
97901
|
+
return payload;
|
|
97902
|
+
try {
|
|
97903
|
+
return JSON.parse(JSON.stringify(payload).split(genInboxSecret).join("[REDACTED:VQA_TEST_ACCOUNT_PASSWORD]"));
|
|
97904
|
+
} catch {
|
|
97905
|
+
return payload;
|
|
97906
|
+
}
|
|
97907
|
+
};
|
|
97347
97908
|
let response;
|
|
97348
97909
|
try {
|
|
97349
97910
|
response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
@@ -97385,7 +97946,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97385
97946
|
messages.push(assistantMessage);
|
|
97386
97947
|
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
|
97387
97948
|
try {
|
|
97388
|
-
onAICall?.({
|
|
97949
|
+
onAICall?.(scrubAudit({
|
|
97389
97950
|
iteration,
|
|
97390
97951
|
groupId,
|
|
97391
97952
|
callType: "scenarioGenerate",
|
|
@@ -97399,7 +97960,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97399
97960
|
requestMessageCount,
|
|
97400
97961
|
requestMessages,
|
|
97401
97962
|
ledger: actionLedger.slice()
|
|
97402
|
-
});
|
|
97963
|
+
}));
|
|
97403
97964
|
} catch {
|
|
97404
97965
|
}
|
|
97405
97966
|
logs2.push(` No tool calls \u2014 agent stopped`);
|
|
@@ -97426,7 +97987,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97426
97987
|
const suspectedBugDeclared = toolArgs.suspectedBug === true;
|
|
97427
97988
|
const bugDescription = String(toolArgs.bugDescription || "").slice(0, 500).replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/\s+/g, " ").trim();
|
|
97428
97989
|
const testObj = { name, description, playwrightCode: code, tags };
|
|
97429
|
-
const validated = validateTests([testObj], logs2, { sensitiveEnvMap:
|
|
97990
|
+
const validated = validateTests([testObj], logs2, { sensitiveEnvMap: genInboxCredentials, authRecipe: opts?.recipe ?? null, basePath: gotoBasePathFromBaseUrl(baseUrl) });
|
|
97430
97991
|
if (validated.length > 0) {
|
|
97431
97992
|
if (actionLedger.length > lastRenderedLedgerEntryCount && actionLedger.length > 0) {
|
|
97432
97993
|
const renderedLedger = renderSessionRecording(actionLedger, credentials);
|
|
@@ -97478,7 +98039,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
|
|
|
97478
98039
|
});
|
|
97479
98040
|
continue;
|
|
97480
98041
|
}
|
|
97481
|
-
const actionCheck = validateFinalActionFidelity(code, actionLedger,
|
|
98042
|
+
const actionCheck = validateFinalActionFidelity(code, actionLedger, genInboxCredentials, { entryRoute: scenario.entryRoute });
|
|
97482
98043
|
if (actionCheck.offendingActions.length > 0 && !actionRevisionAttempted) {
|
|
97483
98044
|
actionRevisionAttempted = true;
|
|
97484
98045
|
const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
|
|
@@ -97791,6 +98352,15 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
|
|
|
97791
98352
|
});
|
|
97792
98353
|
continue;
|
|
97793
98354
|
}
|
|
98355
|
+
if (genInboxReader && genInboxState && inbox_tools_js_1.INBOX_TOOL_NAMES.has(toolName)) {
|
|
98356
|
+
const inboxResult = await (0, inbox_tools_js_1.dispatchInboxTool)(toolName, toolArgs, {
|
|
98357
|
+
reader: genInboxReader,
|
|
98358
|
+
state: genInboxState,
|
|
98359
|
+
mcpClient
|
|
98360
|
+
});
|
|
98361
|
+
toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: inboxResult.content });
|
|
98362
|
+
continue;
|
|
98363
|
+
}
|
|
97794
98364
|
let toolResultContent = "";
|
|
97795
98365
|
try {
|
|
97796
98366
|
const mcpResult = await mcpClient.callTool({ name: toolName, arguments: toolArgs });
|
|
@@ -97953,7 +98523,7 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
|
|
|
97953
98523
|
tool: assistantMessage.tool_calls?.find((tc) => tc.id === tr.tool_call_id)?.function.name ?? "unknown",
|
|
97954
98524
|
result: typeof tr.content === "string" ? tr.content.slice(0, 5e4) : String(tr.content).slice(0, 5e4)
|
|
97955
98525
|
}));
|
|
97956
|
-
onAICall?.({
|
|
98526
|
+
onAICall?.(scrubAudit({
|
|
97957
98527
|
iteration,
|
|
97958
98528
|
groupId,
|
|
97959
98529
|
callType: "scenarioGenerate",
|
|
@@ -97969,7 +98539,7 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
|
|
|
97969
98539
|
requestMessageCount,
|
|
97970
98540
|
requestMessages,
|
|
97971
98541
|
ledger: actionLedger.slice()
|
|
97972
|
-
});
|
|
98542
|
+
}));
|
|
97973
98543
|
} catch {
|
|
97974
98544
|
}
|
|
97975
98545
|
messages.push(...toolResults);
|
|
@@ -97977,8 +98547,12 @@ WARNING: index-based locator (.nth/.first) \u2014 order-dependent and fragile on
|
|
|
97977
98547
|
if (!result) {
|
|
97978
98548
|
logs2.push(` Scenario generation did not produce a test after ${iteration} iterations`);
|
|
97979
98549
|
}
|
|
98550
|
+
await genInboxReader?.close().catch(() => {
|
|
98551
|
+
});
|
|
97980
98552
|
return result;
|
|
97981
98553
|
} catch (err) {
|
|
98554
|
+
await genInboxReader?.close().catch(() => {
|
|
98555
|
+
});
|
|
97982
98556
|
const msg = err instanceof Error ? err.message : String(err);
|
|
97983
98557
|
logs2.push(` Scenario generation failed: ${msg}`);
|
|
97984
98558
|
if ((0, ai_retry_js_1.isAIProviderError)(err)) {
|
|
@@ -324913,7 +325487,7 @@ ${currentSummary}`;
|
|
|
324913
325487
|
const distinctPathsVisited = new Set([...state2.visitedRoutes].map((r) => (0, exploration_parser_js_1.normalizeRouteForFocusScope)(r))).size;
|
|
324914
325488
|
const fb = (0, survey_link_frontier_js_1.evaluateFinishBounce)(finishBounce, distinctPathsVisited);
|
|
324915
325489
|
finishBounce = fb.state;
|
|
324916
|
-
const forceAcceptFinish = fb.escape;
|
|
325490
|
+
const forceAcceptFinish = fb.escape && !finishStreakCountedThisBatch;
|
|
324917
325491
|
if (forceAcceptFinish) {
|
|
324918
325492
|
const stuck = adaptiveActive ? adaptiveDebtRoutes() : (0, survey_link_frontier_js_1.getFinishBlockingFrontier)(state2).map((i) => i.route);
|
|
324919
325493
|
const reasonText = String(toolArgs.summary || "").replace(/\s+/g, " ").trim().slice(0, 400);
|
|
@@ -325035,7 +325609,7 @@ Exploration complete: ${summary}`);
|
|
|
325035
325609
|
continue;
|
|
325036
325610
|
}
|
|
325037
325611
|
if (toolName === "fill_credential" || toolName === "fill_credential_form") {
|
|
325038
|
-
if (emailVerificationUnavailable) {
|
|
325612
|
+
if (emailVerificationUnavailable || inboxState?.authDead) {
|
|
325039
325613
|
const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
|
|
325040
325614
|
if (codeGuard.blocked) {
|
|
325041
325615
|
logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
|
|
@@ -325129,7 +325703,7 @@ Exploration complete: ${summary}`);
|
|
|
325129
325703
|
continue;
|
|
325130
325704
|
}
|
|
325131
325705
|
}
|
|
325132
|
-
if (emailVerificationUnavailable) {
|
|
325706
|
+
if (emailVerificationUnavailable || inboxState?.authDead) {
|
|
325133
325707
|
const codeGuard = (0, inbox_tools_js_1.guardVerificationCodeFabrication)(toolName, toolArgs);
|
|
325134
325708
|
if (codeGuard.blocked) {
|
|
325135
325709
|
logs2.push(` BLOCKED ${toolName}: verification-code field in email-verification-unavailable mode (field="${codeGuard.matchedField}")`);
|
|
@@ -326544,7 +327118,10 @@ var require_auth_capture_run = __commonJS({
|
|
|
326544
327118
|
exports.getAuthCaptureTimeoutMs = getAuthCaptureTimeoutMs;
|
|
326545
327119
|
exports.getAuthCaptureRunRetryMaxAttempts = getAuthCaptureRunRetryMaxAttempts;
|
|
326546
327120
|
exports.classifyCaptureMiss = classifyCaptureMiss;
|
|
327121
|
+
exports.harvestCollapsedAuthProof = harvestCollapsedAuthProof;
|
|
327122
|
+
exports.selectAuthLandingRoute = selectAuthLandingRoute;
|
|
326547
327123
|
exports.harvestAuthProofLive = harvestAuthProofLive;
|
|
327124
|
+
exports.detectRedirectBackCollapse = detectRedirectBackCollapse;
|
|
326548
327125
|
exports.findCredentialLiteralLeaks = findCredentialLiteralLeaks;
|
|
326549
327126
|
exports.runLoginCaptureRunWithOutcome = runLoginCaptureRunWithOutcome;
|
|
326550
327127
|
exports.runLoginCaptureRun = runLoginCaptureRun;
|
|
@@ -326665,35 +327242,26 @@ ${keyLines || "- (none provided)"}
|
|
|
326665
327242
|
return false;
|
|
326666
327243
|
return true;
|
|
326667
327244
|
}
|
|
326668
|
-
|
|
326669
|
-
|
|
326670
|
-
|
|
326671
|
-
|
|
326672
|
-
|
|
326673
|
-
|
|
326674
|
-
|
|
326675
|
-
|
|
326676
|
-
|
|
326677
|
-
|
|
326678
|
-
|
|
326679
|
-
|
|
326680
|
-
|
|
326681
|
-
|
|
326682
|
-
|
|
326683
|
-
|
|
326684
|
-
continue;
|
|
326685
|
-
for (const el of p.elements ?? []) {
|
|
326686
|
-
const nm = (el.label ?? "").trim().toLowerCase();
|
|
326687
|
-
if (el.role && nm)
|
|
326688
|
-
loginRoleNames.add(`${el.role}|${nm}`);
|
|
326689
|
-
if (el.testId)
|
|
326690
|
-
loginTestIds.add(el.testId);
|
|
326691
|
-
}
|
|
326692
|
-
}
|
|
327245
|
+
var LOGOUT_RE = /\b(log\s?out|sign\s?out)\b/i;
|
|
327246
|
+
var ACCOUNT_RE = /\b(account|profile|settings|avatar|notifications?|dashboard|inbox|my\s+account|user\s+menu)\b/i;
|
|
327247
|
+
var GREETING_RE = /\b(welcome|hello|hey|good\s+(morning|afternoon|evening|day))\b/i;
|
|
327248
|
+
function describeAuthProof(proof) {
|
|
327249
|
+
switch (proof.kind) {
|
|
327250
|
+
case "role":
|
|
327251
|
+
return `role:${proof.role}${proof.name ? `(${proof.name})` : ""}`;
|
|
327252
|
+
case "testid":
|
|
327253
|
+
return `testid:${proof.testId}`;
|
|
327254
|
+
case "endpoint":
|
|
327255
|
+
return `endpoint:${proof.url}`;
|
|
327256
|
+
case "url":
|
|
327257
|
+
return `url:${proof.route}`;
|
|
327258
|
+
}
|
|
327259
|
+
}
|
|
327260
|
+
function rankDurableAuthProof(landingElements, loginRoleNames, loginTestIds, credentials, logs2) {
|
|
326693
327261
|
const haveLoginDiff = loginRoleNames.size > 0 || loginTestIds.size > 0;
|
|
326694
327262
|
const roleNameFreq = /* @__PURE__ */ new Map();
|
|
326695
327263
|
const testIdFreq = /* @__PURE__ */ new Map();
|
|
326696
|
-
for (const el of
|
|
327264
|
+
for (const el of landingElements) {
|
|
326697
327265
|
const nm = (el.label ?? "").trim().toLowerCase();
|
|
326698
327266
|
if (el.role && nm)
|
|
326699
327267
|
roleNameFreq.set(`${el.role}|${nm}`, (roleNameFreq.get(`${el.role}|${nm}`) ?? 0) + 1);
|
|
@@ -326733,7 +327301,7 @@ ${keyLines || "- (none provided)"}
|
|
|
326733
327301
|
return true;
|
|
326734
327302
|
};
|
|
326735
327303
|
const ranked = [];
|
|
326736
|
-
for (const el of
|
|
327304
|
+
for (const el of landingElements) {
|
|
326737
327305
|
const label = (el.label ?? "").trim();
|
|
326738
327306
|
const role = el.role ?? "";
|
|
326739
327307
|
const testId = el.testId ?? "";
|
|
@@ -326745,7 +327313,7 @@ ${keyLines || "- (none provided)"}
|
|
|
326745
327313
|
const r = role === "link" ? "link" : "button";
|
|
326746
327314
|
ranked.push({ tier: 1, proof: { kind: "role", role: r, name: label }, desc: `${r} "${label}" (logout control)` });
|
|
326747
327315
|
}
|
|
326748
|
-
if (label && ACCOUNT_RE.test(label) && !personalized && uniqueRoleName && (role === "button" || role === "link") && absentOnLogin(roleNameKey, "")) {
|
|
327316
|
+
if (label && ACCOUNT_RE.test(label) && !personalized && uniqueRoleName && haveLoginDiff && (role === "button" || role === "link") && absentOnLogin(roleNameKey, "")) {
|
|
326749
327317
|
ranked.push({ tier: 2, proof: { kind: "role", role, name: label }, desc: `${role} "${label}" (account control)` });
|
|
326750
327318
|
}
|
|
326751
327319
|
if (testId && isSemanticTestId(testId) && uniqueTestId && haveLoginDiff && absentOnLogin("", testId)) {
|
|
@@ -326759,25 +327327,110 @@ ${keyLines || "- (none provided)"}
|
|
|
326759
327327
|
}
|
|
326760
327328
|
}
|
|
326761
327329
|
if (ranked.length === 0) {
|
|
326762
|
-
|
|
326763
|
-
const toProofPathname = (r) => {
|
|
326764
|
-
if (!r)
|
|
326765
|
-
return null;
|
|
326766
|
-
const p = r.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
326767
|
-
return p && p !== "/" && !isLoginPage(p) && p !== loginPathname ? p : null;
|
|
326768
|
-
};
|
|
326769
|
-
const urlRoute = toProofPathname(redirectRoute) ?? toProofPathname(landing.route);
|
|
326770
|
-
if (urlRoute) {
|
|
326771
|
-
logs2(` \u2713 url auth-proof: authenticated landing route ${urlRoute} (no durable element proof on the default-state page)`);
|
|
326772
|
-
return { kind: "url", route: urlRoute };
|
|
326773
|
-
}
|
|
326774
|
-
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)");
|
|
327330
|
+
logs2(" \u26A0 auth-proof: no durable login-absent element proof on the authenticated page (no url fallback \u2014 coverage deferred to a positive proof)");
|
|
326775
327331
|
return null;
|
|
326776
327332
|
}
|
|
326777
327333
|
ranked.sort((a, b) => a.tier - b.tier);
|
|
326778
327334
|
logs2(` \u2713 live auth-proof: ${ranked[0].desc}`);
|
|
326779
327335
|
return ranked[0].proof;
|
|
326780
327336
|
}
|
|
327337
|
+
function harvestCollapsedAuthProof(landingElements, loginAbsentElements, credentials, logs2) {
|
|
327338
|
+
const loginRoleNames = /* @__PURE__ */ new Set();
|
|
327339
|
+
const loginTestIds = /* @__PURE__ */ new Set();
|
|
327340
|
+
for (const el of loginAbsentElements) {
|
|
327341
|
+
const nm = (el.label ?? "").trim().toLowerCase();
|
|
327342
|
+
if (el.role && nm)
|
|
327343
|
+
loginRoleNames.add(`${el.role}|${nm}`);
|
|
327344
|
+
if (el.testId)
|
|
327345
|
+
loginTestIds.add(el.testId);
|
|
327346
|
+
}
|
|
327347
|
+
return rankDurableAuthProof(landingElements, loginRoleNames, loginTestIds, credentials, logs2);
|
|
327348
|
+
}
|
|
327349
|
+
function pickAuthLandingPage(siteMap, redirectRoute, loginRoute) {
|
|
327350
|
+
const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
|
|
327351
|
+
const authedPages = siteMap.pages.filter((p) => (p.requiresAuth || redirectRoute !== null && p.route === redirectRoute) && !isLoginPage(p.route));
|
|
327352
|
+
return (redirectRoute !== null ? authedPages.find((p) => p.route === redirectRoute) : void 0) ?? authedPages[0] ?? null;
|
|
327353
|
+
}
|
|
327354
|
+
function selectAuthLandingRoute(siteMap, redirectRoute, loginRoute) {
|
|
327355
|
+
return pickAuthLandingPage(siteMap, redirectRoute, loginRoute)?.route ?? null;
|
|
327356
|
+
}
|
|
327357
|
+
function harvestAuthProofLive(siteMap, redirectRoute, loginRoute, credentials, logs2) {
|
|
327358
|
+
const isLoginPage = (route2) => loginRoute != null && route2 === loginRoute || AUTH_ROUTE_RE.test(route2 ?? "");
|
|
327359
|
+
const landing = pickAuthLandingPage(siteMap, redirectRoute, loginRoute);
|
|
327360
|
+
if (!landing) {
|
|
327361
|
+
logs2(" \u26A0 auth-proof: no authenticated landing page captured \u2014 cannot derive a durable proof");
|
|
327362
|
+
return null;
|
|
327363
|
+
}
|
|
327364
|
+
const loginRoleNames = /* @__PURE__ */ new Set();
|
|
327365
|
+
const loginTestIds = /* @__PURE__ */ new Set();
|
|
327366
|
+
for (const p of siteMap.pages) {
|
|
327367
|
+
if (!isLoginPage(p.route) || p.route === landing.route)
|
|
327368
|
+
continue;
|
|
327369
|
+
for (const el of p.elements ?? []) {
|
|
327370
|
+
const nm = (el.label ?? "").trim().toLowerCase();
|
|
327371
|
+
if (el.role && nm)
|
|
327372
|
+
loginRoleNames.add(`${el.role}|${nm}`);
|
|
327373
|
+
if (el.testId)
|
|
327374
|
+
loginTestIds.add(el.testId);
|
|
327375
|
+
}
|
|
327376
|
+
}
|
|
327377
|
+
return rankDurableAuthProof(landing.elements ?? [], loginRoleNames, loginTestIds, credentials, logs2);
|
|
327378
|
+
}
|
|
327379
|
+
var AUTH_ROUTE_RE = /\/(log[\s_-]?in|sign[\s_-]?in|sign[\s_-]?up|register|auth|forgot|reset)\b/i;
|
|
327380
|
+
var AUTO_NAV_TRIGGERS = /* @__PURE__ */ new Set(["AUTO", "REDIRECT"]);
|
|
327381
|
+
var USER_ACTION_TRIGGERS = /* @__PURE__ */ new Set(["CLICK", "SUBMIT", "NAVIGATION"]);
|
|
327382
|
+
var stripToBarePath = (route2) => {
|
|
327383
|
+
if (!route2)
|
|
327384
|
+
return null;
|
|
327385
|
+
let path = route2;
|
|
327386
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(route2)) {
|
|
327387
|
+
try {
|
|
327388
|
+
path = new URL(route2).pathname;
|
|
327389
|
+
} catch {
|
|
327390
|
+
}
|
|
327391
|
+
}
|
|
327392
|
+
const p = path.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
327393
|
+
return p || "/";
|
|
327394
|
+
};
|
|
327395
|
+
function detectRedirectBackCollapse(baseUrl, transitions) {
|
|
327396
|
+
let basePath;
|
|
327397
|
+
try {
|
|
327398
|
+
basePath = stripToBarePath(new URL(baseUrl).pathname);
|
|
327399
|
+
} catch {
|
|
327400
|
+
return null;
|
|
327401
|
+
}
|
|
327402
|
+
if (!basePath || basePath === "/" || AUTH_ROUTE_RE.test(basePath))
|
|
327403
|
+
return null;
|
|
327404
|
+
const userActionEdges = new Set(transitions.filter((t) => USER_ACTION_TRIGGERS.has(t.triggerType)).map((t) => `${stripToBarePath(t.fromRoute) ?? ""} ${stripToBarePath(t.toRoute) ?? ""}`));
|
|
327405
|
+
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) ?? ""}`));
|
|
327406
|
+
if (bounceIdx < 0)
|
|
327407
|
+
return null;
|
|
327408
|
+
const authRoute = stripToBarePath(transitions[bounceIdx].toRoute);
|
|
327409
|
+
if (!authRoute)
|
|
327410
|
+
return null;
|
|
327411
|
+
const returned = transitions.slice(bounceIdx + 1).some((t) => AUTH_ROUTE_RE.test(stripToBarePath(t.fromRoute) ?? "") && stripToBarePath(t.toRoute) === basePath);
|
|
327412
|
+
if (!returned)
|
|
327413
|
+
return null;
|
|
327414
|
+
return { basePath, authRoute };
|
|
327415
|
+
}
|
|
327416
|
+
function pickRouteElements(routeElements, wantRoute) {
|
|
327417
|
+
const want = stripToBarePath(wantRoute);
|
|
327418
|
+
for (const [route2, els] of routeElements) {
|
|
327419
|
+
if (stripToBarePath(route2) === want && (els?.length ?? 0) > 0)
|
|
327420
|
+
return els;
|
|
327421
|
+
}
|
|
327422
|
+
return [];
|
|
327423
|
+
}
|
|
327424
|
+
function pickLoginFormElements(routeElements, authRoute) {
|
|
327425
|
+
const exact = pickRouteElements(routeElements, authRoute);
|
|
327426
|
+
if (exact.length > 0)
|
|
327427
|
+
return exact;
|
|
327428
|
+
for (const [route2, els] of routeElements) {
|
|
327429
|
+
if (AUTH_ROUTE_RE.test(stripToBarePath(route2) ?? "") && (els?.length ?? 0) > 0)
|
|
327430
|
+
return els;
|
|
327431
|
+
}
|
|
327432
|
+
return [];
|
|
327433
|
+
}
|
|
326781
327434
|
function findCredentialLiteralLeaks(rendered, credentials) {
|
|
326782
327435
|
const leaked = [];
|
|
326783
327436
|
for (const [key, value] of Object.entries(credentials)) {
|
|
@@ -326790,12 +327443,22 @@ ${keyLines || "- (none provided)"}
|
|
|
326790
327443
|
return leaked;
|
|
326791
327444
|
}
|
|
326792
327445
|
async function verifyAuthRecipe(recipe, baseUrl, credentials, logs2) {
|
|
327446
|
+
const baselineRoute = recipe.redirectRoute ?? baseUrl;
|
|
326793
327447
|
const spec = [
|
|
326794
327448
|
`import { test, expect } from '@playwright/test';`,
|
|
326795
327449
|
``,
|
|
326796
327450
|
(0, auth_recipe_js_1.renderAuthFn)(recipe),
|
|
326797
327451
|
``,
|
|
326798
327452
|
`test('__vqa_auth_verify', async ({ page }) => {`,
|
|
327453
|
+
` // DIFFERENTIAL BASELINE (logged out): the durable proof must NOT already hold here, else it`,
|
|
327454
|
+
` // does not discriminate an authenticated session from an anonymous one \u2014 reject rather than`,
|
|
327455
|
+
` // publish a proof that passes while logged out (the cardinal invariant). The goto is NOT`,
|
|
327456
|
+
` // .catch-swallowed: a genuine navigation failure must fail the verify (fail-closed) rather than`,
|
|
327457
|
+
` // leave the baseline running on about:blank where the proof is vacuously "absent". An auth-gated`,
|
|
327458
|
+
` // route resolves fine (it follows the redirect to /login).`,
|
|
327459
|
+
` await page.goto(${JSON.stringify(baselineRoute)}, { waitUntil: 'domcontentloaded' });`,
|
|
327460
|
+
` ${(0, auth_recipe_js_1.renderProofBaselineCheck)(recipe.authProof, " ")}`,
|
|
327461
|
+
` // AUTHED: real login + the durable proof assertion (inside __vqaLogin).`,
|
|
326799
327462
|
` await __vqaLogin(page);`,
|
|
326800
327463
|
`});`,
|
|
326801
327464
|
``
|
|
@@ -326897,21 +327560,50 @@ ${keyLines || "- (none provided)"}
|
|
|
326897
327560
|
cmds: login.cmds.map((c) => (0, scrub_credentials_js_1.scrubCredentials)(c, credentials)),
|
|
326898
327561
|
preNavCmds: login.preNavCmds?.map((c) => (0, scrub_credentials_js_1.scrubCredentials)(c, credentials))
|
|
326899
327562
|
};
|
|
326900
|
-
|
|
327563
|
+
let authProof = harvestAuthProofLive(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route, credentials, logs2);
|
|
327564
|
+
let effectiveLogin = scrubbedLogin;
|
|
327565
|
+
if (authProof && (authProof.kind === "role" || authProof.kind === "testid")) {
|
|
327566
|
+
const landingRoute = selectAuthLandingRoute(loginSiteMap, scrubbedLogin.redirectRoute, scrubbedLogin.route);
|
|
327567
|
+
if (landingRoute)
|
|
327568
|
+
effectiveLogin = { ...effectiveLogin, redirectRoute: landingRoute };
|
|
327569
|
+
}
|
|
326901
327570
|
if (!authProof) {
|
|
326902
|
-
|
|
326903
|
-
|
|
327571
|
+
const collapse = detectRedirectBackCollapse(baseUrl, exploreResult.collectedTransitions);
|
|
327572
|
+
if (collapse) {
|
|
327573
|
+
const landingEls = pickRouteElements(handle.routeElements, collapse.basePath);
|
|
327574
|
+
const loginEls = pickLoginFormElements(handle.routeElements, collapse.authRoute);
|
|
327575
|
+
const collapsedProof = harvestCollapsedAuthProof(landingEls, loginEls, credentials, logs2);
|
|
327576
|
+
if (collapsedProof) {
|
|
327577
|
+
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)`);
|
|
327578
|
+
authProof = collapsedProof;
|
|
327579
|
+
effectiveLogin = { ...effectiveLogin, redirectRoute: collapse.basePath };
|
|
327580
|
+
} else {
|
|
327581
|
+
logs2(" \u26A0 redirect-back-to-entry collapse detected but no durable authed-only element differenced (no url fallback) \u2014 deferring to model-authored login");
|
|
327582
|
+
}
|
|
327583
|
+
}
|
|
326904
327584
|
}
|
|
326905
|
-
|
|
326906
|
-
|
|
326907
|
-
|
|
326908
|
-
|
|
326909
|
-
|
|
326910
|
-
|
|
327585
|
+
let identityEndpointProof = null;
|
|
327586
|
+
try {
|
|
327587
|
+
const cookies = await handle.page.context().cookies();
|
|
327588
|
+
if (cookies.some((c) => /(next-auth|authjs)\.session-token/.test(c.name))) {
|
|
327589
|
+
identityEndpointProof = { kind: "endpoint", url: "/api/auth/session" };
|
|
327590
|
+
logs2(" \u2139 NextAuth/Auth.js session cookie detected \u2014 trying an identity-endpoint proof (/api/auth/session) before the element proof");
|
|
327591
|
+
}
|
|
327592
|
+
} catch {
|
|
326911
327593
|
}
|
|
326912
|
-
const
|
|
326913
|
-
if (
|
|
326914
|
-
|
|
327594
|
+
const proofCandidates = [];
|
|
327595
|
+
if (identityEndpointProof)
|
|
327596
|
+
proofCandidates.push(identityEndpointProof);
|
|
327597
|
+
if (authProof)
|
|
327598
|
+
proofCandidates.push(authProof);
|
|
327599
|
+
if (proofCandidates.length === 0) {
|
|
327600
|
+
logs2(" \u26A0 auth unverified (no positive proof \u2014 no identity endpoint, no login-absent element) \u2014 falling back to model-authored login");
|
|
327601
|
+
return { recipe: null, outcome: exploreMiss("verify_failed") };
|
|
327602
|
+
}
|
|
327603
|
+
const loginText = [...effectiveLogin.cmds, ...effectiveLogin.preNavCmds ?? []].join("\n") + "\n" + JSON.stringify(effectiveLogin.replayPlan ?? []);
|
|
327604
|
+
const loginLeaks = findCredentialLiteralLeaks(loginText, credentials);
|
|
327605
|
+
if (loginLeaks.length > 0) {
|
|
327606
|
+
logs2(` \u26A0 auth recipe rejected: login sequence contains literal credential value(s) for ${loginLeaks.join(", ")} \u2014 falling back to model-authored login`);
|
|
326915
327607
|
return { recipe: null, outcome: "credential_leak" };
|
|
326916
327608
|
}
|
|
326917
327609
|
try {
|
|
@@ -326919,14 +327611,25 @@ ${keyLines || "- (none provided)"}
|
|
|
326919
327611
|
} catch {
|
|
326920
327612
|
}
|
|
326921
327613
|
handle = null;
|
|
326922
|
-
|
|
326923
|
-
|
|
326924
|
-
|
|
326925
|
-
|
|
327614
|
+
let lastReject = "no candidate verified";
|
|
327615
|
+
for (const proof of proofCandidates) {
|
|
327616
|
+
const candidate = { ...(0, auth_recipe_js_1.buildAuthRecipe)(effectiveLogin, proof, (/* @__PURE__ */ new Date()).toISOString()), appOrigin: loginRedirectAppOrigin };
|
|
327617
|
+
const renderLeaks = findCredentialLiteralLeaks((0, auth_recipe_js_1.renderAuthFn)(candidate), credentials);
|
|
327618
|
+
if (renderLeaks.length > 0) {
|
|
327619
|
+
lastReject = `${describeAuthProof(proof)} render coincidentally matched credential value(s) ${renderLeaks.join(", ")}`;
|
|
327620
|
+
logs2(` \u26A0 auth-proof candidate skipped (${lastReject}) \u2014 trying the next candidate`);
|
|
327621
|
+
continue;
|
|
327622
|
+
}
|
|
327623
|
+
const ok = await verifyAuthRecipe(candidate, baseUrl, credentials, logs2);
|
|
327624
|
+
if (ok) {
|
|
327625
|
+
logs2(` \u2713 auth recipe verified (hash ${candidate.recipeHash}, proof ${describeAuthProof(proof)}, ${(candidate.replayPlan ?? []).length} replay steps) \u2014 stamping into @needs-auth tests`);
|
|
327626
|
+
return { recipe: candidate, outcome: "captured" };
|
|
327627
|
+
}
|
|
327628
|
+
lastReject = `${describeAuthProof(proof)} failed the differential verify`;
|
|
327629
|
+
logs2(` \u26A0 auth-proof candidate rejected (${lastReject}) \u2014 trying the next candidate`);
|
|
326926
327630
|
}
|
|
326927
|
-
|
|
326928
|
-
|
|
326929
|
-
return { recipe: candidate, outcome: "captured" };
|
|
327631
|
+
logs2(` \u26A0 auth unverified (all proof candidates failed: ${lastReject}) \u2014 falling back to model-authored login`);
|
|
327632
|
+
return { recipe: null, outcome: exploreMiss("verify_failed") };
|
|
326930
327633
|
} catch (err) {
|
|
326931
327634
|
logs2(` \u26A0 auth-capture run failed: ${err instanceof Error ? err.message : String(err)} \u2014 falling back to model-authored login`);
|
|
326932
327635
|
return { recipe: null, outcome: (0, ai_retry_js_1.isTransientRateError)(err) ? "rate_limited" : "error" };
|
|
@@ -334053,8 +334756,13 @@ Respond with ONLY valid JSON:
|
|
|
334053
334756
|
if (!(0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && ctx.mode !== "capture")
|
|
334054
334757
|
return null;
|
|
334055
334758
|
if (ctx.authRecipe) {
|
|
334056
|
-
|
|
334057
|
-
|
|
334759
|
+
if (ctx.authRecipe.authProof?.kind === "url") {
|
|
334760
|
+
pushLog(`auth-prologue: persisted recipe ${ctx.authRecipe.recipeHash} uses a demoted url proof \u2014 re-capturing with the differential pipeline (legacy invalidation)`);
|
|
334761
|
+
ctx.authRecipe = void 0;
|
|
334762
|
+
} else {
|
|
334763
|
+
pushLog(`auth-prologue: reusing persisted login recipe ${ctx.authRecipe.recipeHash} \u2014 skipping capture`);
|
|
334764
|
+
return Promise.resolve(ctx.authRecipe);
|
|
334765
|
+
}
|
|
334058
334766
|
}
|
|
334059
334767
|
const exploreModel = ctx.exploreModel ?? "";
|
|
334060
334768
|
const authCaptureModel = ctx.authCaptureModel ?? "";
|
|
@@ -349601,7 +350309,7 @@ var require_package4 = __commonJS({
|
|
|
349601
350309
|
"package.json"(exports, module) {
|
|
349602
350310
|
module.exports = {
|
|
349603
350311
|
name: "@validate.qa/runner",
|
|
349604
|
-
version: "1.0.
|
|
350312
|
+
version: "1.0.58",
|
|
349605
350313
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
349606
350314
|
bin: {
|
|
349607
350315
|
"validate-runner": "dist/cli.js"
|
|
@@ -351280,6 +351988,12 @@ function isKeyboardAlreadyHiddenError(err) {
|
|
|
351280
351988
|
return /keyboard.*(not|already).*(shown|visible|present|open)|no.*keyboard/i.test(message);
|
|
351281
351989
|
}
|
|
351282
351990
|
async function dismissKeyboard(sessionPath, platform3) {
|
|
351991
|
+
try {
|
|
351992
|
+
const shown = await appiumRequest(`${sessionPath}/appium/device/is_keyboard_shown`, { method: "GET" });
|
|
351993
|
+
if (readValue(shown) === false) return;
|
|
351994
|
+
} catch (err) {
|
|
351995
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
351996
|
+
}
|
|
351283
351997
|
if (platform3 === "ANDROID") {
|
|
351284
351998
|
await pressBack(sessionPath);
|
|
351285
351999
|
return;
|
|
@@ -355866,11 +356580,17 @@ var MobileBridgeDriver = class {
|
|
|
355866
356580
|
}
|
|
355867
356581
|
/** Type `value`; pass `bounds` to tap-to-focus first when nothing is focused. */
|
|
355868
356582
|
async type(value, bounds) {
|
|
355869
|
-
return this.runAction(() =>
|
|
356583
|
+
return this.runAction(async () => {
|
|
356584
|
+
await typeText(this.sessionPath, value, bounds);
|
|
356585
|
+
await hideKeyboardIfShown(this.sessionPath);
|
|
356586
|
+
});
|
|
355870
356587
|
}
|
|
355871
356588
|
/** Invoke the focused field's Enter/Done action without clearing its value. */
|
|
355872
356589
|
async submitKeyboard(bounds) {
|
|
355873
|
-
return this.runAction(() =>
|
|
356590
|
+
return this.runAction(async () => {
|
|
356591
|
+
await submitKeyboard(this.sessionPath, bounds);
|
|
356592
|
+
await hideKeyboardIfShown(this.sessionPath);
|
|
356593
|
+
});
|
|
355874
356594
|
}
|
|
355875
356595
|
/** Clear the focused element; pass `bounds` to tap-to-focus first. */
|
|
355876
356596
|
async clear(bounds) {
|