@validate.qa/runner 1.0.91 → 1.0.93
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 +1239 -180
- package/dist/cli.mjs +1239 -180
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2038,7 +2038,8 @@ var init_project_key = __esm({
|
|
|
2038
2038
|
"reviews",
|
|
2039
2039
|
"admin",
|
|
2040
2040
|
"login",
|
|
2041
|
-
"admin-view"
|
|
2041
|
+
"admin-view",
|
|
2042
|
+
"mcp"
|
|
2042
2043
|
]);
|
|
2043
2044
|
PROJECT_KEY_MIN_LENGTH = 2;
|
|
2044
2045
|
PROJECT_KEY_MAX_LENGTH = 12;
|
|
@@ -3129,22 +3130,23 @@ function labelForTarget(target) {
|
|
|
3129
3130
|
return nonEmpty2(target?.locatorBundle?.accessibilityId) ?? nonEmpty2(target?.locatorBundle?.text) ?? nonEmpty2(target?.locatorBundle?.contentDesc) ?? nonEmpty2(target?.primary);
|
|
3130
3131
|
}
|
|
3131
3132
|
function semanticTarget(label, platform3, base2) {
|
|
3132
|
-
const
|
|
3133
|
+
const textXPath = mobileTextXPath(label, platform3);
|
|
3134
|
+
const primary = nonEmpty2(base2?.primary) ?? textXPath;
|
|
3133
3135
|
return {
|
|
3134
3136
|
primary,
|
|
3135
3137
|
fallbacks: uniq([
|
|
3138
|
+
...base2?.fallbacks ?? [],
|
|
3139
|
+
textXPath,
|
|
3136
3140
|
`~${label}`,
|
|
3137
|
-
label
|
|
3138
|
-
base2?.primary,
|
|
3139
|
-
...base2?.fallbacks ?? []
|
|
3141
|
+
label
|
|
3140
3142
|
]).filter((value) => value !== primary),
|
|
3141
3143
|
screenName: base2?.screenName ?? "HealedAssertion",
|
|
3142
3144
|
locatorBundle: {
|
|
3143
|
-
accessibilityId: label,
|
|
3144
|
-
xpath:
|
|
3145
|
-
className: "",
|
|
3146
|
-
text: label,
|
|
3147
|
-
contentDesc: label,
|
|
3145
|
+
accessibilityId: nonEmpty2(base2?.locatorBundle?.accessibilityId) ?? label,
|
|
3146
|
+
xpath: nonEmpty2(base2?.locatorBundle?.xpath) ?? textXPath,
|
|
3147
|
+
className: base2?.locatorBundle?.className ?? "",
|
|
3148
|
+
text: nonEmpty2(base2?.locatorBundle?.text) ?? label,
|
|
3149
|
+
contentDesc: nonEmpty2(base2?.locatorBundle?.contentDesc) ?? label,
|
|
3148
3150
|
...base2?.locatorBundle?.resourceId ? { resourceId: base2.locatorBundle.resourceId } : {}
|
|
3149
3151
|
}
|
|
3150
3152
|
};
|
|
@@ -3322,6 +3324,26 @@ var init_mobile_step_healer = __esm({
|
|
|
3322
3324
|
});
|
|
3323
3325
|
|
|
3324
3326
|
// ../shared/dist/mobile-cross-platform.js
|
|
3327
|
+
function androidActivityPackageOf(owner) {
|
|
3328
|
+
const lastDot = owner.lastIndexOf(".");
|
|
3329
|
+
if (lastDot <= 0)
|
|
3330
|
+
return null;
|
|
3331
|
+
return /^[A-Z]/.test(owner.slice(lastDot + 1)) ? owner.slice(0, lastDot) : null;
|
|
3332
|
+
}
|
|
3333
|
+
function mobileScreenOwnerBelongsToApp(owner, targetAppId) {
|
|
3334
|
+
const ownerId = owner.trim();
|
|
3335
|
+
const appId = targetAppId.trim();
|
|
3336
|
+
if (!ownerId || !appId)
|
|
3337
|
+
return false;
|
|
3338
|
+
if (ownerId === appId)
|
|
3339
|
+
return true;
|
|
3340
|
+
if (ownerId.startsWith(`${appId}.`))
|
|
3341
|
+
return true;
|
|
3342
|
+
const activityPackage = androidActivityPackageOf(ownerId);
|
|
3343
|
+
if (activityPackage && appId.startsWith(`${activityPackage}.`))
|
|
3344
|
+
return true;
|
|
3345
|
+
return false;
|
|
3346
|
+
}
|
|
3325
3347
|
function isMobilePlatform(value) {
|
|
3326
3348
|
return value === "IOS" || value === "ANDROID";
|
|
3327
3349
|
}
|
|
@@ -6574,6 +6596,7 @@ __export(dist_exports, {
|
|
|
6574
6596
|
mobileCredentialPlaceholder: () => mobileCredentialPlaceholder,
|
|
6575
6597
|
mobileLocatorBundleHasIdentity: () => mobileLocatorBundleHasIdentity,
|
|
6576
6598
|
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
6599
|
+
mobileScreenOwnerBelongsToApp: () => mobileScreenOwnerBelongsToApp,
|
|
6577
6600
|
mobileWanderTraceEnabled: () => mobileWanderTraceEnabled,
|
|
6578
6601
|
normalizeControlName: () => normalizeControlName,
|
|
6579
6602
|
normalizeTtsModel: () => normalizeTtsModel,
|
|
@@ -8532,6 +8555,127 @@ var require_scrub_credentials = __commonJS({
|
|
|
8532
8555
|
}
|
|
8533
8556
|
});
|
|
8534
8557
|
|
|
8558
|
+
// ../runner-core/dist/services/google-signin.js
|
|
8559
|
+
var require_google_signin = __commonJS({
|
|
8560
|
+
"../runner-core/dist/services/google-signin.js"(exports2) {
|
|
8561
|
+
"use strict";
|
|
8562
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
8563
|
+
exports2.GOOGLE_SIGNIN_ENV_KEYS = exports2.GOOGLE_SIGNIN_PASSWORD_KEY = void 0;
|
|
8564
|
+
exports2.loadGoogleSignInConfig = loadGoogleSignInConfig;
|
|
8565
|
+
exports2.mergeGoogleSignInSecrets = mergeGoogleSignInSecrets;
|
|
8566
|
+
exports2.googleSignInGuidanceOpts = googleSignInGuidanceOpts;
|
|
8567
|
+
exports2.googleSignInAllowedEmails = googleSignInAllowedEmails;
|
|
8568
|
+
exports2.collectGoogleSignInEnv = collectGoogleSignInEnv;
|
|
8569
|
+
exports2.generateTotp = generateTotp;
|
|
8570
|
+
exports2.buildGoogleSignInPromptSection = buildGoogleSignInPromptSection;
|
|
8571
|
+
var node_crypto_1 = require("crypto");
|
|
8572
|
+
exports2.GOOGLE_SIGNIN_PASSWORD_KEY = "GOOGLE_SIGNIN_PASSWORD";
|
|
8573
|
+
exports2.GOOGLE_SIGNIN_ENV_KEYS = [
|
|
8574
|
+
"GOOGLE_SIGNIN_PASSWORD",
|
|
8575
|
+
"GOOGLE_SIGNIN_EMAIL",
|
|
8576
|
+
"GOOGLE_SIGNIN_TOTP_SECRET"
|
|
8577
|
+
];
|
|
8578
|
+
function trimEnv(env, name) {
|
|
8579
|
+
const value = env[name]?.trim();
|
|
8580
|
+
return value ? value : void 0;
|
|
8581
|
+
}
|
|
8582
|
+
function loadGoogleSignInConfig(env = process.env) {
|
|
8583
|
+
const password = trimEnv(env, "GOOGLE_SIGNIN_PASSWORD");
|
|
8584
|
+
if (!password)
|
|
8585
|
+
return null;
|
|
8586
|
+
const email = (trimEnv(env, "GOOGLE_SIGNIN_EMAIL") ?? trimEnv(env, "VERIFICATION_INBOX_ADDRESS"))?.toLowerCase();
|
|
8587
|
+
if (!email || !email.includes("@"))
|
|
8588
|
+
return null;
|
|
8589
|
+
const totpSecret = trimEnv(env, "GOOGLE_SIGNIN_TOTP_SECRET");
|
|
8590
|
+
return {
|
|
8591
|
+
email,
|
|
8592
|
+
password,
|
|
8593
|
+
passwordKey: exports2.GOOGLE_SIGNIN_PASSWORD_KEY,
|
|
8594
|
+
totpSecret,
|
|
8595
|
+
totpAvailable: !!totpSecret
|
|
8596
|
+
};
|
|
8597
|
+
}
|
|
8598
|
+
function mergeGoogleSignInSecrets(credentials, env = process.env) {
|
|
8599
|
+
const cfg = loadGoogleSignInConfig(env);
|
|
8600
|
+
if (!cfg)
|
|
8601
|
+
return credentials;
|
|
8602
|
+
if (credentials[exports2.GOOGLE_SIGNIN_PASSWORD_KEY])
|
|
8603
|
+
return credentials;
|
|
8604
|
+
return { ...credentials, [exports2.GOOGLE_SIGNIN_PASSWORD_KEY]: cfg.password };
|
|
8605
|
+
}
|
|
8606
|
+
function googleSignInGuidanceOpts(env = process.env) {
|
|
8607
|
+
const cfg = loadGoogleSignInConfig(env);
|
|
8608
|
+
if (!cfg)
|
|
8609
|
+
return void 0;
|
|
8610
|
+
return {
|
|
8611
|
+
googleEmail: cfg.email,
|
|
8612
|
+
googlePasswordKey: cfg.passwordKey,
|
|
8613
|
+
googleTotpAvailable: cfg.totpAvailable
|
|
8614
|
+
};
|
|
8615
|
+
}
|
|
8616
|
+
function googleSignInAllowedEmails(env = process.env) {
|
|
8617
|
+
const cfg = loadGoogleSignInConfig(env);
|
|
8618
|
+
return cfg ? [cfg.email] : [];
|
|
8619
|
+
}
|
|
8620
|
+
function collectGoogleSignInEnv(env = process.env) {
|
|
8621
|
+
const out = {};
|
|
8622
|
+
for (const key of exports2.GOOGLE_SIGNIN_ENV_KEYS) {
|
|
8623
|
+
const value = env[key]?.trim();
|
|
8624
|
+
if (value)
|
|
8625
|
+
out[key] = value;
|
|
8626
|
+
}
|
|
8627
|
+
return out;
|
|
8628
|
+
}
|
|
8629
|
+
function generateTotp(secret, atMs = Date.now(), stepSeconds = 30, digits = 6) {
|
|
8630
|
+
const key = decodeBase32(secret);
|
|
8631
|
+
const counter = Math.floor(atMs / 1e3 / stepSeconds);
|
|
8632
|
+
const buf = Buffer.alloc(8);
|
|
8633
|
+
buf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
8634
|
+
buf.writeUInt32BE(counter >>> 0, 4);
|
|
8635
|
+
const hmac = (0, node_crypto_1.createHmac)("sha1", key).update(buf).digest();
|
|
8636
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
8637
|
+
const bin = (hmac[offset] & 127) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3];
|
|
8638
|
+
const otp = bin % 10 ** digits;
|
|
8639
|
+
return String(otp).padStart(digits, "0");
|
|
8640
|
+
}
|
|
8641
|
+
function decodeBase32(input) {
|
|
8642
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
8643
|
+
const cleaned = input.toUpperCase().replace(/[\s=-]+/g, "");
|
|
8644
|
+
let bits = 0;
|
|
8645
|
+
let value = 0;
|
|
8646
|
+
const out = [];
|
|
8647
|
+
for (const ch of cleaned) {
|
|
8648
|
+
const idx = alphabet.indexOf(ch);
|
|
8649
|
+
if (idx < 0)
|
|
8650
|
+
continue;
|
|
8651
|
+
value = value << 5 | idx;
|
|
8652
|
+
bits += 5;
|
|
8653
|
+
if (bits >= 8) {
|
|
8654
|
+
bits -= 8;
|
|
8655
|
+
out.push(value >>> bits & 255);
|
|
8656
|
+
}
|
|
8657
|
+
}
|
|
8658
|
+
return Buffer.from(out);
|
|
8659
|
+
}
|
|
8660
|
+
function buildGoogleSignInPromptSection(opts, variant) {
|
|
8661
|
+
const totpNative = opts.googleTotpAvailable ? " If Google asks for a 2-step / Authenticator code, call `provision_google_totp` and type the 6-digit `code` it returns (do not invent a code)." : " If Google asks for 2-step verification and you have no authenticator code tool, STOP and report Google SSO as BLOCKED (2FA not configured on this runner).";
|
|
8662
|
+
const totpCli = opts.googleTotpAvailable ? " If Google asks for a 2-step / Authenticator code, call `inbox_google_totp` and type the 6-digit `code` it returns." : " If Google asks for 2-step verification, STOP and report Google SSO as BLOCKED (2FA not configured on this runner).";
|
|
8663
|
+
const totp = variant === "cli" ? totpCli : totpNative;
|
|
8664
|
+
const fill = variant === "cli" ? `fill_credential with credentialKey \`${opts.googlePasswordKey}\`` : `\`fill_credential\` (credentialKey \`${opts.googlePasswordKey}\`)`;
|
|
8665
|
+
const plainProvision = variant === "cli" ? "`inbox_provision_identity` with `plain: true`" : "`provision_identity` with `plain: true`";
|
|
8666
|
+
return `
|
|
8667
|
+
Google / Gmail SSO fallback (SECOND choice \u2014 email+password signup is FIRST):
|
|
8668
|
+
- FIRST follow the email-field playbook above whenever a signup form has an email / username / password field (including after clicking "Sign up with email"). Do NOT click Google if that form exists.
|
|
8669
|
+
- ONLY if there is no email+password signup field and the only path is "Continue with Google" / "Sign up with Gmail" / "Sign in with Google": click that button. If a Google popup opens, switch to it (browser tabs).
|
|
8670
|
+
- On accounts.google.com, type the BARE Google account email \`${opts.googleEmail}\` (no plus-tag \u2014 Google account login rejects plus-addresses) with browser_type, click Next, then fill the Google password with ${fill}. NEVER type the Google password as text and NEVER use the IMAP app password here.
|
|
8671
|
+
- Click Next.${totp}
|
|
8672
|
+
- Click Continue / Allow on any consent screen, then return to the app tab.
|
|
8673
|
+
- If the app then emails a verification code/link, first call ${plainProvision} (Google SSO uses the bare inbox address, not the +tag), then complete it with the inbox tools as usual.
|
|
8674
|
+
- Register / Google-sign-in ONCE per run. If Google says the account is already used on this app, that is success \u2014 continue as the logged-in user.`;
|
|
8675
|
+
}
|
|
8676
|
+
}
|
|
8677
|
+
});
|
|
8678
|
+
|
|
8535
8679
|
// ../runner-core/dist/services/credential-tools.js
|
|
8536
8680
|
var require_credential_tools = __commonJS({
|
|
8537
8681
|
"../runner-core/dist/services/credential-tools.js"(exports2) {
|
|
@@ -8550,6 +8694,7 @@ var require_credential_tools = __commonJS({
|
|
|
8550
8694
|
exports2.mcpResultIsError = mcpResultIsError;
|
|
8551
8695
|
exports2.aggressiveScrubCredentials = aggressiveScrubCredentials;
|
|
8552
8696
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
8697
|
+
var google_signin_js_1 = require_google_signin();
|
|
8553
8698
|
exports2.FILL_CREDENTIAL_TOOL = {
|
|
8554
8699
|
type: "function",
|
|
8555
8700
|
function: {
|
|
@@ -8560,7 +8705,7 @@ var require_credential_tools = __commonJS({
|
|
|
8560
8705
|
properties: {
|
|
8561
8706
|
credentialKey: {
|
|
8562
8707
|
type: "string",
|
|
8563
|
-
description: 'The credential variable name (e.g. "TEST_USER_EMAIL", "TEST_USER_PASSWORD", "
|
|
8708
|
+
description: 'The credential variable name (e.g. "TEST_USER_EMAIL", "TEST_USER_PASSWORD", "GOOGLE_SIGNIN_PASSWORD"). Must be one of the keys listed in the Test Credentials section of the prompt.'
|
|
8564
8709
|
},
|
|
8565
8710
|
element: {
|
|
8566
8711
|
type: "string",
|
|
@@ -8583,7 +8728,7 @@ var require_credential_tools = __commonJS({
|
|
|
8583
8728
|
type: "function",
|
|
8584
8729
|
function: {
|
|
8585
8730
|
name: "set_credential",
|
|
8586
|
-
description: 'Persist a NEW value for a stored credential after a test has successfully rotated it (
|
|
8731
|
+
description: 'Persist a NEW value for a stored credential after a test has successfully rotated it. REQUIRED after any successful password change OR forgot-password/reset of the STORED test user (the account whose email is TEST_USER_EMAIL) so Test Data stays in sync and later tests do not fail with stale credentials. Also use after in-session "change password" from settings. Call this AFTER you have verified the change succeeded (success toast, re-login with the new value, or confirmed post-reset state). The runner updates the project test variable, fires a CREDENTIAL_UPDATED notification, and the new value is used by all subsequent tests. Never call this on a negative test (one that verifies wrong-credential is rejected) and never call it speculatively before submit+verify. The new value you supply MUST be the value you typed into the page \u2014 the runner does not re-read it from the DOM. Only call for keys already present in the Test Credentials section.',
|
|
8587
8732
|
parameters: {
|
|
8588
8733
|
type: "object",
|
|
8589
8734
|
properties: {
|
|
@@ -8597,7 +8742,7 @@ var require_credential_tools = __commonJS({
|
|
|
8597
8742
|
},
|
|
8598
8743
|
reason: {
|
|
8599
8744
|
type: "string",
|
|
8600
|
-
description: 'Short human-readable explanation of why the credential changed (e.g. "User changed password from settings page"). Surfaced in the notification.'
|
|
8745
|
+
description: 'Short human-readable explanation of why the credential changed (e.g. "User changed password from settings page" or "Password reset completed for stored test user"). Surfaced in the notification.'
|
|
8601
8746
|
}
|
|
8602
8747
|
},
|
|
8603
8748
|
required: ["credentialKey", "newValue"]
|
|
@@ -8659,12 +8804,13 @@ SECURITY RULES:
|
|
|
8659
8804
|
- NEVER include credential values in \`capture_page\` observations, \`record_journey\` steps, \`record_transition\` triggerLabel, or any summary text. (You can't \u2014 they're not in your context \u2014 but do not invent placeholder values either; describe the action as "Fill email with test credentials".)
|
|
8660
8805
|
- Registration flows that need a UNIQUE email (e.g. \`testuser-\${Date.now()}@example.com\`) should still use \`browser_type\` with a synthetic value \u2014 \`fill_credential\` is only for stored login credentials.
|
|
8661
8806
|
${opts.canRotate ? `
|
|
8662
|
-
CREDENTIAL ROTATION (
|
|
8663
|
-
-
|
|
8664
|
-
(1)
|
|
8665
|
-
(2)
|
|
8666
|
-
|
|
8667
|
-
-
|
|
8807
|
+
CREDENTIAL ROTATION (required when the STORED user's secret changes \u2014 read carefully):
|
|
8808
|
+
- You MUST call \`set_credential\` when the test successfully changes a secret belonging to the STORED test user (the account whose keys are listed above \u2014 typically TEST_USER_EMAIL / TEST_USER_PASSWORD). Covered flows:
|
|
8809
|
+
(1) **In-session change password** \u2014 log in as the stored user, open settings \u2192 Change Password, submit a new password, verify success; OR
|
|
8810
|
+
(2) **Forgot-password / password-reset of the stored user** \u2014 request reset for the stored email (fill_credential with TEST_USER_EMAIL on the forgot-password form), complete the emailed link/code, set a NEW password on the reset form, and verify success. You do NOT need to log in with the old password first \u2014 a successful reset of the stored account still MUST write the new password back.
|
|
8811
|
+
- After either flow succeeds, call \`set_credential\` with credentialKey \`TEST_USER_PASSWORD\` (or the matching stored password key) and newValue = the new password you typed. This updates the Test Data page so every later test uses the correct credentials.
|
|
8812
|
+
- Proof required before calling: success toast / confirmed reset state / re-login with the new value works.
|
|
8813
|
+
- DO NOT call \`set_credential\` if the test registered a fresh throwaway user (e.g. \`testuser-\${Date.now()}@example.com\` or a provision_identity inbox used only for a one-off signup) and changed THAT user's password \u2014 the stored credential is untouched in that case.
|
|
8668
8814
|
- DO NOT call \`set_credential\` on a negative test (one that verifies a bad/mismatched password is rejected, or a permission-denied case).
|
|
8669
8815
|
- DO NOT call it before submit + verify \u2014 a partial failure would corrupt the stored credential.
|
|
8670
8816
|
- Call it AT MOST ONCE per test, AFTER verification. The \`newValue\` is the value you just typed into the new-password / new-token field; the runner persists it server-side and never echoes it back.
|
|
@@ -8825,6 +8971,14 @@ CREDENTIAL ROTATION (rare \u2014 read this carefully):
|
|
|
8825
8971
|
didMutate: false
|
|
8826
8972
|
};
|
|
8827
8973
|
}
|
|
8974
|
+
if (key === google_signin_js_1.GOOGLE_SIGNIN_PASSWORD_KEY) {
|
|
8975
|
+
return {
|
|
8976
|
+
content: JSON.stringify({
|
|
8977
|
+
error: "set_credential: GOOGLE_SIGNIN_PASSWORD is a runner-operator secret and cannot be persisted to project Test Data"
|
|
8978
|
+
}),
|
|
8979
|
+
didMutate: false
|
|
8980
|
+
};
|
|
8981
|
+
}
|
|
8828
8982
|
if (!(key in ctx.credentials)) {
|
|
8829
8983
|
const available = Object.keys(ctx.credentials).filter((k) => ctx.credentials[k]).join(", ") || "(none configured)";
|
|
8830
8984
|
return {
|
|
@@ -74086,7 +74240,7 @@ var require_inbox_tools = __commonJS({
|
|
|
74086
74240
|
"../runner-core/dist/services/inbox-tools.js"(exports2) {
|
|
74087
74241
|
"use strict";
|
|
74088
74242
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
74089
|
-
exports2.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = exports2.INBOX_TOOL_NAMES = exports2.INBOX_TOOLS = exports2.OPEN_VERIFICATION_LINK_TOOL = exports2.GET_VERIFICATION_LINK_TOOL = exports2.EXTRACT_VERIFICATION_CODE_TOOL = exports2.READ_VERIFICATION_EMAIL_TOOL = exports2.WAIT_FOR_VERIFICATION_TOOL = exports2.CHECK_INBOX_TOOL = exports2.PROVISION_IDENTITY_TOOL = void 0;
|
|
74243
|
+
exports2.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = exports2.INBOX_TOOL_NAMES = exports2.INBOX_TOOLS = exports2.PROVISION_GOOGLE_TOTP_TOOL = exports2.OPEN_VERIFICATION_LINK_TOOL = exports2.GET_VERIFICATION_LINK_TOOL = exports2.EXTRACT_VERIFICATION_CODE_TOOL = exports2.READ_VERIFICATION_EMAIL_TOOL = exports2.WAIT_FOR_VERIFICATION_TOOL = exports2.CHECK_INBOX_TOOL = exports2.PROVISION_IDENTITY_TOOL = void 0;
|
|
74090
74244
|
exports2.createInboxRunState = createInboxRunState;
|
|
74091
74245
|
exports2.dispatchInboxTool = dispatchInboxTool;
|
|
74092
74246
|
exports2.isPublicHttpUrl = isPublicHttpUrl;
|
|
@@ -74101,6 +74255,7 @@ var require_inbox_tools = __commonJS({
|
|
|
74101
74255
|
exports2.clearThrowawayRegisterSeed = clearThrowawayRegisterSeed;
|
|
74102
74256
|
exports2.buildInboxAgentsGuidance = buildInboxAgentsGuidance;
|
|
74103
74257
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
74258
|
+
var google_signin_js_1 = require_google_signin();
|
|
74104
74259
|
function createInboxRunState(config, opts = {}) {
|
|
74105
74260
|
const baseEmail = config.address;
|
|
74106
74261
|
const tag = opts.tag !== void 0 ? opts.tag : opts.tagSeed ? (0, verification_inbox_js_1.deriveProjectInboxTag)(opts.tagSeed, opts.projectScope) : null;
|
|
@@ -74222,6 +74377,14 @@ var require_inbox_tools = __commonJS({
|
|
|
74222
74377
|
}
|
|
74223
74378
|
}
|
|
74224
74379
|
};
|
|
74380
|
+
exports2.PROVISION_GOOGLE_TOTP_TOOL = {
|
|
74381
|
+
type: "function",
|
|
74382
|
+
function: {
|
|
74383
|
+
name: "provision_google_totp",
|
|
74384
|
+
description: "Return a one-time 6-digit Authenticator code for the operator Google account. Call this ONLY when Google Sign-In shows a 2-step / Authenticator challenge after you submitted the Google password. Type the returned code into the Google prompt. Do not invent a code.",
|
|
74385
|
+
parameters: { type: "object", properties: {} }
|
|
74386
|
+
}
|
|
74387
|
+
};
|
|
74225
74388
|
exports2.INBOX_TOOLS = [
|
|
74226
74389
|
exports2.PROVISION_IDENTITY_TOOL,
|
|
74227
74390
|
exports2.CHECK_INBOX_TOOL,
|
|
@@ -74229,7 +74392,8 @@ var require_inbox_tools = __commonJS({
|
|
|
74229
74392
|
exports2.READ_VERIFICATION_EMAIL_TOOL,
|
|
74230
74393
|
exports2.EXTRACT_VERIFICATION_CODE_TOOL,
|
|
74231
74394
|
exports2.GET_VERIFICATION_LINK_TOOL,
|
|
74232
|
-
exports2.OPEN_VERIFICATION_LINK_TOOL
|
|
74395
|
+
exports2.OPEN_VERIFICATION_LINK_TOOL,
|
|
74396
|
+
exports2.PROVISION_GOOGLE_TOTP_TOOL
|
|
74233
74397
|
];
|
|
74234
74398
|
exports2.INBOX_TOOL_NAMES = new Set(exports2.INBOX_TOOLS.map((t) => t.function.name));
|
|
74235
74399
|
var DEFAULT_MAX_RESULT_CHARS = 8e3;
|
|
@@ -74241,7 +74405,7 @@ var require_inbox_tools = __commonJS({
|
|
|
74241
74405
|
}
|
|
74242
74406
|
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.";
|
|
74243
74407
|
async function dispatchInboxTool(toolName, toolArgs, ctx) {
|
|
74244
|
-
if (ctx.state.authDead) {
|
|
74408
|
+
if (ctx.state.authDead && toolName !== "provision_google_totp") {
|
|
74245
74409
|
return err(`${toolName} skipped: ${INBOX_AUTH_DEAD_GUIDANCE}`);
|
|
74246
74410
|
}
|
|
74247
74411
|
try {
|
|
@@ -74260,6 +74424,8 @@ var require_inbox_tools = __commonJS({
|
|
|
74260
74424
|
return await handleGetLink(toolArgs, ctx);
|
|
74261
74425
|
case "open_verification_link":
|
|
74262
74426
|
return await handleOpenLink(toolArgs, ctx);
|
|
74427
|
+
case "provision_google_totp":
|
|
74428
|
+
return handleProvisionGoogleTotp();
|
|
74263
74429
|
default:
|
|
74264
74430
|
return err(`inbox dispatch: unknown tool "${toolName}"`);
|
|
74265
74431
|
}
|
|
@@ -74435,6 +74601,20 @@ var require_inbox_tools = __commonJS({
|
|
|
74435
74601
|
didMutate: !failed
|
|
74436
74602
|
};
|
|
74437
74603
|
}
|
|
74604
|
+
function handleProvisionGoogleTotp(env = process.env) {
|
|
74605
|
+
const cfg = (0, google_signin_js_1.loadGoogleSignInConfig)(env);
|
|
74606
|
+
if (!cfg) {
|
|
74607
|
+
return err("Google SSO is not configured on this runner (GOOGLE_SIGNIN_PASSWORD unset). Prefer email-field signup, or report Google SSO as BLOCKED.");
|
|
74608
|
+
}
|
|
74609
|
+
if (!cfg.totpSecret) {
|
|
74610
|
+
return err("Google Authenticator 2FA is not configured on this runner (GOOGLE_SIGNIN_TOTP_SECRET unset). STOP and report Google SSO as BLOCKED (2FA not configured).");
|
|
74611
|
+
}
|
|
74612
|
+
return ok({
|
|
74613
|
+
code: (0, google_signin_js_1.generateTotp)(cfg.totpSecret),
|
|
74614
|
+
digits: 6,
|
|
74615
|
+
instructions: "Type this 6-digit code into the Google 2-step / Authenticator prompt. Do not reuse it later \u2014 call this tool again if Google asks for another code."
|
|
74616
|
+
});
|
|
74617
|
+
}
|
|
74438
74618
|
function requireProvisioned(ctx) {
|
|
74439
74619
|
if (ctx.state.provisioned)
|
|
74440
74620
|
return null;
|
|
@@ -74569,7 +74749,7 @@ var require_inbox_tools = __commonJS({
|
|
|
74569
74749
|
${where} You register using ONE real, monitored email (\`${opts.email}\`) and can complete email-verification (OTP code or confirmation link) end-to-end. This OVERRIDES the general "do not submit forms" rule for the signup/registration form ONLY.
|
|
74570
74750
|
|
|
74571
74751
|
Steps:
|
|
74572
|
-
1. Navigate to the Sign Up / Register page (click the link in the UI).
|
|
74752
|
+
1. Navigate to the Sign Up / Register page (click the link in the UI). FIRST look for an email+password signup form (including after "Sign up with email"). Use Google / Gmail SSO ONLY if that form does not exist.
|
|
74573
74753
|
2. Call \`provision_identity\` \u2192 it returns the email to register with (\`${opts.email}\`). The \`+\` part is a normal sub-address that still lands in our inbox.
|
|
74574
74754
|
3. Fill the signup form: type that email into the email field with \`browser_type\` (this generated inbox email is intentionally visible and is allowed for registration); fill the password field with single-field \`fill_credential\` (credentialKey \`${opts.passwordKey}\`); fill any name/other required fields with simple test values. Do NOT use \`fill_credential_form\` for registration.
|
|
74575
74755
|
- If the form REJECTS the email (says "invalid email" / will not accept the "+"), call \`provision_identity\` again with \`plain: true\` and use the bare address it returns instead.
|
|
@@ -74587,6 +74767,11 @@ Rules:
|
|
|
74587
74767
|
- The provisioned email is reused for this project; if signup says it is ALREADY registered, the account already exists \u2014 log in with that email + the \`${opts.passwordKey}\` password instead of registering again.
|
|
74588
74768
|
- You register ONCE per run. After you submit the signup form in step 4, NEVER submit it again and NEVER create a second account \u2014 even if verification is slow, even if a later login attempt fails. Recover by re-polling \`wait_for_verification\` (and at most one "Resend"), not by re-registering.
|
|
74589
74769
|
- Only call the verification tools AFTER submitting the signup form, and pass the site as \`senderMatch\` so you read YOUR verification email (the inbox is shared).
|
|
74770
|
+
${opts.googleEmail && opts.googlePasswordKey ? (0, google_signin_js_1.buildGoogleSignInPromptSection)({
|
|
74771
|
+
googleEmail: opts.googleEmail,
|
|
74772
|
+
googlePasswordKey: opts.googlePasswordKey,
|
|
74773
|
+
googleTotpAvailable: opts.googleTotpAvailable
|
|
74774
|
+
}, "native") : ""}
|
|
74590
74775
|
`;
|
|
74591
74776
|
}
|
|
74592
74777
|
exports2.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = `## EMAIL VERIFICATION UNAVAILABLE \u2014 FINAL OVERRIDE (supersedes any instruction above)
|
|
@@ -74741,11 +74926,12 @@ This project asked to REGISTER a new account, but this runner CANNOT receive ema
|
|
|
74741
74926
|
}
|
|
74742
74927
|
return false;
|
|
74743
74928
|
}
|
|
74744
|
-
function buildInboxAgentsGuidance(email, passwordKey) {
|
|
74929
|
+
function buildInboxAgentsGuidance(email, passwordKey, google) {
|
|
74930
|
+
const googleBlock = google ? (0, google_signin_js_1.buildGoogleSignInPromptSection)(google, "cli") : "";
|
|
74745
74931
|
return `## Account registration & email verification (ENABLED)
|
|
74746
74932
|
This run registers a new account using our ONE real, monitored inbox (the address \`${email}\` \u2014 the "+" part is a normal sub-address that still lands in our inbox) and can complete email verification end-to-end. This overrides any "do not submit forms" guidance for the signup form only.
|
|
74747
74933
|
|
|
74748
|
-
1. Go to the Sign Up / Register page.
|
|
74934
|
+
1. Go to the Sign Up / Register page. FIRST look for an email+password signup form (including after "Sign up with email"). Use Google / Gmail SSO ONLY if that form does not exist.
|
|
74749
74935
|
2. Call \`inbox_provision_identity\` \u2192 it returns the email to register with (\`${email}\`). Type that email into the email field.
|
|
74750
74936
|
- If the form REJECTS the email (says "invalid email" / will not accept the "+"), call \`inbox_provision_identity\` with \`plain: true\` and use the bare address it returns instead.
|
|
74751
74937
|
3. Fill the password with the \`${passwordKey}\` credential (use fill_credential). Submit the signup form.
|
|
@@ -74756,6 +74942,7 @@ This run registers a new account using our ONE real, monitored inbox (the addres
|
|
|
74756
74942
|
- If it returns BOTH, try the code first; if the app insists on the link, open it, then enter the code if the page it lands on asks for one.
|
|
74757
74943
|
- Otherwise call \`inbox_read_email\` to read the body.
|
|
74758
74944
|
5. NEVER invent an email address \u2014 always use exactly what \`inbox_provision_identity\` returns. The address is reused for this project; if signup says it is already registered, log in with it + \`${passwordKey}\` instead. Register ONCE per run \u2014 never submit signup twice or make a second account; if verification is slow, re-poll \`inbox_wait_for_verification\` (at most one Resend) instead of re-registering.
|
|
74945
|
+
${googleBlock}
|
|
74759
74946
|
`;
|
|
74760
74947
|
}
|
|
74761
74948
|
}
|
|
@@ -75739,7 +75926,7 @@ Rules:
|
|
|
75739
75926
|
- Register ONCE per run. If signup says the account ALREADY exists, log in with that email + the \`${opts.passwordKey}\` password instead of registering again.
|
|
75740
75927
|
- Only call the verification tools AFTER submitting the signup form; pass the app as \`senderMatch\` (the inbox is shared).`;
|
|
75741
75928
|
}
|
|
75742
|
-
function buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, appBrief, targetAppId, existingScreens, incremental) {
|
|
75929
|
+
function buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, appBrief, targetAppId, existingScreens, incremental, knowledgeContext) {
|
|
75743
75930
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
75744
75931
|
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
75745
75932
|
const surveyBody = `You are a mobile QA exploration agent driving a real ${platformLabel} app on a connected device. Your goal: MAP every reachable screen so downstream agents know what to test. You are NOT testing \u2014 you are building a complete screen map.
|
|
@@ -75784,12 +75971,18 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
|
75784
75971
|
## Application Brief
|
|
75785
75972
|
${appBrief}
|
|
75786
75973
|
Use this to prioritize which screens and flows to cover.` : "";
|
|
75974
|
+
const facts = knowledgeContext?.trim();
|
|
75975
|
+
const knowledgeBlock = facts ? `
|
|
75976
|
+
|
|
75977
|
+
## Known Facts (project documents \u2014 optional)
|
|
75978
|
+
${facts.slice(0, 4e3)}
|
|
75979
|
+
Prioritize exploring surfaces related to these facts when visible. If absent, explore solely from live UI. Never invent screens from facts alone.` : "";
|
|
75787
75980
|
const existingScreensBlock = mode === "survey" && incremental && existingScreens && existingScreens.length > 0 ? `
|
|
75788
75981
|
|
|
75789
75982
|
## ALREADY DISCOVERED SCREENS (skip these; find new)
|
|
75790
75983
|
These screens were mapped in a previous survey. Do NOT re-map them \u2014 use your budget to find NEW screens and flows not listed here. If you land on one of these, move on to something new.
|
|
75791
75984
|
${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.name}"` : ""}${s.screenType ? ` [${s.screenType}]` : ""}`).join("\n")}` : "";
|
|
75792
|
-
return `${mode === "survey" ? surveyBody : deepBody}${briefBlock}${existingScreensBlock}`;
|
|
75985
|
+
return `${mode === "survey" ? surveyBody : deepBody}${briefBlock}${knowledgeBlock}${existingScreensBlock}`;
|
|
75793
75986
|
}
|
|
75794
75987
|
function buildKickoffMessage(mode) {
|
|
75795
75988
|
return mode === "survey" ? "Start with mobile_snapshot on the launch screen. Driver observations are saved automatically; use capture_screen only to give important stable destinations useful names. Map the entire app: open every tab, menu, and one row per list. Record transitions as you go." : "Start by calling mobile_snapshot on the launch screen and capture_screen it. Then use the features: fill fields, submit forms, open menus, and observe outcomes. Capture each distinct screen and record journeys after meaningful flows.";
|
|
@@ -76189,7 +76382,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
76189
76382
|
if (authTracker.required) {
|
|
76190
76383
|
log2(`Authenticated survey completion gate enabled (${ctx.target?.resetPolicy ?? "AUTO"}): the run must reach a structurally observed product surface before planning.`);
|
|
76191
76384
|
}
|
|
76192
|
-
const systemPrompt = buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, ctx.appBrief, targetAppId, existingScreens, ctx.incremental) + (!registerRequested ? buildMobileCredentialPromptBlock(credentialSecrets) : "") + (inboxEnabled && inboxState ? buildMobileInboxPromptBlock({
|
|
76385
|
+
const systemPrompt = buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, ctx.appBrief, targetAppId, existingScreens, ctx.incremental, ctx.knowledgeContext) + (!registerRequested ? buildMobileCredentialPromptBlock(credentialSecrets) : "") + (inboxEnabled && inboxState ? buildMobileInboxPromptBlock({
|
|
76193
76386
|
email: inboxState.email,
|
|
76194
76387
|
passwordKey: inboxState.passwordKey ?? "TEST_USER_PASSWORD",
|
|
76195
76388
|
context: mode
|
|
@@ -80357,8 +80550,10 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80357
80550
|
exports2.inferScenarioKind = inferScenarioKind;
|
|
80358
80551
|
exports2.inferScenarioKinds = inferScenarioKinds;
|
|
80359
80552
|
exports2.isStrongOracleKind = isStrongOracleKind;
|
|
80553
|
+
exports2.kindRequiresStateDelta = kindRequiresStateDelta;
|
|
80360
80554
|
exports2.formatRequiredScenarioKindsPrompt = formatRequiredScenarioKindsPrompt;
|
|
80361
80555
|
exports2.formatCoverageGapLog = formatCoverageGapLog;
|
|
80556
|
+
exports2.synthesizeScenariosForMissingKinds = synthesizeScenariosForMissingKinds;
|
|
80362
80557
|
exports2.ALL_SCENARIO_KINDS = [
|
|
80363
80558
|
"smoke_nav",
|
|
80364
80559
|
"happy_submit",
|
|
@@ -80369,7 +80564,9 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80369
80564
|
"auth_session",
|
|
80370
80565
|
"navigation_integrity",
|
|
80371
80566
|
"permission_system",
|
|
80372
|
-
"double_submit"
|
|
80567
|
+
"double_submit",
|
|
80568
|
+
"search_filter",
|
|
80569
|
+
"list_refresh"
|
|
80373
80570
|
];
|
|
80374
80571
|
var DELETE_RE = /\b(delete|remove|archive|trash)\b/i;
|
|
80375
80572
|
var EMAIL_RE = /\b(e-?mail|email address)\b/i;
|
|
@@ -80381,7 +80578,12 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80381
80578
|
var BADGE_LABEL_RE = /\b(badge|count|open invoices|unread|notifications?)\b/i;
|
|
80382
80579
|
var PERMISSION_RE = /\b(camera|microphone|location|photos?|permission|allow access|not now)\b/i;
|
|
80383
80580
|
var SETTINGS_RE = /\b(settings|preferences|profile|account)\b/i;
|
|
80384
|
-
var TAB_RE = /\b(tab|home|
|
|
80581
|
+
var TAB_RE = /\b(tab|home|inbox|feed|dashboard)\b/i;
|
|
80582
|
+
var SEARCH_RE = /\b(search|filter|find|query)\b/i;
|
|
80583
|
+
var EMPTY_STATE_RE = /\b(no items|nothing here|empty|get started|add first|create first|no results)\b/i;
|
|
80584
|
+
var EMPTY_CTA_RE = /\b(add first|create first|get started|add item|new item)\b/i;
|
|
80585
|
+
var REFRESH_RE = /\b(pull to refresh|pull-to-refresh|refresh|updated:)\b/i;
|
|
80586
|
+
var FAVORITE_RE = /\b(favorite|favourite|heart|bookmark|star|like)\b/i;
|
|
80385
80587
|
function elementText(el) {
|
|
80386
80588
|
return [
|
|
80387
80589
|
el.label ?? "",
|
|
@@ -80419,6 +80621,10 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80419
80621
|
let hasPrimarySubmit = false;
|
|
80420
80622
|
let hasTabOrDrawerNav = false;
|
|
80421
80623
|
let hasPermissionCopy = false;
|
|
80624
|
+
let hasSearchField = false;
|
|
80625
|
+
let hasEmptyStateCta = false;
|
|
80626
|
+
let hasPullToRefresh = false;
|
|
80627
|
+
let hasFavoriteToggle = false;
|
|
80422
80628
|
for (const state2 of screens) {
|
|
80423
80629
|
const labels = state2.elements.map(elementText);
|
|
80424
80630
|
const blob = [state2.screenName ?? "", state2.screenType ?? "", ...state2.observations ?? [], ...labels].join(" ");
|
|
@@ -80450,6 +80656,17 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80450
80656
|
}
|
|
80451
80657
|
if (PERMISSION_RE.test(blob) || state2.screenType === "PERMISSION")
|
|
80452
80658
|
hasPermissionCopy = true;
|
|
80659
|
+
if (SEARCH_RE.test(blob) || state2.elements.some((el) => isEditable(el) && SEARCH_RE.test(elementText(el))) || state2.elements.some((el) => el.role === "searchbox" || /SearchView|SearchBar/i.test(el.role ?? ""))) {
|
|
80660
|
+
hasSearchField = true;
|
|
80661
|
+
}
|
|
80662
|
+
if (EMPTY_STATE_RE.test(blob) && (EMPTY_CTA_RE.test(blob) || labels.some((t) => EMPTY_CTA_RE.test(t)))) {
|
|
80663
|
+
hasEmptyStateCta = true;
|
|
80664
|
+
}
|
|
80665
|
+
if (REFRESH_RE.test(blob))
|
|
80666
|
+
hasPullToRefresh = true;
|
|
80667
|
+
if (FAVORITE_RE.test(blob) || state2.elements.some((el) => FAVORITE_RE.test(elementText(el)) && el.actionable !== false)) {
|
|
80668
|
+
hasFavoriteToggle = true;
|
|
80669
|
+
}
|
|
80453
80670
|
}
|
|
80454
80671
|
return {
|
|
80455
80672
|
hasListWithRowActions,
|
|
@@ -80461,7 +80678,11 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80461
80678
|
hasDestructiveConfirm,
|
|
80462
80679
|
hasPrimarySubmit,
|
|
80463
80680
|
hasTabOrDrawerNav,
|
|
80464
|
-
hasPermissionCopy
|
|
80681
|
+
hasPermissionCopy,
|
|
80682
|
+
hasSearchField,
|
|
80683
|
+
hasEmptyStateCta,
|
|
80684
|
+
hasPullToRefresh,
|
|
80685
|
+
hasFavoriteToggle
|
|
80465
80686
|
};
|
|
80466
80687
|
}
|
|
80467
80688
|
function requiredScenarioKinds(signals) {
|
|
@@ -80478,13 +80699,13 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80478
80699
|
if (signals.hasNumericBadge) {
|
|
80479
80700
|
kinds.add("count_delta");
|
|
80480
80701
|
}
|
|
80481
|
-
if (signals.hasSettingsToggles) {
|
|
80702
|
+
if (signals.hasSettingsToggles || signals.hasFavoriteToggle) {
|
|
80482
80703
|
kinds.add("persistence_roundtrip");
|
|
80483
80704
|
}
|
|
80484
80705
|
if (signals.hasSignOut) {
|
|
80485
80706
|
kinds.add("auth_session");
|
|
80486
80707
|
}
|
|
80487
|
-
if (signals.hasTabOrDrawerNav) {
|
|
80708
|
+
if (signals.hasTabOrDrawerNav || signals.hasEmptyStateCta) {
|
|
80488
80709
|
kinds.add("navigation_integrity");
|
|
80489
80710
|
}
|
|
80490
80711
|
if (signals.hasPermissionCopy) {
|
|
@@ -80493,6 +80714,12 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80493
80714
|
if (signals.hasPrimarySubmit && signals.hasFormFields) {
|
|
80494
80715
|
kinds.add("double_submit");
|
|
80495
80716
|
}
|
|
80717
|
+
if (signals.hasSearchField) {
|
|
80718
|
+
kinds.add("search_filter");
|
|
80719
|
+
}
|
|
80720
|
+
if (signals.hasPullToRefresh) {
|
|
80721
|
+
kinds.add("list_refresh");
|
|
80722
|
+
}
|
|
80496
80723
|
return exports2.ALL_SCENARIO_KINDS.filter((k) => kinds.has(k));
|
|
80497
80724
|
}
|
|
80498
80725
|
function coverageGaps(signals, plannedKinds) {
|
|
@@ -80507,6 +80734,12 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80507
80734
|
if (/\b(delete|remove|wrong row|entity|archive)\b/.test(text) && /\b(list|row|item|record)\b/.test(text)) {
|
|
80508
80735
|
return "crud_identity";
|
|
80509
80736
|
}
|
|
80737
|
+
if (/\b(pull.?to.?refresh|refresh list|list refresh|after refresh)\b/.test(text)) {
|
|
80738
|
+
return "list_refresh";
|
|
80739
|
+
}
|
|
80740
|
+
if (/\b(search|filter results|query)\b/.test(text)) {
|
|
80741
|
+
return "search_filter";
|
|
80742
|
+
}
|
|
80510
80743
|
if (/\b(badge|count|unread|open invoices|\+\d|increment|decrement)\b/.test(text)) {
|
|
80511
80744
|
return "count_delta";
|
|
80512
80745
|
}
|
|
@@ -80516,7 +80749,7 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80516
80749
|
if (/\b(sign\s*out|log\s*out|session|sign\s*in|remember me|auth)\b/.test(text) || scenario.type === "HAPPY_PATH" && /\b(login|sign in)\b/.test(text)) {
|
|
80517
80750
|
return "auth_session";
|
|
80518
80751
|
}
|
|
80519
|
-
if (/\b(tab|navigate|navigation|back|deep link|empty.?state)\b/.test(text)) {
|
|
80752
|
+
if (/\b(tab|navigate|navigation|back|deep link|empty.?state|empty cta|dead route)\b/.test(text)) {
|
|
80520
80753
|
return "navigation_integrity";
|
|
80521
80754
|
}
|
|
80522
80755
|
if (/\b(permission|camera|microphone|location|allow)\b/.test(text)) {
|
|
@@ -80539,6 +80772,9 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80539
80772
|
function isStrongOracleKind(kind) {
|
|
80540
80773
|
return kind !== "smoke_nav" && kind !== "navigation_integrity";
|
|
80541
80774
|
}
|
|
80775
|
+
function kindRequiresStateDelta(kind) {
|
|
80776
|
+
return kind === "crud_identity" || kind === "count_delta" || kind === "persistence_roundtrip" || kind === "double_submit" || kind === "list_refresh" || kind === "search_filter";
|
|
80777
|
+
}
|
|
80542
80778
|
function formatRequiredScenarioKindsPrompt(signals) {
|
|
80543
80779
|
const required = requiredScenarioKinds(signals);
|
|
80544
80780
|
if (required.length === 0)
|
|
@@ -80565,17 +80801,280 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80565
80801
|
return "- permission_system: permission prompt or settings path behaves honestly (allow/deny/settings)";
|
|
80566
80802
|
case "double_submit":
|
|
80567
80803
|
return "- double_submit: single user intent must not double-create (assert count +1 not +2, or single confirmation)";
|
|
80804
|
+
case "search_filter":
|
|
80805
|
+
return "- search_filter: type a distinctive query observed on-device, assert the result set filters (row count/labels change); empty query restores or shows empty-state honestly";
|
|
80806
|
+
case "list_refresh":
|
|
80807
|
+
return "- list_refresh: capture list ids/count, pull-to-refresh or tap Refresh, assert list still coherent (not permanently wiped) OR honest empty + recoverable";
|
|
80568
80808
|
default:
|
|
80569
80809
|
return `- ${kind}`;
|
|
80570
80810
|
}
|
|
80571
80811
|
});
|
|
80572
|
-
return "## Required scenario kinds (from observed UI \u2014 fill each or explain impossibility in reasoning)\nThese kinds are derived ONLY from controls/screens the explorer saw. Plan at least one scenario per kind when evidence supports it.\n" + lines.join("\n");
|
|
80812
|
+
return "## Required scenario kinds (from observed UI \u2014 fill each or explain impossibility in reasoning)\nThese kinds are derived ONLY from controls/screens the explorer saw. Plan at least one scenario per kind when evidence supports it.\nCap pure smoke_nav to ONE scenario per feature. Prefer strong kinds when signals require them.\nAuth credential validation (empty/wrong/malformed login) belongs ONLY on Authentication/Sign-in features \u2014 elsewhere login is setup, not a scenario.\nIf a scenario name claims delete/count/persist/refresh, expectedOutcomes MUST require before/after state proof (capture + delta), not tab chrome.\n" + lines.join("\n");
|
|
80573
80813
|
}
|
|
80574
80814
|
function formatCoverageGapLog(gaps) {
|
|
80575
80815
|
if (gaps.length === 0)
|
|
80576
80816
|
return "Scenario taxonomy: all required kinds covered by plan";
|
|
80577
80817
|
return `Scenario taxonomy coverage gaps (required but not planned): ${gaps.join(", ")}`;
|
|
80578
80818
|
}
|
|
80819
|
+
function synthesizeScenariosForMissingKinds(input) {
|
|
80820
|
+
const feature = input.featureName.trim() || "Feature";
|
|
80821
|
+
const used = input.existingNames ?? /* @__PURE__ */ new Set();
|
|
80822
|
+
const maxNew = Math.max(1, input.maxNew ?? 6);
|
|
80823
|
+
const pages = input.targetPages.length > 0 ? input.targetPages : [input.entryRoute];
|
|
80824
|
+
const entry = input.entryRoute || pages[0] || "";
|
|
80825
|
+
const out = [];
|
|
80826
|
+
for (const kind of input.gaps) {
|
|
80827
|
+
if (out.length >= maxNew)
|
|
80828
|
+
break;
|
|
80829
|
+
if (kind === "smoke_nav")
|
|
80830
|
+
continue;
|
|
80831
|
+
const draft = scenarioStubForKind(kind, feature, entry, pages);
|
|
80832
|
+
if (!draft)
|
|
80833
|
+
continue;
|
|
80834
|
+
let name = draft.name;
|
|
80835
|
+
let n = 2;
|
|
80836
|
+
while (used.has(name.toLowerCase())) {
|
|
80837
|
+
name = `${draft.name} (${n})`;
|
|
80838
|
+
n += 1;
|
|
80839
|
+
if (n > 20)
|
|
80840
|
+
break;
|
|
80841
|
+
}
|
|
80842
|
+
if (used.has(name.toLowerCase()))
|
|
80843
|
+
continue;
|
|
80844
|
+
used.add(name.toLowerCase());
|
|
80845
|
+
out.push({ ...draft, name });
|
|
80846
|
+
}
|
|
80847
|
+
return out;
|
|
80848
|
+
}
|
|
80849
|
+
function scenarioStubForKind(kind, feature, entryRoute, targetPages) {
|
|
80850
|
+
const base2 = {
|
|
80851
|
+
featureGroup: feature,
|
|
80852
|
+
entryRoute,
|
|
80853
|
+
startFromLanding: true,
|
|
80854
|
+
targetPages: [...targetPages],
|
|
80855
|
+
evidence: {
|
|
80856
|
+
observedLocators: [],
|
|
80857
|
+
observedTransitions: []
|
|
80858
|
+
},
|
|
80859
|
+
prerequisites: [],
|
|
80860
|
+
isAISuggested: false
|
|
80861
|
+
};
|
|
80862
|
+
const step = (action, target) => ({ action, target });
|
|
80863
|
+
const outcome = (type, description) => ({ type, description, reliability: "medium" });
|
|
80864
|
+
switch (kind) {
|
|
80865
|
+
case "crud_identity":
|
|
80866
|
+
return {
|
|
80867
|
+
...base2,
|
|
80868
|
+
name: `${feature} - Delete entity proves identity removed`,
|
|
80869
|
+
type: "HAPPY_PATH",
|
|
80870
|
+
priority: 1,
|
|
80871
|
+
variant: "happy",
|
|
80872
|
+
goal: "Capture list/entity identity, delete or remove that entity, prove the identity set changed (not merely that the list screen is still visible)",
|
|
80873
|
+
steps: [
|
|
80874
|
+
step("Launch the app and sign in if the product requires a session", entryRoute),
|
|
80875
|
+
step(`Navigate to the ${feature} list or collection screen`),
|
|
80876
|
+
step("Capture visible row/entity identifiers into a state bag before mutation"),
|
|
80877
|
+
step("Perform delete/remove/archive on one specific observed entity"),
|
|
80878
|
+
step("Assert state delta: that entity identity is gone or the wrong neighbor was not removed")
|
|
80879
|
+
],
|
|
80880
|
+
expectedOutcomes: [
|
|
80881
|
+
outcome("element-hidden", "Target entity identity no longer present in the list"),
|
|
80882
|
+
outcome("element-visible", "List or collection still reachable after mutation")
|
|
80883
|
+
]
|
|
80884
|
+
};
|
|
80885
|
+
case "count_delta":
|
|
80886
|
+
return {
|
|
80887
|
+
...base2,
|
|
80888
|
+
name: `${feature} - Count or badge changes after action`,
|
|
80889
|
+
type: "HAPPY_PATH",
|
|
80890
|
+
priority: 1,
|
|
80891
|
+
variant: "happy",
|
|
80892
|
+
goal: "Capture a numeric badge or count, perform a product action, assert the number changed as expected",
|
|
80893
|
+
steps: [
|
|
80894
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80895
|
+
step(`Navigate to the ${feature} surface showing a count or badge`),
|
|
80896
|
+
step("Capture the numeric count/badge into a state bag"),
|
|
80897
|
+
step("Perform an action that should change the count (add, remove, mark read, complete)"),
|
|
80898
|
+
step("Assert state delta: count increased or decreased as expected")
|
|
80899
|
+
],
|
|
80900
|
+
expectedOutcomes: [
|
|
80901
|
+
outcome("element-visible", "Count or badge reflects the post-action value")
|
|
80902
|
+
]
|
|
80903
|
+
};
|
|
80904
|
+
case "persistence_roundtrip":
|
|
80905
|
+
return {
|
|
80906
|
+
...base2,
|
|
80907
|
+
name: `${feature} - Change persists after leave and return`,
|
|
80908
|
+
type: "HAPPY_PATH",
|
|
80909
|
+
priority: 1,
|
|
80910
|
+
variant: "happy",
|
|
80911
|
+
goal: "Change a setting, toggle, or favorite; leave the screen; return; prove the value persisted or honestly reset",
|
|
80912
|
+
steps: [
|
|
80913
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80914
|
+
step(`Navigate to the ${feature} settings or toggle surface`),
|
|
80915
|
+
step("Capture current field/toggle/favorite state"),
|
|
80916
|
+
step("Change a durable control (toggle, text field, favorite) and save if required"),
|
|
80917
|
+
step("Navigate away to another tab or parent screen"),
|
|
80918
|
+
step("Return to the same surface"),
|
|
80919
|
+
step("Assert state delta: value persisted (or product honestly shows it did not)")
|
|
80920
|
+
],
|
|
80921
|
+
expectedOutcomes: [
|
|
80922
|
+
outcome("element-visible", "Changed value is still reflected after round-trip")
|
|
80923
|
+
]
|
|
80924
|
+
};
|
|
80925
|
+
case "negative_validation":
|
|
80926
|
+
return {
|
|
80927
|
+
...base2,
|
|
80928
|
+
name: `${feature} - Invalid or empty input is rejected`,
|
|
80929
|
+
type: "EDGE_CASE",
|
|
80930
|
+
priority: 2,
|
|
80931
|
+
variant: "validation",
|
|
80932
|
+
goal: "Submit empty or malformed input and assert error feedback or blocked submit \u2014 never invent success",
|
|
80933
|
+
steps: [
|
|
80934
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80935
|
+
step(`Navigate to the ${feature} form`),
|
|
80936
|
+
step("Leave required fields empty or type structurally invalid input (e.g. email-like field without a domain)"),
|
|
80937
|
+
step("Attempt submit"),
|
|
80938
|
+
step("Assert validation error is visible OR submit remains blocked on the form")
|
|
80939
|
+
],
|
|
80940
|
+
expectedOutcomes: [
|
|
80941
|
+
outcome("element-visible", "Validation error or blocked submit state is visible")
|
|
80942
|
+
]
|
|
80943
|
+
};
|
|
80944
|
+
case "happy_submit":
|
|
80945
|
+
return {
|
|
80946
|
+
...base2,
|
|
80947
|
+
name: `${feature} - Happy path durable submit`,
|
|
80948
|
+
type: "HAPPY_PATH",
|
|
80949
|
+
priority: 1,
|
|
80950
|
+
variant: "happy",
|
|
80951
|
+
goal: "Complete the primary form/action and assert a durable product result (new row, saved state, confirmation that outlives a toast)",
|
|
80952
|
+
steps: [
|
|
80953
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80954
|
+
step(`Navigate to the ${feature} form or primary action`),
|
|
80955
|
+
step("Fill required fields with run-scoped values"),
|
|
80956
|
+
step("Submit or save"),
|
|
80957
|
+
step("Assert durable outcome (new row, success state, or persisted values)")
|
|
80958
|
+
],
|
|
80959
|
+
expectedOutcomes: [
|
|
80960
|
+
outcome("element-visible", "Durable success result is visible")
|
|
80961
|
+
]
|
|
80962
|
+
};
|
|
80963
|
+
case "auth_session":
|
|
80964
|
+
return {
|
|
80965
|
+
...base2,
|
|
80966
|
+
name: `${feature} - Session proof after sign-in or sign-out`,
|
|
80967
|
+
type: "HAPPY_PATH",
|
|
80968
|
+
priority: 1,
|
|
80969
|
+
variant: "happy",
|
|
80970
|
+
goal: "Prove authenticated session (profile/sign-out) or that sign-out leaves only logged-out chrome",
|
|
80971
|
+
steps: [
|
|
80972
|
+
step("Launch the app", entryRoute),
|
|
80973
|
+
step("Sign in with test credentials if not already authenticated"),
|
|
80974
|
+
step("Open account/profile or use a signed-in-only control"),
|
|
80975
|
+
step("Assert Sign out / profile identity is available, or complete sign-out and assert login chrome")
|
|
80976
|
+
],
|
|
80977
|
+
expectedOutcomes: [
|
|
80978
|
+
outcome("element-visible", "Signed-in or signed-out durable chrome matches the action")
|
|
80979
|
+
]
|
|
80980
|
+
};
|
|
80981
|
+
case "navigation_integrity":
|
|
80982
|
+
return {
|
|
80983
|
+
...base2,
|
|
80984
|
+
name: `${feature} - Navigation reaches intended destination`,
|
|
80985
|
+
type: "USER_JOURNEY",
|
|
80986
|
+
priority: 2,
|
|
80987
|
+
variant: "happy",
|
|
80988
|
+
goal: "Follow in-app navigation (tab, back, empty-state CTA, overflow) and assert the intended product destination \u2014 not hub chrome after wandering",
|
|
80989
|
+
steps: [
|
|
80990
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80991
|
+
step(`Navigate into ${feature} via the observed entry path`),
|
|
80992
|
+
step("If an empty-state CTA is present, tap it and assert the destination form/error"),
|
|
80993
|
+
step("Use back/up if nested and assert the parent screen family")
|
|
80994
|
+
],
|
|
80995
|
+
expectedOutcomes: [
|
|
80996
|
+
outcome("screen-visible", "Intended destination screen family is visible")
|
|
80997
|
+
]
|
|
80998
|
+
};
|
|
80999
|
+
case "permission_system":
|
|
81000
|
+
return {
|
|
81001
|
+
...base2,
|
|
81002
|
+
name: `${feature} - Permission prompt behaves honestly`,
|
|
81003
|
+
type: "EDGE_CASE",
|
|
81004
|
+
priority: 2,
|
|
81005
|
+
variant: "happy",
|
|
81006
|
+
goal: "Trigger permission flow and assert allow/deny/settings path is honest",
|
|
81007
|
+
steps: [
|
|
81008
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
81009
|
+
step(`Navigate to the ${feature} permission-related surface`),
|
|
81010
|
+
step("Trigger the permission request"),
|
|
81011
|
+
step("Assert system or in-app permission UI appears and deny/allow path is reachable")
|
|
81012
|
+
],
|
|
81013
|
+
expectedOutcomes: [
|
|
81014
|
+
outcome("element-visible", "Permission or settings path is visible")
|
|
81015
|
+
]
|
|
81016
|
+
};
|
|
81017
|
+
case "double_submit":
|
|
81018
|
+
return {
|
|
81019
|
+
...base2,
|
|
81020
|
+
name: `${feature} - Single submit does not double-create`,
|
|
81021
|
+
type: "EDGE_CASE",
|
|
81022
|
+
priority: 2,
|
|
81023
|
+
variant: "bug",
|
|
81024
|
+
goal: "Submit once and assert count +1 (not +2) or a single confirmation",
|
|
81025
|
+
steps: [
|
|
81026
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
81027
|
+
step(`Navigate to the ${feature} submit flow`),
|
|
81028
|
+
step("Capture pre-submit count or confirmation state"),
|
|
81029
|
+
step("Tap primary submit only once"),
|
|
81030
|
+
step("Assert state delta: single creation (count +1 or one confirmation id)")
|
|
81031
|
+
],
|
|
81032
|
+
expectedOutcomes: [
|
|
81033
|
+
outcome("element-visible", "Exactly one durable result from a single submit")
|
|
81034
|
+
]
|
|
81035
|
+
};
|
|
81036
|
+
case "search_filter":
|
|
81037
|
+
return {
|
|
81038
|
+
...base2,
|
|
81039
|
+
name: `${feature} - Search filters the visible result set`,
|
|
81040
|
+
type: "HAPPY_PATH",
|
|
81041
|
+
priority: 1,
|
|
81042
|
+
variant: "happy",
|
|
81043
|
+
goal: "Type a distinctive query and assert the result set filters; clear/empty query restores or shows honest empty state",
|
|
81044
|
+
steps: [
|
|
81045
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
81046
|
+
step(`Navigate to the ${feature} search/filter surface`),
|
|
81047
|
+
step("Capture the unfiltered list identifiers"),
|
|
81048
|
+
step("Type a distinctive query into the search field"),
|
|
81049
|
+
step("Assert state delta: visible results changed (filtered or empty-state)")
|
|
81050
|
+
],
|
|
81051
|
+
expectedOutcomes: [
|
|
81052
|
+
outcome("element-visible", "Filtered results or empty-state copy matches the query")
|
|
81053
|
+
]
|
|
81054
|
+
};
|
|
81055
|
+
case "list_refresh":
|
|
81056
|
+
return {
|
|
81057
|
+
...base2,
|
|
81058
|
+
name: `${feature} - Pull to refresh keeps list coherent`,
|
|
81059
|
+
type: "HAPPY_PATH",
|
|
81060
|
+
priority: 1,
|
|
81061
|
+
variant: "happy",
|
|
81062
|
+
goal: "Capture list state, pull-to-refresh or tap Refresh, assert list remains coherent (not permanently wiped) or honest empty",
|
|
81063
|
+
steps: [
|
|
81064
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
81065
|
+
step(`Navigate to the ${feature} list with refresh`),
|
|
81066
|
+
step("Capture list identifiers before refresh"),
|
|
81067
|
+
step("Pull to refresh or activate Refresh"),
|
|
81068
|
+
step("Assert state delta: list still has coherent content or an honest empty state")
|
|
81069
|
+
],
|
|
81070
|
+
expectedOutcomes: [
|
|
81071
|
+
outcome("element-visible", "List after refresh is coherent (not permanently destroyed without recovery)")
|
|
81072
|
+
]
|
|
81073
|
+
};
|
|
81074
|
+
default:
|
|
81075
|
+
return null;
|
|
81076
|
+
}
|
|
81077
|
+
}
|
|
80579
81078
|
}
|
|
80580
81079
|
});
|
|
80581
81080
|
|
|
@@ -80664,10 +81163,15 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
80664
81163
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
80665
81164
|
var scenario_taxonomy_js_1 = require_scenario_taxonomy();
|
|
80666
81165
|
var coverage_map_js_1 = require_coverage_map();
|
|
81166
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
80667
81167
|
function isTargetAppScreenId(screenId, appId) {
|
|
80668
81168
|
if (!appId)
|
|
80669
81169
|
return true;
|
|
80670
|
-
|
|
81170
|
+
const hashIndex = screenId.indexOf("#");
|
|
81171
|
+
const owner = (hashIndex >= 0 ? screenId.slice(0, hashIndex) : screenId).trim();
|
|
81172
|
+
if (!owner)
|
|
81173
|
+
return false;
|
|
81174
|
+
return (0, shared_1.mobileScreenOwnerBelongsToApp)(owner, appId);
|
|
80671
81175
|
}
|
|
80672
81176
|
function targetAppScreens(graph, appId) {
|
|
80673
81177
|
return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
|
|
@@ -80809,12 +81313,19 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
80809
81313
|
Citing observed labels / accessibility-ids in \`expectedOutcomes\` (per Rule 12) is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
80810
81314
|
|
|
80811
81315
|
## Coverage Strategy
|
|
80812
|
-
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 12) + 1 EDGE_CASE (empty/invalid submission)
|
|
80813
|
-
- For auth flows: sign-in happy path (verify the target screen AND a concrete signed-in/session signal per rule 13) + validation (wrong password, empty fields)
|
|
81316
|
+
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 12) + 1 EDGE_CASE (empty/invalid submission). When email-like fields exist, include ONE weak/malformed value case (structurally invalid input such as missing domain) and assert reject OR honest accept \u2014 never invent error copy not seen in evidence.
|
|
81317
|
+
- For auth flows: sign-in happy path (verify the target screen AND a concrete signed-in/session signal per rule 13) + validation (wrong password, empty fields) **only when this deep feature is Authentication / Sign-in / Onboarding / Account**. For every other feature, credentials are setup steps inside product scenarios \u2014 do NOT emit standalone "Sign In - Empty Credentials" / "Wrong Password" scenarios under Home, Inbox, Settings, etc.
|
|
80814
81318
|
- For credential sign-in flows: only plan credential scenarios when evidence shows the credential path itself \u2014 an email/username field, a password field, and a native Sign In/Login/Submit control on the target screen, or an Explorer-Recorded Journey that typed those fields and submitted them. Do NOT treat Google/Apple/Facebook/SSO buttons, magic-link entry, logout menus, profile menus, or already-signed-in screens as evidence for credential sign-in. If the explorer only observed signed-in menu/logout/session surfaces, plan session/logout scenarios only; surface missing credential coverage as a gap instead of inventing credential sign-in variants.
|
|
80815
|
-
- For CRUD: each scenario MUST own the entity it operates on. Read/search/update/delete/cancel-on-existing/view-existing scenarios MUST include a create step earlier in the same scenario (or declare an immutable seed fixture by name). Never assume a record from prior runs or sibling tests exists. When create/delete are tightly coupled, bundle into a single USER_JOURNEY: create \u2192 verify \u2192 delete \u2192 verify removal.
|
|
81319
|
+
- For CRUD: each scenario MUST own the entity it operates on. Read/search/update/delete/cancel-on-existing/view-existing scenarios MUST include a create step earlier in the same scenario (or declare an immutable seed fixture by name). Never assume a record from prior runs or sibling tests exists. When create/delete are tightly coupled, bundle into a single USER_JOURNEY: create \u2192 verify \u2192 delete \u2192 verify removal. Delete scenarios MUST capture entity identity BEFORE delete and assert that identity is gone or the wrong neighbor remains (list chrome still visible is NOT enough).
|
|
81320
|
+
- For list counts / badges: plan before/after count or badge checks when those numbers were observed \u2014 not "Navigate up is visible".
|
|
81321
|
+
- For search/filter fields: plan a filter scenario with a distinctive query and assert the visible result set changes.
|
|
81322
|
+
- For pull-to-refresh / Refresh: capture list state, refresh, assert list remains coherent (not permanently wiped) or honest empty.
|
|
81323
|
+
- For empty-state CTAs ("Add first\u2026", "Create first\u2026"): tap the CTA and assert the DESTINATION product surface (form, detail, or honest error) \u2014 not hub tab chrome after wandering.
|
|
81324
|
+
- For settings/toggles/favorites: change value, leave screen (navigate away), return, assert value persisted or honestly reset.
|
|
80816
81325
|
- For AI/async features (Generate, Analyze, Process, Import, Sync): happy path MUST complete the flow per rule 12 - never stop at "button tapped" or "sheet opened"
|
|
80817
81326
|
- For navigation: multi-screen user journeys within the feature scope (do NOT drift into unrelated features)
|
|
81327
|
+
- **Smoke budget:** at most ONE pure smoke/navigation-only scenario per feature. Prefer required strong kinds when UI signals support them.
|
|
81328
|
+
- **Name = proof:** if the scenario name claims delete/count/persist/refresh/empty-CTA destination, expectedOutcomes and steps must prove that claim. Never name a test after a failure mode the steps do not exercise.
|
|
80818
81329
|
|
|
80819
81330
|
## Output JSON Schema
|
|
80820
81331
|
{
|
|
@@ -81105,6 +81616,13 @@ Each entry is exactly one of:
|
|
|
81105
81616
|
if (context.appBrief) {
|
|
81106
81617
|
sections.push(`## Application Brief
|
|
81107
81618
|
${context.appBrief.slice(0, 500)}`);
|
|
81619
|
+
}
|
|
81620
|
+
const knownFacts = context.knowledgeContext?.trim();
|
|
81621
|
+
if (knownFacts) {
|
|
81622
|
+
sections.push(`## Known Facts
|
|
81623
|
+
${knownFacts.slice(0, 8e3)}
|
|
81624
|
+
|
|
81625
|
+
These facts come from project documents / knowledge. Prefer scenarios that verify them when the explorer observed related UI. If a fact conflicts with live UI evidence, prefer live evidence. When this section is absent, rely only on explorer evidence.`);
|
|
81108
81626
|
}
|
|
81109
81627
|
const targetBits = [];
|
|
81110
81628
|
if (context.platform)
|
|
@@ -81543,14 +82061,46 @@ ${constraints.join("\n")}`);
|
|
|
81543
82061
|
}
|
|
81544
82062
|
return normalized;
|
|
81545
82063
|
}
|
|
82064
|
+
function buildMobileTaxonomyScenarios(input, gaps) {
|
|
82065
|
+
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
82066
|
+
if (orderedScreens.length === 0)
|
|
82067
|
+
return [];
|
|
82068
|
+
const entryScreenId = orderedScreens[0].screenId;
|
|
82069
|
+
const targetPages = orderedScreens.slice(0, 8).map((s) => s.screenId);
|
|
82070
|
+
const featureName = input.featureName?.trim() || nonGenericScreenName(orderedScreens[0], input.appId) || inferMobileFeatureGroupName(orderedScreens.slice(0, 3), input.appId) || "App";
|
|
82071
|
+
const signals = (0, scenario_taxonomy_js_1.extractUiSignals)(input.graph, input.appId);
|
|
82072
|
+
const missing = gaps && gaps.length > 0 ? gaps : (0, scenario_taxonomy_js_1.requiredScenarioKinds)(signals).filter((k) => k !== "smoke_nav");
|
|
82073
|
+
if (missing.length === 0) {
|
|
82074
|
+
return (0, scenario_taxonomy_js_1.synthesizeScenariosForMissingKinds)({
|
|
82075
|
+
gaps: ["navigation_integrity", "happy_submit"],
|
|
82076
|
+
featureName,
|
|
82077
|
+
entryRoute: entryScreenId,
|
|
82078
|
+
targetPages,
|
|
82079
|
+
existingNames: new Set((input.existingTestNames ?? []).map((n) => n.toLowerCase())),
|
|
82080
|
+
maxNew: Math.min(3, Math.max(1, input.testBudget || 3))
|
|
82081
|
+
});
|
|
82082
|
+
}
|
|
82083
|
+
return (0, scenario_taxonomy_js_1.synthesizeScenariosForMissingKinds)({
|
|
82084
|
+
gaps: missing,
|
|
82085
|
+
featureName,
|
|
82086
|
+
entryRoute: entryScreenId,
|
|
82087
|
+
targetPages,
|
|
82088
|
+
existingNames: new Set((input.existingTestNames ?? []).map((n) => n.toLowerCase())),
|
|
82089
|
+
maxNew: Math.min(6, Math.max(2, input.testBudget || 6))
|
|
82090
|
+
});
|
|
82091
|
+
}
|
|
81546
82092
|
function buildMobileFallbackScenarios(input) {
|
|
81547
82093
|
if (input.mode === "survey")
|
|
81548
82094
|
return [];
|
|
81549
|
-
if (input.featureName?.trim())
|
|
81550
|
-
return
|
|
82095
|
+
if (input.featureName?.trim()) {
|
|
82096
|
+
return buildMobileTaxonomyScenarios(input);
|
|
82097
|
+
}
|
|
81551
82098
|
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
81552
82099
|
if (orderedScreens.length === 0)
|
|
81553
82100
|
return [];
|
|
82101
|
+
const taxonomy = buildMobileTaxonomyScenarios(input);
|
|
82102
|
+
if (taxonomy.length > 0)
|
|
82103
|
+
return taxonomy;
|
|
81554
82104
|
const entryScreenId = orderedScreens[0].screenId;
|
|
81555
82105
|
const usedNames = new Set((input.existingTestNames ?? []).map((name) => name.toLowerCase()));
|
|
81556
82106
|
const scenarios = [];
|
|
@@ -81843,6 +82393,7 @@ ${constraints.join("\n")}`);
|
|
|
81843
82393
|
userPromptOverride: formatMobileEvidenceForPlanner(input.graph, {
|
|
81844
82394
|
mode: input.mode,
|
|
81845
82395
|
appBrief: input.appBrief,
|
|
82396
|
+
knowledgeContext: input.knowledgeContext,
|
|
81846
82397
|
appId: input.appId,
|
|
81847
82398
|
platform: input.platform,
|
|
81848
82399
|
featureName: input.featureName,
|
|
@@ -81875,44 +82426,65 @@ ${constraints.join("\n")}`);
|
|
|
81875
82426
|
if (guestWallFilter.dropped > 0) {
|
|
81876
82427
|
mobileAdaptationLogs.push(`Dropped ${guestWallFilter.dropped} guest-wall scenario(s) \u2014 explore session was authenticated; re-exec would not see sign-in gates`);
|
|
81877
82428
|
}
|
|
81878
|
-
|
|
82429
|
+
let plannedScenarios = guestWallFilter.scenarios;
|
|
81879
82430
|
let taxonomyGapLogs = [];
|
|
81880
82431
|
let taxonomyCoverageGaps = result.coverageGaps ?? [];
|
|
81881
|
-
if (input.mode !== "survey" &&
|
|
82432
|
+
if (input.mode !== "survey" && input.graph.screens.size > 0) {
|
|
81882
82433
|
const signals = (0, scenario_taxonomy_js_1.extractUiSignals)(input.graph, input.appId);
|
|
81883
82434
|
const plannedKinds = (0, scenario_taxonomy_js_1.inferScenarioKinds)(plannedScenarios);
|
|
81884
|
-
|
|
81885
|
-
|
|
81886
|
-
|
|
81887
|
-
appId: input.appId,
|
|
81888
|
-
plannedScenarios
|
|
81889
|
-
});
|
|
81890
|
-
taxonomyGapLogs = [(0, scenario_taxonomy_js_1.formatCoverageGapLog)(gaps), coverageMap.summary];
|
|
81891
|
-
if (coverageMap.warnings.length > 0) {
|
|
81892
|
-
taxonomyGapLogs.push(...coverageMap.warnings.slice(0, 12));
|
|
82435
|
+
let gaps = (0, scenario_taxonomy_js_1.coverageGaps)(signals, plannedKinds);
|
|
82436
|
+
if (plannedScenarios.length === 0) {
|
|
82437
|
+
gaps = (0, scenario_taxonomy_js_1.requiredScenarioKinds)(signals).filter((k) => k !== "smoke_nav");
|
|
81893
82438
|
}
|
|
81894
82439
|
if (gaps.length > 0) {
|
|
81895
|
-
|
|
81896
|
-
|
|
81897
|
-
|
|
81898
|
-
...
|
|
81899
|
-
|
|
81900
|
-
|
|
81901
|
-
|
|
81902
|
-
|
|
81903
|
-
|
|
81904
|
-
|
|
81905
|
-
|
|
81906
|
-
|
|
81907
|
-
|
|
81908
|
-
|
|
81909
|
-
|
|
81910
|
-
|
|
81911
|
-
|
|
81912
|
-
|
|
81913
|
-
|
|
82440
|
+
const synthesized = buildMobileTaxonomyScenarios(input, gaps);
|
|
82441
|
+
if (synthesized.length > 0) {
|
|
82442
|
+
const before = plannedScenarios.length;
|
|
82443
|
+
plannedScenarios = [...plannedScenarios, ...synthesized];
|
|
82444
|
+
mobileAdaptationLogs.push(`Taxonomy synthesis: added ${synthesized.length} scenario(s) for missing kinds [${gaps.join(", ")}] (plan had ${before})`);
|
|
82445
|
+
gaps = (0, scenario_taxonomy_js_1.coverageGaps)(signals, (0, scenario_taxonomy_js_1.inferScenarioKinds)(plannedScenarios));
|
|
82446
|
+
}
|
|
82447
|
+
}
|
|
82448
|
+
if (plannedScenarios.length > 0) {
|
|
82449
|
+
const coverageMap = (0, coverage_map_js_1.buildMobileCoverageMap)({
|
|
82450
|
+
graph: input.graph,
|
|
82451
|
+
appId: input.appId,
|
|
82452
|
+
plannedScenarios
|
|
82453
|
+
});
|
|
82454
|
+
taxonomyGapLogs = [(0, scenario_taxonomy_js_1.formatCoverageGapLog)(gaps), coverageMap.summary];
|
|
82455
|
+
if (coverageMap.warnings.length > 0) {
|
|
82456
|
+
taxonomyGapLogs.push(...coverageMap.warnings.slice(0, 12));
|
|
82457
|
+
}
|
|
82458
|
+
if (gaps.length > 0 || coverageMap.warnings.length > 0) {
|
|
82459
|
+
taxonomyGapLogs.push(`UI signals: listRowActions=${signals.hasListWithRowActions} forms=${signals.hasFormFields} email=${signals.hasEmailField} badge=${signals.hasNumericBadge} toggles=${signals.hasSettingsToggles} signOut=${signals.hasSignOut} tabs=${signals.hasTabOrDrawerNav} search=${signals.hasSearchField} refresh=${signals.hasPullToRefresh} emptyCta=${signals.hasEmptyStateCta}`);
|
|
82460
|
+
const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
82461
|
+
taxonomyCoverageGaps = [
|
|
82462
|
+
...taxonomyCoverageGaps,
|
|
82463
|
+
...gaps.map((kind) => ({
|
|
82464
|
+
scenarioName: `[taxonomy] ${kind}`,
|
|
82465
|
+
reason: `Required scenario kind "${kind}" not represented in the planned set`,
|
|
82466
|
+
suggestion: `Add a ${kind} scenario backed by observed controls, or document impossibility`,
|
|
82467
|
+
unmetPrerequisites: [],
|
|
82468
|
+
detectedAt
|
|
82469
|
+
})),
|
|
82470
|
+
...coverageMap.warnings.filter((w) => /smoke-only|no strong-oracle/i.test(w)).map((w) => ({
|
|
82471
|
+
scenarioName: "[coverage-map] strong-oracle gap",
|
|
82472
|
+
reason: w,
|
|
82473
|
+
suggestion: "Author durable assertStateDelta/assertText outcomes for this kind",
|
|
82474
|
+
unmetPrerequisites: [],
|
|
82475
|
+
detectedAt
|
|
82476
|
+
}))
|
|
82477
|
+
];
|
|
82478
|
+
}
|
|
81914
82479
|
}
|
|
81915
82480
|
}
|
|
82481
|
+
if (input.mode !== "survey" && input.featureName?.trim()) {
|
|
82482
|
+
const deepFeature = input.featureName.trim();
|
|
82483
|
+
plannedScenarios = plannedScenarios.map((s) => ({
|
|
82484
|
+
...s,
|
|
82485
|
+
featureGroup: s.featureGroup?.trim() || deepFeature
|
|
82486
|
+
}));
|
|
82487
|
+
}
|
|
81916
82488
|
if (input.mode !== "survey" && plannedScenarios.length === 0 && input.graph.screens.size > 0) {
|
|
81917
82489
|
const fallbackScenarios = buildMobileFallbackScenarios(input);
|
|
81918
82490
|
if (fallbackScenarios.length > 0) {
|
|
@@ -81923,7 +82495,7 @@ ${constraints.join("\n")}`);
|
|
|
81923
82495
|
logs: [
|
|
81924
82496
|
...result.logs,
|
|
81925
82497
|
...mobileAdaptationLogs,
|
|
81926
|
-
`Mobile fallback: synthesized ${fallbackScenarios.length}
|
|
82498
|
+
`Mobile fallback: synthesized ${fallbackScenarios.length} taxonomy/bug-capture scenario(s) from native UI signals`
|
|
81927
82499
|
],
|
|
81928
82500
|
fallbackUsed: true,
|
|
81929
82501
|
fallbackReason: result.fallbackReason ?? "Mobile planner produced no scenarios"
|
|
@@ -81967,6 +82539,25 @@ ${constraints.join("\n")}`);
|
|
|
81967
82539
|
fallbackReason: result.fallbackReason ?? plannerGroupSelection.reason ?? `Planner omitted ${supplementedSurveyRouteCount} plannable mobile screen(s)`
|
|
81968
82540
|
};
|
|
81969
82541
|
}
|
|
82542
|
+
if (mobileFeatureGroups.length === 0 && input.graph.screens.size > 0) {
|
|
82543
|
+
const appId = input.appId?.trim();
|
|
82544
|
+
const owners = [...new Set([...input.graph.screens.values()].map((state2) => {
|
|
82545
|
+
const hashIndex = state2.screenId.indexOf("#");
|
|
82546
|
+
return (hashIndex >= 0 ? state2.screenId.slice(0, hashIndex) : state2.screenId).trim();
|
|
82547
|
+
}).filter(Boolean))];
|
|
82548
|
+
const inAppOwners = appId ? owners.filter((owner) => (0, shared_1.mobileScreenOwnerBelongsToApp)(owner, appId)) : owners;
|
|
82549
|
+
const reason = inAppOwners.length === 0 && appId ? `Mapped ${input.graph.screens.size} screen(s) but none were attributed to app "${appId}" (screen owners seen: ${owners.slice(0, 4).join(", ")}). No features could be created.` : `Mapped ${input.graph.screens.size} screen(s) but none could be grouped into a feature (too few actionable elements, or every screen was filtered as generic/system).`;
|
|
82550
|
+
return {
|
|
82551
|
+
...result,
|
|
82552
|
+
scenarios: plannedScenarios,
|
|
82553
|
+
featureGroups: mobileFeatureGroups,
|
|
82554
|
+
coverageGaps: taxonomyCoverageGaps.length > 0 ? taxonomyCoverageGaps : result.coverageGaps,
|
|
82555
|
+
logs: [...result.logs, ...mobileAdaptationLogs, ...taxonomyGapLogs, `\u26A0 ${reason}`],
|
|
82556
|
+
warning: result.warning ?? reason,
|
|
82557
|
+
fallbackUsed: true,
|
|
82558
|
+
fallbackReason: result.fallbackReason ?? reason
|
|
82559
|
+
};
|
|
82560
|
+
}
|
|
81970
82561
|
return {
|
|
81971
82562
|
...result,
|
|
81972
82563
|
scenarios: plannedScenarios,
|
|
@@ -82035,6 +82626,7 @@ var require_anr_env = __commonJS({
|
|
|
82035
82626
|
exports2.isAnrOrSystemUiEnvironmentFailure = isAnrOrSystemUiEnvironmentFailure;
|
|
82036
82627
|
exports2.stepLooksLikeAnrChromeAssert = stepLooksLikeAnrChromeAssert;
|
|
82037
82628
|
var ANR_TEXT_RE = /system\s*ui\s*isn['’]?t\s*responding|app\s*isn['’]?t\s*responding|application\s*not\s*responding|\banr\b|close\s*app|wait\s*and\s*see|isn['’]?t\s*responding/i;
|
|
82629
|
+
var SYSTEM_FREEZE_RE = /system\s*ui\s*isn['’]?t\s*responding|\bsystem\s*ui\b[^.\n]{0,40}\b(froze|frozen|hung|unresponsive|not\s*responding)\b|\b(device|emulator|simulator)\b[^.\n]{0,40}\b(froze|frozen|hung|unresponsive)\b/i;
|
|
82038
82630
|
function textLooksLikeAnrOrSystemUi(text) {
|
|
82039
82631
|
if (!text || typeof text !== "string")
|
|
82040
82632
|
return false;
|
|
@@ -82043,7 +82635,7 @@ var require_anr_env = __commonJS({
|
|
|
82043
82635
|
function isAnrOrSystemUiEnvironmentFailure(error2, logs2) {
|
|
82044
82636
|
const blob = `${error2 ?? ""}
|
|
82045
82637
|
${logs2 ?? ""}`;
|
|
82046
|
-
if (
|
|
82638
|
+
if (SYSTEM_FREEZE_RE.test(blob))
|
|
82047
82639
|
return true;
|
|
82048
82640
|
if (/\bcold-boot\b/i.test(blob) && /\b(froze|hang|anr|unresponsive)\b/i.test(blob))
|
|
82049
82641
|
return true;
|
|
@@ -82065,13 +82657,48 @@ var require_oracle_quality = __commonJS({
|
|
|
82065
82657
|
"../runner-core/dist/services/mobile/oracle-quality.js"(exports2) {
|
|
82066
82658
|
"use strict";
|
|
82067
82659
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
82660
|
+
exports2.isToastOrAckOutcomeText = isToastOrAckOutcomeText;
|
|
82661
|
+
exports2.isAuthPrimaryFeatureName = isAuthPrimaryFeatureName;
|
|
82662
|
+
exports2.isStandaloneAuthValidationScenario = isStandaloneAuthValidationScenario;
|
|
82663
|
+
exports2.textLooksLikeProductOutcome = textLooksLikeProductOutcome;
|
|
82664
|
+
exports2.textLooksLikeChromeSurface = textLooksLikeChromeSurface;
|
|
82068
82665
|
exports2.assessStrongOracleQuality = assessStrongOracleQuality;
|
|
82666
|
+
exports2.assessScenarioClaimCoherence = assessScenarioClaimCoherence;
|
|
82069
82667
|
var anr_env_js_1 = require_anr_env();
|
|
82668
|
+
var scenario_taxonomy_js_1 = require_scenario_taxonomy();
|
|
82070
82669
|
var CHROME_ONLY_RE = /\b(tab|home tab|catalog tab|forms tab|account tab|back|menu|navigation|nav bar|bottom nav|status bar|query|search field)\b/i;
|
|
82071
82670
|
var PRODUCT_OUTCOME_TEXT_RE = /\b(error|invalid|required|must|cannot|failed|denied|blocked|empty|missing|deleted|removed|saved|success|created|updated|signed?\s*out|logged?\s*out|wrong|incorrect)\b/i;
|
|
82671
|
+
var TOAST_ACK_EXACT_RE = /^(saved!?|success!?|successfully (saved|deleted|updated|created)!?|deleted!?|removed!?|done!?|ok!?|updated!?|created!?)$/i;
|
|
82672
|
+
function isToastOrAckOutcomeText(text) {
|
|
82673
|
+
const t = text.trim();
|
|
82674
|
+
if (!t)
|
|
82675
|
+
return false;
|
|
82676
|
+
if (TOAST_ACK_EXACT_RE.test(t))
|
|
82677
|
+
return true;
|
|
82678
|
+
const parts = t.split(/\s+/).filter(Boolean);
|
|
82679
|
+
return parts.length > 0 && parts.every((part) => TOAST_ACK_EXACT_RE.test(part));
|
|
82680
|
+
}
|
|
82681
|
+
function isAuthPrimaryFeatureName(name) {
|
|
82682
|
+
const n = (name ?? "").trim();
|
|
82683
|
+
if (!n)
|
|
82684
|
+
return false;
|
|
82685
|
+
return /\b(auth|authentication|sign[\s-]*in|log[\s-]*in|login|session|onboarding|account)\b/i.test(n);
|
|
82686
|
+
}
|
|
82687
|
+
function isStandaloneAuthValidationScenario(scenario) {
|
|
82688
|
+
const text = `${scenario.name ?? ""} ${scenario.goal ?? ""} ${scenario.variant ?? ""}`.toLowerCase();
|
|
82689
|
+
if (!/\b(sign\s*in|log\s*in|login|credentials?|password|email)\b/.test(text))
|
|
82690
|
+
return false;
|
|
82691
|
+
return /\b(empty|invalid|wrong password|malformed|required|validation|reject|blocked)\b/.test(text) || scenario.variant === "validation";
|
|
82692
|
+
}
|
|
82072
82693
|
function stepBlob(step) {
|
|
82073
82694
|
return [step.description, step.assertion, step.value, step.target?.primary].filter(Boolean).join(" ");
|
|
82074
82695
|
}
|
|
82696
|
+
function textLooksLikeProductOutcome(text) {
|
|
82697
|
+
return PRODUCT_OUTCOME_TEXT_RE.test(text);
|
|
82698
|
+
}
|
|
82699
|
+
function textLooksLikeChromeSurface(text) {
|
|
82700
|
+
return CHROME_ONLY_RE.test(text);
|
|
82701
|
+
}
|
|
82075
82702
|
function isChromeOnlyVisible(step) {
|
|
82076
82703
|
if (step.action !== "assertVisible")
|
|
82077
82704
|
return false;
|
|
@@ -82108,6 +82735,8 @@ var require_oracle_quality = __commonJS({
|
|
|
82108
82735
|
if (/^(email|password|username|name|query|search)$/i.test((s.assertion ?? "").trim())) {
|
|
82109
82736
|
return false;
|
|
82110
82737
|
}
|
|
82738
|
+
if (isToastOrAckOutcomeText(s.assertion ?? s.value ?? ""))
|
|
82739
|
+
return false;
|
|
82111
82740
|
return blob.length >= 3;
|
|
82112
82741
|
});
|
|
82113
82742
|
const hasStrongValidationVisible = steps.some((s) => {
|
|
@@ -82115,7 +82744,12 @@ var require_oracle_quality = __commonJS({
|
|
|
82115
82744
|
return false;
|
|
82116
82745
|
if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s) || isChromeOnlyVisible(s))
|
|
82117
82746
|
return false;
|
|
82118
|
-
|
|
82747
|
+
const blob = stepBlob(s);
|
|
82748
|
+
if (isToastOrAckOutcomeText(s.assertion ?? s.value ?? ""))
|
|
82749
|
+
return false;
|
|
82750
|
+
if (isToastOrAckOutcomeText(blob))
|
|
82751
|
+
return false;
|
|
82752
|
+
return PRODUCT_OUTCOME_TEXT_RE.test(blob);
|
|
82119
82753
|
});
|
|
82120
82754
|
const strong = hasStateDelta || hasStrongAssertText || hasStrongValidationVisible;
|
|
82121
82755
|
let reason;
|
|
@@ -82140,6 +82774,70 @@ var require_oracle_quality = __commonJS({
|
|
|
82140
82774
|
reason
|
|
82141
82775
|
};
|
|
82142
82776
|
}
|
|
82777
|
+
function assessScenarioClaimCoherence(scenario, steps) {
|
|
82778
|
+
const variant = scenario.variant === "happy" || scenario.variant === "validation" || scenario.variant === "bug" ? scenario.variant : void 0;
|
|
82779
|
+
const kind = (0, scenario_taxonomy_js_1.inferScenarioKind)({
|
|
82780
|
+
type: scenario.type || "HAPPY_PATH",
|
|
82781
|
+
variant,
|
|
82782
|
+
name: scenario.name ?? "",
|
|
82783
|
+
goal: scenario.goal ?? ""
|
|
82784
|
+
});
|
|
82785
|
+
const quality = assessStrongOracleQuality(steps);
|
|
82786
|
+
const text = `${scenario.name ?? ""} ${scenario.goal ?? ""}`.toLowerCase();
|
|
82787
|
+
const featureGroup = scenario.featureGroup ?? "";
|
|
82788
|
+
if (isStandaloneAuthValidationScenario(scenario) && featureGroup && !isAuthPrimaryFeatureName(featureGroup)) {
|
|
82789
|
+
return {
|
|
82790
|
+
coherent: false,
|
|
82791
|
+
kind,
|
|
82792
|
+
discard: true,
|
|
82793
|
+
reason: `auth credential validation scenario under non-auth feature "${featureGroup}" \u2014 login is setup only`
|
|
82794
|
+
};
|
|
82795
|
+
}
|
|
82796
|
+
if (quality.anrOnly) {
|
|
82797
|
+
return {
|
|
82798
|
+
coherent: false,
|
|
82799
|
+
kind,
|
|
82800
|
+
discard: true,
|
|
82801
|
+
reason: quality.reason
|
|
82802
|
+
};
|
|
82803
|
+
}
|
|
82804
|
+
if ((0, scenario_taxonomy_js_1.kindRequiresStateDelta)(kind) && !quality.hasStateDelta) {
|
|
82805
|
+
const namesStateClaim = /\b(count|delta|delete|removed|persist|roundtrip|wrong row|entity|double.?submit|fires?:?\s*2|refresh)\b/i.test(text);
|
|
82806
|
+
return {
|
|
82807
|
+
coherent: false,
|
|
82808
|
+
kind,
|
|
82809
|
+
// Discard when the name/kind clearly promised state proof; otherwise demote.
|
|
82810
|
+
discard: namesStateClaim || kind === "crud_identity" || kind === "count_delta" || kind === "list_refresh",
|
|
82811
|
+
reason: `scenario kind ${kind} claims durable state change but steps lack assertStateDelta (${quality.reason})`
|
|
82812
|
+
};
|
|
82813
|
+
}
|
|
82814
|
+
if (/\b(empty.?state|empty cta|dead route|404|not found|broken blank)\b/i.test(text)) {
|
|
82815
|
+
const hasProductDestination = steps.some((s) => {
|
|
82816
|
+
if (s.action !== "assertVisible" && s.action !== "assertText")
|
|
82817
|
+
return false;
|
|
82818
|
+
if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s))
|
|
82819
|
+
return false;
|
|
82820
|
+
const blob = stepBlob(s);
|
|
82821
|
+
if (CHROME_ONLY_RE.test(blob) && !PRODUCT_OUTCOME_TEXT_RE.test(blob))
|
|
82822
|
+
return false;
|
|
82823
|
+
return PRODUCT_OUTCOME_TEXT_RE.test(blob) || /\b(404|not found|empty|no items|blank|error)\b/i.test(blob);
|
|
82824
|
+
});
|
|
82825
|
+
if (!hasProductDestination && !quality.hasStateDelta) {
|
|
82826
|
+
return {
|
|
82827
|
+
coherent: false,
|
|
82828
|
+
kind,
|
|
82829
|
+
discard: false,
|
|
82830
|
+
reason: "empty-state/dead-route claim without destination or empty-state product assertion"
|
|
82831
|
+
};
|
|
82832
|
+
}
|
|
82833
|
+
}
|
|
82834
|
+
return {
|
|
82835
|
+
coherent: true,
|
|
82836
|
+
kind,
|
|
82837
|
+
discard: false,
|
|
82838
|
+
reason: "scenario claim matches step oracles"
|
|
82839
|
+
};
|
|
82840
|
+
}
|
|
82143
82841
|
}
|
|
82144
82842
|
});
|
|
82145
82843
|
|
|
@@ -82657,7 +83355,18 @@ ${hybridSkeleton.join("\n")}`;
|
|
|
82657
83355
|
function hasStableId(element) {
|
|
82658
83356
|
return !!nonEmpty3(element.accessibilityId) || !!nonEmpty3(element.resourceId);
|
|
82659
83357
|
}
|
|
82660
|
-
function
|
|
83358
|
+
function buildKnownFactsPromptBlock(knowledgeContext) {
|
|
83359
|
+
const text = knowledgeContext?.trim();
|
|
83360
|
+
if (!text)
|
|
83361
|
+
return "";
|
|
83362
|
+
return `
|
|
83363
|
+
|
|
83364
|
+
## Known Facts (project documents \u2014 optional)
|
|
83365
|
+
${text.slice(0, 6e3)}
|
|
83366
|
+
|
|
83367
|
+
When facts are present, prefer assertions that prove them if the live UI supports it. If a fact conflicts with what you see on device, prefer live UI. Never invent requirements not in these facts and not visible on screen. When this section is absent, rely only on the scenario + live UI.`;
|
|
83368
|
+
}
|
|
83369
|
+
function buildSystemPrompt(platform3, targetAppId, maxIterations = MAX_SCENARIO_ITERATIONS, knowledgeContext) {
|
|
82661
83370
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
82662
83371
|
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
82663
83372
|
return `You are an expert mobile QA agent generating ONE automated test by driving a real ${platformLabel} app on a connected device. The app has just been relaunched into a clean state.
|
|
@@ -82681,14 +83390,14 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
|
|
|
82681
83390
|
- For list delete/create or count changes: mobile_capture_state({stateKey, kind}) BEFORE the mutation, then mobile_assert_state_delta({stateKey, expect}) AFTER \u2014 BOTH are required. captureState alone does NOT prove the outcome.
|
|
82682
83391
|
- Negative validation: leave a required field EMPTY (or type clearly invalid email-like input), submit, assert the app BLOCKS success \u2014 error message / still on form / no navigation to Home. Do NOT fill a valid password-only path and call it "empty email".
|
|
82683
83392
|
- After sign-in \u2192 signed-in product signal; after like \u2192 liked/unlike control or durable like state; after open Settings \u2192 a settings-specific control.
|
|
82684
|
-
- Do NOT assert bottom-nav tab labels alone
|
|
83393
|
+
- Do NOT assert bottom-nav tab labels alone or notification badge counts ("N unread") as the scenario outcome.
|
|
82685
83394
|
- NEVER assert system freeze chrome ("System UI isn't responding", ANR dialogs, Wait/Close app). If the app freezes, finish_generate with verified:false (environment) \u2014 do NOT write a test whose only proof is the ANR dialog.
|
|
82686
83395
|
5. Call finish_generate with verified:true ONLY if you completed the goal AND asserted a matching durable outcome. If the app cannot complete the goal, finish with verified:false and explain \u2014 never invent a green path from unrelated screens.
|
|
82687
83396
|
|
|
82688
83397
|
## Rules
|
|
82689
83398
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
82690
83399
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
82691
|
-
- Assert only DURABLE
|
|
83400
|
+
- Assert only DURABLE product outcomes. Prefer entity identity, form values, validation copy, destination screens after navigation. For empty-state CTA scenarios: tap the CTA and assert the DESTINATION (form, detail, or honest error) \u2014 do NOT treat the CTA label alone as success, and do NOT use empty-state CTAs as proof for unrelated happy paths.
|
|
82692
83401
|
- Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages to others; and ESPECIALLY account/data deletion, deactivation, or any "type DELETE to confirm" gate (the runner hard-blocks destructive taps and confirmation keystrokes). Note them and assert what you safely can.
|
|
82693
83402
|
- STATE OWNERSHIP for mutation scenarios (edit / rename / archive / remove): CREATE the record first in this same recording, then act on the row YOU created \u2014 never mutate pre-existing data (it may not exist on a clean install, so the replay fails forever, and another test may depend on it). If the app offers no way to create one, do NOT act on someone else's data: assert what you can safely prove and finish with verified:false, explaining why in the description.
|
|
82694
83403
|
- If the APP itself misbehaves (product error screen, broken validation, wrong entity deleted), assert the product failure and call finish_generate with suspectedBug:true and a concrete bugDescription. If the device shows ANR / "System UI isn't responding" / freeze dialogs, that is ENVIRONMENT \u2014 finish with verified:false and do NOT assert the freeze dialog as the test outcome.
|
|
@@ -82701,7 +83410,7 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
|
|
|
82701
83410
|
- Each action must make NEW progress toward the goal. If an action leaves the screen unchanged, do NOT repeat it \u2014 pick a different element or move on.
|
|
82702
83411
|
- Finish promptly once the outcome is proven: the instant the expected outcome is visible, assert it and call finish_generate. Do not keep exploring after the goal is proven.
|
|
82703
83412
|
|
|
82704
|
-
Max iterations: ${maxIterations}. You will be warned near the end
|
|
83413
|
+
Max iterations: ${maxIterations}. You will be warned near the end.` + buildKnownFactsPromptBlock(knowledgeContext);
|
|
82705
83414
|
}
|
|
82706
83415
|
function buildGenerateCredentialPromptBlock(secrets) {
|
|
82707
83416
|
const keys = Object.keys(secrets).filter((k) => secrets[k] && secrets[k].length > 0);
|
|
@@ -82791,7 +83500,7 @@ Start by calling mobile_snapshot, then execute ONLY the steps needed for this go
|
|
|
82791
83500
|
}
|
|
82792
83501
|
}
|
|
82793
83502
|
async function runScenarioLoop(args) {
|
|
82794
|
-
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch } = args;
|
|
83503
|
+
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch, knowledgeContext } = args;
|
|
82795
83504
|
const log2 = (line) => emitLog(redactCredentialValues(line, redactionCredentials));
|
|
82796
83505
|
const actions = [];
|
|
82797
83506
|
const stateBag = /* @__PURE__ */ new Map();
|
|
@@ -83169,7 +83878,7 @@ ${lines.join("\n")}`
|
|
|
83169
83878
|
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
|
|
83170
83879
|
log2(` seed snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
83171
83880
|
}
|
|
83172
|
-
const systemPrompt = buildSystemPrompt(platform3, targetAppId, maxIterations) + buildGenerateCredentialPromptBlock(credentials);
|
|
83881
|
+
const systemPrompt = buildSystemPrompt(platform3, targetAppId, maxIterations, knowledgeContext) + buildGenerateCredentialPromptBlock(credentials);
|
|
83173
83882
|
const kickoff = buildScenarioKickoff(scenario);
|
|
83174
83883
|
const recentExchanges = [];
|
|
83175
83884
|
const hasStoredCredentials = Object.values(credentials).some((v) => typeof v === "string" && v.length > 0);
|
|
@@ -85118,6 +85827,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85118
85827
|
const t = text.trim();
|
|
85119
85828
|
if (!t)
|
|
85120
85829
|
return false;
|
|
85830
|
+
if ((0, oracle_quality_js_1.isToastOrAckOutcomeText)(t))
|
|
85831
|
+
return true;
|
|
85121
85832
|
if (/\b\d+\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|wk|wks|week|weeks|mo|mos|month|months|y|yr|yrs|year|years)\s+ago\b/i.test(t)) {
|
|
85122
85833
|
return true;
|
|
85123
85834
|
}
|
|
@@ -85223,6 +85934,30 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85223
85934
|
}
|
|
85224
85935
|
const ephemeral = modelAsserts.filter((a) => isEphemeralContentAssertText(assertIdentityText(a)));
|
|
85225
85936
|
const durable = modelAsserts.filter((a) => !isEphemeralContentAssertText(assertIdentityText(a)));
|
|
85937
|
+
const chromeOnlyVisible = (a) => {
|
|
85938
|
+
if (a.type !== "ASSERT_VISIBLE")
|
|
85939
|
+
return false;
|
|
85940
|
+
const blob = assertIdentityText(a);
|
|
85941
|
+
if ((0, oracle_quality_js_1.isToastOrAckOutcomeText)(blob))
|
|
85942
|
+
return true;
|
|
85943
|
+
return /\b(tab|home|settings|back|menu|navigation|nav bar|bottom nav|status bar)\b/i.test(blob) && !/\b(error|invalid|required|failed|deleted|removed|created)\b/i.test(blob);
|
|
85944
|
+
};
|
|
85945
|
+
if (durable.length > 0 && durable.every(chromeOnlyVisible) && !actions.some((a) => a.type === "ASSERT_STATE_DELTA")) {
|
|
85946
|
+
return {
|
|
85947
|
+
ok: false,
|
|
85948
|
+
severity: "downgrade",
|
|
85949
|
+
reason: "only landing/chrome visibility after interaction \u2014 not a durable product outcome",
|
|
85950
|
+
signals: {
|
|
85951
|
+
apiTest: false,
|
|
85952
|
+
interacted: actions.some((a) => a.type === "TAP" || a.type === "TYPE"),
|
|
85953
|
+
mutated: actions.some((a) => a.type === "TYPE" || a.type === "CLEAR" || a.type === "TAP"),
|
|
85954
|
+
navOnly: false,
|
|
85955
|
+
hasOutcomeProof: false,
|
|
85956
|
+
errorBanner: false,
|
|
85957
|
+
terminal: null
|
|
85958
|
+
}
|
|
85959
|
+
};
|
|
85960
|
+
}
|
|
85226
85961
|
if (ephemeral.length > 0 && durable.length === 0) {
|
|
85227
85962
|
return {
|
|
85228
85963
|
ok: false,
|
|
@@ -85317,10 +86052,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85317
86052
|
}
|
|
85318
86053
|
const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
|
|
85319
86054
|
const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
|
|
86055
|
+
const bugWitnessEnabled = (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_BUG_WITNESS_ENABLED, true);
|
|
85320
86056
|
const bugLooksLikeAnr = (0, anr_env_js_1.textLooksLikeAnrOrSystemUi)(bugDescription);
|
|
85321
|
-
const bugWitnessHonored =
|
|
86057
|
+
const bugWitnessHonored = bugWitnessEnabled && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0 && !bugLooksLikeAnr;
|
|
85322
86058
|
if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
|
|
85323
|
-
log2(" suspectedBug declared but NOT honored: " + (bugLooksLikeAnr ? "ANR/System UI freeze is environment noise, not a product bug-witness." :
|
|
86059
|
+
log2(" suspectedBug declared but NOT honored: " + (bugLooksLikeAnr ? "ANR/System UI freeze is environment noise, not a product bug-witness." : !bugWitnessEnabled ? "bug-witness channel disabled (DISCOVERY_BUG_WITNESS_ENABLED)." : bugDescription.length < 12 ? "bugDescription too short." : "no honored assert backs the observation."));
|
|
85324
86060
|
}
|
|
85325
86061
|
const strengthVerdict = bugWitnessHonored ? {
|
|
85326
86062
|
...baseStrengthVerdict,
|
|
@@ -85350,30 +86086,48 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85350
86086
|
if (selectorLintEnforced && verified) {
|
|
85351
86087
|
log2(` selector-quality lint (enforce): ${selectorQuality.summary.high} high-severity locator issue(s) \u2014 tagging @needs-review.`);
|
|
85352
86088
|
}
|
|
85353
|
-
const
|
|
86089
|
+
const strengthBlocked = !strengthVerdict.ok || strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug" || !verified || coldResetUnavailable;
|
|
86090
|
+
const claimCoherence = (0, oracle_quality_js_1.assessScenarioClaimCoherence)({
|
|
86091
|
+
name: scenario.name,
|
|
86092
|
+
goal: scenario.goal,
|
|
86093
|
+
type: scenario.type,
|
|
86094
|
+
variant: scenario.variant,
|
|
86095
|
+
featureGroup: scenario.featureGroup
|
|
86096
|
+
}, compiled.steps);
|
|
86097
|
+
const oracleQualityEarly = (0, oracle_quality_js_1.assessStrongOracleQuality)(compiled.steps);
|
|
86098
|
+
if (oracleQualityEarly.anrOnly || claimCoherence.discard) {
|
|
86099
|
+
log2(` discarding incoherent/ANR recording: ${claimCoherence.discard ? claimCoherence.reason : oracleQualityEarly.reason}`);
|
|
86100
|
+
return null;
|
|
86101
|
+
}
|
|
86102
|
+
const reviewTag = verified && !selectorLintEnforced && claimCoherence.coherent ? "@verified" : "@needs-review";
|
|
85354
86103
|
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
85355
86104
|
if (strengthVerdict.severity === "downgrade")
|
|
85356
86105
|
tags.push("@smoke");
|
|
86106
|
+
if (strengthVerdict.severity === "route-bug" && !tags.includes("@bug-found")) {
|
|
86107
|
+
tags.push("@bug-found");
|
|
86108
|
+
}
|
|
85357
86109
|
if (coldResetUnavailable)
|
|
85358
86110
|
tags.push("@warm-state");
|
|
85359
|
-
const strengthBlocked = !strengthVerdict.ok || strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug" || !verified || coldResetUnavailable;
|
|
85360
86111
|
{
|
|
85361
86112
|
const kind = (0, scenario_taxonomy_js_1.inferScenarioKind)(scenario);
|
|
85362
|
-
const oracleQuality =
|
|
85363
|
-
if (oracleQuality.anrOnly) {
|
|
85364
|
-
log2(` discarding ANR-chrome-only recording: ${oracleQuality.reason}`);
|
|
85365
|
-
return null;
|
|
85366
|
-
}
|
|
86113
|
+
const oracleQuality = oracleQualityEarly;
|
|
85367
86114
|
if (oracleQuality.unpairedCaptures > 0) {
|
|
85368
86115
|
log2(` oracle quality: ${oracleQuality.reason}`);
|
|
85369
86116
|
}
|
|
85370
|
-
|
|
86117
|
+
if (!claimCoherence.coherent) {
|
|
86118
|
+
log2(` scenario claim weak: ${claimCoherence.reason}`);
|
|
86119
|
+
if (!tags.includes("@smoke"))
|
|
86120
|
+
tags.push("@smoke");
|
|
86121
|
+
}
|
|
86122
|
+
const strong = claimCoherence.coherent && verified && !strengthBlocked && strengthVerdict.severity === "none" && oracleQuality.strong && ((0, scenario_taxonomy_js_1.isStrongOracleKind)(kind) || oracleQuality.hasStateDelta || oracleQuality.hasStrongAssertText);
|
|
85371
86123
|
if (strong)
|
|
85372
86124
|
tags.push("@strong-oracle");
|
|
85373
86125
|
else if (!tags.includes("@smoke")) {
|
|
85374
86126
|
tags.push("@smoke");
|
|
85375
86127
|
if (!oracleQuality.strong) {
|
|
85376
86128
|
log2(` tagged @smoke (not strong-oracle): ${oracleQuality.reason}`);
|
|
86129
|
+
} else if (!claimCoherence.coherent) {
|
|
86130
|
+
log2(` tagged @smoke (claim incoherent): ${claimCoherence.reason}`);
|
|
85377
86131
|
}
|
|
85378
86132
|
}
|
|
85379
86133
|
}
|
|
@@ -85393,11 +86147,9 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85393
86147
|
playwrightCode: "",
|
|
85394
86148
|
verified,
|
|
85395
86149
|
tags,
|
|
85396
|
-
// Per-test
|
|
85397
|
-
//
|
|
85398
|
-
//
|
|
85399
|
-
// every test under the deep-dived feature. The scenario's own group is the
|
|
85400
|
-
// correct home; the runner forwards it as `suiteName`.
|
|
86150
|
+
// Per-test suite hint. Server deep mode overrides suite to ctx.featureName
|
|
86151
|
+
// so leaf features receive their own tests; full-mode multi-group still uses
|
|
86152
|
+
// featureGroup when present.
|
|
85401
86153
|
...scenario.featureGroup?.trim() ? { suiteName: scenario.featureGroup.trim() } : {},
|
|
85402
86154
|
// Carry the planner scenario identity so incremental/resume can match an
|
|
85403
86155
|
// already-generated scenario reliably (scenarioRef.name === the plan's
|
|
@@ -85564,7 +86316,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85564
86316
|
credentials: credentialSecrets,
|
|
85565
86317
|
redactionCredentials: credentialValuesForRedaction,
|
|
85566
86318
|
log: log2,
|
|
85567
|
-
allowRelaunch: (ctx.target?.resetPolicy ?? "AUTO") !== "PRESERVE_APP_DATA"
|
|
86319
|
+
allowRelaunch: (ctx.target?.resetPolicy ?? "AUTO") !== "PRESERVE_APP_DATA",
|
|
86320
|
+
knowledgeContext: ctx.knowledgeContext
|
|
85568
86321
|
});
|
|
85569
86322
|
consecutiveInfraFailures = 0;
|
|
85570
86323
|
const test = assembleTest({
|
|
@@ -85629,13 +86382,23 @@ var require_failure_triage = __commonJS({
|
|
|
85629
86382
|
"../runner-core/dist/services/mobile/failure-triage.js"(exports2) {
|
|
85630
86383
|
"use strict";
|
|
85631
86384
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
86385
|
+
exports2.isOracleStep = isOracleStep;
|
|
85632
86386
|
exports2.hasStrongMobileOracle = hasStrongMobileOracle;
|
|
85633
86387
|
exports2.isStrongOracleStep = isStrongOracleStep;
|
|
85634
86388
|
exports2.listStrongOracleKinds = listStrongOracleKinds;
|
|
85635
86389
|
exports2.triageMobileFailure = triageMobileFailure;
|
|
85636
86390
|
exports2.triageToHealTerminal = triageToHealTerminal;
|
|
85637
86391
|
var anr_env_js_1 = require_anr_env();
|
|
85638
|
-
var
|
|
86392
|
+
var oracle_quality_js_1 = require_oracle_quality();
|
|
86393
|
+
var STRONG_ACTIONS = /* @__PURE__ */ new Set(["assertStateDelta", "assertText"]);
|
|
86394
|
+
var ORACLE_ACTIONS = /* @__PURE__ */ new Set([
|
|
86395
|
+
"assertVisible",
|
|
86396
|
+
"assertText",
|
|
86397
|
+
"assertStateDelta"
|
|
86398
|
+
]);
|
|
86399
|
+
function isOracleStep(step) {
|
|
86400
|
+
return ORACLE_ACTIONS.has(step.action);
|
|
86401
|
+
}
|
|
85639
86402
|
var CHROME_VISIBLE_RE = /\b(tab|home|settings|back|menu|navigation|nav bar|bottom nav|status bar)\b/i;
|
|
85640
86403
|
function hasStrongMobileOracle(steps) {
|
|
85641
86404
|
return steps.some(isStrongOracleStep);
|
|
@@ -85651,23 +86414,31 @@ var require_failure_triage = __commonJS({
|
|
|
85651
86414
|
step.target?.primary ?? "",
|
|
85652
86415
|
...step.target?.fallbacks ?? []
|
|
85653
86416
|
].join(" ");
|
|
85654
|
-
if (
|
|
86417
|
+
if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(step))
|
|
86418
|
+
return false;
|
|
86419
|
+
if ((0, oracle_quality_js_1.isToastOrAckOutcomeText)(step.assertion ?? step.value ?? ""))
|
|
86420
|
+
return false;
|
|
86421
|
+
if ((0, oracle_quality_js_1.textLooksLikeProductOutcome)(blob))
|
|
85655
86422
|
return true;
|
|
85656
|
-
|
|
85657
|
-
if (
|
|
86423
|
+
const anchor = [step.target?.primary ?? "", ...step.target?.fallbacks ?? []].join(" ").trim();
|
|
86424
|
+
if (!anchor)
|
|
85658
86425
|
return false;
|
|
85659
|
-
|
|
86426
|
+
if (CHROME_VISIBLE_RE.test(blob) || (0, oracle_quality_js_1.textLooksLikeChromeSurface)(blob))
|
|
86427
|
+
return false;
|
|
86428
|
+
return blob.trim().length >= 3;
|
|
85660
86429
|
}
|
|
85661
86430
|
function listStrongOracleKinds(steps) {
|
|
85662
86431
|
const kinds = /* @__PURE__ */ new Set();
|
|
86432
|
+
const hasDelta = steps.some((s) => s.action === "assertStateDelta");
|
|
85663
86433
|
for (const step of steps) {
|
|
85664
86434
|
if (step.action === "assertStateDelta")
|
|
85665
86435
|
kinds.add("assertStateDelta");
|
|
85666
86436
|
else if (step.action === "assertText")
|
|
85667
86437
|
kinds.add("assertText");
|
|
85668
|
-
else if (step.action === "captureState")
|
|
85669
|
-
|
|
85670
|
-
|
|
86438
|
+
else if (step.action === "captureState") {
|
|
86439
|
+
if (hasDelta)
|
|
86440
|
+
kinds.add("captureState");
|
|
86441
|
+
} else if (isStrongOracleStep(step))
|
|
85671
86442
|
kinds.add(step.action);
|
|
85672
86443
|
}
|
|
85673
86444
|
return [...kinds];
|
|
@@ -85689,6 +86460,8 @@ ${logs2 ?? ""}`.trim();
|
|
|
85689
86460
|
return true;
|
|
85690
86461
|
if (/Missing mobile test variable/i.test(text))
|
|
85691
86462
|
return true;
|
|
86463
|
+
if (/entry.?screen drift|screen drift|started on an unexpected screen/i.test(text))
|
|
86464
|
+
return true;
|
|
85692
86465
|
if (/\b(quota|rate.?limit|429|lockout)\b/i.test(text))
|
|
85693
86466
|
return true;
|
|
85694
86467
|
if (/\b(device (?:offline|disconnected|not found)|session (?:not started|terminated|deleted))\b/i.test(text)) {
|
|
@@ -85709,7 +86482,10 @@ ${logs2 ?? ""}`.trim();
|
|
|
85709
86482
|
return /not found|no such element|No identity locator|unable to locate/i.test(text);
|
|
85710
86483
|
}
|
|
85711
86484
|
function looksLikeOracleFailure(text) {
|
|
85712
|
-
return /assertStateDelta/i.test(text) || /expected state/i.test(text) || /expected (?:["'].+["'] )?(?:to be |text|visible|removed|count)/i.test(text) || /assertion failed|Assert .+ (?:visible|shows|contains)/i.test(text);
|
|
86485
|
+
return /assertStateDelta/i.test(text) || /expected state/i.test(text) || /expected (?:["'].+["'] )?(?:to be |text|visible|removed|count)/i.test(text) || /assertion failed|Assert .+ (?:visible|shows|contains)/i.test(text) || /found but not visible/i.test(text);
|
|
86486
|
+
}
|
|
86487
|
+
function looksLikeUnpairedStateDelta(text) {
|
|
86488
|
+
return /assertStateDelta[^\n]*no prior captureState/i.test(text);
|
|
85713
86489
|
}
|
|
85714
86490
|
function triageMobileFailure(input) {
|
|
85715
86491
|
const failureCount = Math.max(1, input.failureCount ?? 1);
|
|
@@ -85718,11 +86494,20 @@ ${logs2 ?? ""}`.trim();
|
|
|
85718
86494
|
const prefix = leadingPrefix(input.error);
|
|
85719
86495
|
const strongKinds = listStrongOracleKinds(input.steps);
|
|
85720
86496
|
const hasStrong = strongKinds.length > 0 || tags.some((t) => t === "@strong-oracle" || t === "strong-oracle");
|
|
86497
|
+
const failureText = (input.error ?? "").trim() || text;
|
|
86498
|
+
const failedStep = typeof input.failedStepOrder === "number" ? input.steps.find((s) => s.order === input.failedStepOrder) : void 0;
|
|
86499
|
+
const failedStepIsOracle = !!failedStep && isOracleStep(failedStep);
|
|
86500
|
+
const postHeal = input.postHeal === true;
|
|
86501
|
+
const locatorDrift = looksLikeLocatorMiss(failureText) && !failedStepIsOracle;
|
|
86502
|
+
const explicitOracleFailure = looksLikeOracleFailure(failureText);
|
|
86503
|
+
const missingOutcome = failedStepIsOracle && looksLikeLocatorMiss(failureText);
|
|
85721
86504
|
const evidence = {
|
|
85722
86505
|
hasStrongOracle: hasStrong,
|
|
85723
86506
|
strongOracleKinds: strongKinds,
|
|
85724
86507
|
failureCount,
|
|
85725
|
-
errorPrefix: prefix
|
|
86508
|
+
errorPrefix: prefix,
|
|
86509
|
+
...failedStep ? { failedStepAction: failedStep.action } : {},
|
|
86510
|
+
...failedStep ? { failedOnOracleStep: failedStepIsOracle } : {}
|
|
85726
86511
|
};
|
|
85727
86512
|
if ((0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(input.error, input.logs)) {
|
|
85728
86513
|
return {
|
|
@@ -85787,11 +86572,47 @@ ${logs2 ?? ""}`.trim();
|
|
|
85787
86572
|
evidence
|
|
85788
86573
|
};
|
|
85789
86574
|
}
|
|
85790
|
-
if (
|
|
85791
|
-
|
|
86575
|
+
if (looksLikeUnpairedStateDelta(failureText)) {
|
|
86576
|
+
return {
|
|
86577
|
+
verdict: "test_invalid",
|
|
86578
|
+
reason: "assertStateDelta has no prior captureState for its key \u2014 the test cannot prove a delta",
|
|
86579
|
+
shouldHeal: false,
|
|
86580
|
+
healVerdict: "test_invalid",
|
|
86581
|
+
evidence
|
|
86582
|
+
};
|
|
86583
|
+
}
|
|
86584
|
+
if (hasStrong && strongKinds.includes("assertStateDelta") && /assertStateDelta/i.test(failureText) && explicitOracleFailure && !locatorDrift) {
|
|
86585
|
+
return {
|
|
86586
|
+
verdict: "app_bug_candidate",
|
|
86587
|
+
reason: "assertStateDelta oracle failed \u2014 durable product state did not match expected outcome",
|
|
86588
|
+
shouldHeal: false,
|
|
86589
|
+
healVerdict: "app_bug",
|
|
86590
|
+
evidence
|
|
86591
|
+
};
|
|
86592
|
+
}
|
|
86593
|
+
if (hasStrong && (strongKinds.includes("assertText") || strongKinds.includes("assertVisible")) && explicitOracleFailure && !locatorDrift) {
|
|
86594
|
+
return {
|
|
86595
|
+
verdict: "app_bug_candidate",
|
|
86596
|
+
reason: `Strong oracle (${strongKinds.join(", ") || "tagged"}) failed with product-shaped error`,
|
|
86597
|
+
shouldHeal: false,
|
|
86598
|
+
healVerdict: "app_bug",
|
|
86599
|
+
evidence
|
|
86600
|
+
};
|
|
86601
|
+
}
|
|
86602
|
+
if (hasStrong && missingOutcome && postHeal) {
|
|
86603
|
+
return {
|
|
86604
|
+
verdict: "app_bug_candidate",
|
|
86605
|
+
reason: `Strong oracle (${strongKinds.join(", ") || "tagged"}) outcome never appeared and heal could not locate it`,
|
|
86606
|
+
shouldHeal: false,
|
|
86607
|
+
healVerdict: "app_bug",
|
|
86608
|
+
evidence
|
|
86609
|
+
};
|
|
86610
|
+
}
|
|
86611
|
+
if (hasStrong && failureCount >= 2) {
|
|
86612
|
+
if ((locatorDrift || missingOutcome) && !postHeal) {
|
|
85792
86613
|
return {
|
|
85793
86614
|
verdict: "needs_heal",
|
|
85794
|
-
reason: "Strong oracle present but
|
|
86615
|
+
reason: "Strong oracle present but the element was not found \u2014 attempt heal first",
|
|
85795
86616
|
shouldHeal: true,
|
|
85796
86617
|
evidence
|
|
85797
86618
|
};
|
|
@@ -85938,7 +86759,7 @@ var require_oracle_protection = __commonJS({
|
|
|
85938
86759
|
return `heal patch dropped assertText outcome proof (${before.assertTexts.length} \u2192 ${after.assertTexts.length})`;
|
|
85939
86760
|
}
|
|
85940
86761
|
if (lost.length > 0) {
|
|
85941
|
-
const softKept = lost.every((t) => after.assertTexts.some((a) => a
|
|
86762
|
+
const softKept = lost.every((t) => after.assertTexts.some((a) => a === t || a.includes(t)));
|
|
85942
86763
|
if (!softKept) {
|
|
85943
86764
|
return `heal patch replaced assertText expectations (${lost.slice(0, 2).join("; ")}) \u2014 outcome must stay`;
|
|
85944
86765
|
}
|
|
@@ -85964,6 +86785,23 @@ var require_oracle_protection = __commonJS({
|
|
|
85964
86785
|
return `heal patch downgraded strong oracle "${key.slice(0, 60)}" to assertVisible`;
|
|
85965
86786
|
}
|
|
85966
86787
|
}
|
|
86788
|
+
const OUTCOME_LANG = /\b(error|invalid|required|empty|saved|success|created|updated|deleted|removed|failed|missing)\b/i;
|
|
86789
|
+
const CHROME_TARGET = /\b(tab|home|settings|back|menu|navigation|nav bar|bottom nav|status bar)\b/i;
|
|
86790
|
+
for (const orig of original) {
|
|
86791
|
+
if (orig.action !== "assertVisible" || !(0, failure_triage_js_1.isStrongOracleStep)(orig))
|
|
86792
|
+
continue;
|
|
86793
|
+
const origBlob = [orig.assertion, orig.description, orig.target?.primary].filter(Boolean).join(" ");
|
|
86794
|
+
if (!OUTCOME_LANG.test(origBlob))
|
|
86795
|
+
continue;
|
|
86796
|
+
const key = normalizeText(orig.description ?? orig.assertion ?? "");
|
|
86797
|
+
const match = patched.find((p) => p.action === "assertVisible" && normalizeText(p.description ?? p.assertion ?? "") === key);
|
|
86798
|
+
if (!match)
|
|
86799
|
+
continue;
|
|
86800
|
+
const newTarget = match.target?.primary ?? "";
|
|
86801
|
+
if (CHROME_TARGET.test(newTarget) && !OUTCOME_LANG.test(newTarget)) {
|
|
86802
|
+
return `heal patch retargeted outcome assert "${key.slice(0, 60)}" onto chrome`;
|
|
86803
|
+
}
|
|
86804
|
+
}
|
|
85967
86805
|
return null;
|
|
85968
86806
|
}
|
|
85969
86807
|
function oraclesPreserved(original, patched) {
|
|
@@ -86054,14 +86892,14 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
86054
86892
|
isVerifiedFix = false;
|
|
86055
86893
|
const anrEnv = (0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(heal.healReason ?? args.finalError ?? args.reproduceError, typeof args.finalError === "string" ? args.finalError : void 0);
|
|
86056
86894
|
const isConfigIssue = heal.verdict === "config_issue" || heal.verdict === "app_bug" && anrEnv;
|
|
86057
|
-
const isAppBug = !anrEnv &&
|
|
86895
|
+
const isAppBug = !anrEnv && heal.verdict === "app_bug";
|
|
86058
86896
|
const extraTags = new Set((heal.extraTags ?? []).filter((t) => typeof t === "string" && t.length > 0));
|
|
86059
86897
|
if (secretLeak)
|
|
86060
86898
|
extraTags.add("@needs-review");
|
|
86061
|
-
if (
|
|
86062
|
-
extraTags.add("@needs-review");
|
|
86899
|
+
if (isAppBug)
|
|
86063
86900
|
extraTags.add("@bug-found");
|
|
86064
|
-
|
|
86901
|
+
if (oracleWeakening && !anrEnv)
|
|
86902
|
+
extraTags.add("@needs-review");
|
|
86065
86903
|
if (anrEnv) {
|
|
86066
86904
|
extraTags.delete("@bug-found");
|
|
86067
86905
|
extraTags.add("@needs-review");
|
|
@@ -86085,7 +86923,7 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
86085
86923
|
} else if (secretLeak) {
|
|
86086
86924
|
error2 = `HEAL_SECRET_LEAK: healed steps contained a literal secret (${secretLeak}); patch dropped, tagged @needs-review`;
|
|
86087
86925
|
} else if (oracleWeakening && !anrEnv) {
|
|
86088
|
-
error2 = `
|
|
86926
|
+
error2 = `HEAL_ORACLE_REFUSED: heal attempted to weaken a strong oracle (${oracleWeakening}); patch refused, tagged @needs-review`;
|
|
86089
86927
|
} else if (isConfigIssue) {
|
|
86090
86928
|
const reason = heal.healReason ?? args.finalError ?? args.reproduceError ?? "environment/rate-limit blocker";
|
|
86091
86929
|
error2 = anrEnv ? `CONFIG_ISSUE: ANR / System UI freeze (not a product assertion failure) \u2014 ${reason}` : `CONFIG_ISSUE: ${reason}`;
|
|
@@ -86542,7 +87380,7 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
86542
87380
|
1. ERROR: which step failed, on what locator/assertion? "element not found"/"no such element" almost always means a WRONG LOCATOR on a screen that DOES exist \u2014 pick a stronger IDENTITY locator that is actually present in the captured tree: a resource-id, an accessibility-id/content-desc, or an EXACT @text= predicate (never a loose contains()). Do NOT declare a feature absent from one failed locator.
|
|
86543
87381
|
2. STEP RESULTS: which steps passed before the failure.
|
|
86544
87382
|
3. TREE: does the current screen match what the failed step expects?
|
|
86545
|
-
3b.
|
|
87383
|
+
3b. MISSING PRODUCT OUTCOME: a failing assert on empty-state CTAs, confirmation copy, a created/deleted row, or validation text is a PRODUCT oracle, not a locator problem. If that outcome is absent from a healthy screen, return verdict "app_bug". Do NOT replace outcome asserts with tab / header / nav chrome. Only retarget a locator when the SAME outcome copy is present in the tree.
|
|
86546
87384
|
3c. ANR / "System UI isn't responding" / freeze dialogs: this is ENVIRONMENT (config_issue), NOT app_bug. Do not quarantine product bugs for device freezes.
|
|
86547
87385
|
4. EXTERNAL AUTH HANDOFF: if the failure says the foreground app changed outside the target app after a Login / Sign In tap, and the steps never enter email/password credentials, do NOT heal by choosing Google/Apple/Facebook/SSO or another external app. If the test later expects Logout/session state, return test_invalid or prereq_unmet: the recorded test lacks a real credential setup.
|
|
86548
87386
|
5. ENTRY-SCREEN DRIFT ("unexpected screen"/"screen drift"): the app started on a DIFFERENT screen than recorded; the fix is usually a MISSING NAVIGATION step (e.g. tap the onboarding/welcome CTA present in the tree before the recorded first step). Author it from the tree if you can (verdict fix); if a single static tree is insufficient, return needs_device.
|
|
@@ -86793,6 +87631,14 @@ For a fix, return the COMPLETE steps array (not a diff).
|
|
|
86793
87631
|
function isIdentityEqualityXpath(value) {
|
|
86794
87632
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
86795
87633
|
}
|
|
87634
|
+
var STEP_ACTIONS_NEEDING_ELEMENT = /* @__PURE__ */ new Set([
|
|
87635
|
+
"tap",
|
|
87636
|
+
"type",
|
|
87637
|
+
"clear",
|
|
87638
|
+
"waitForElement",
|
|
87639
|
+
"assertVisible",
|
|
87640
|
+
"assertText"
|
|
87641
|
+
]);
|
|
86796
87642
|
function stepLacksIdentityAnchor(step) {
|
|
86797
87643
|
const metadata = step.metadata;
|
|
86798
87644
|
if (metadata && (metadata.appContext === "WEBVIEW" || typeof metadata.webCss === "string"))
|
|
@@ -86833,7 +87679,7 @@ For a fix, return the COMPLETE steps array (not a diff).
|
|
|
86833
87679
|
async function tryIdentityGateStaticRepair(steps, target, ctx, options) {
|
|
86834
87680
|
if (!ctx.error || !IDENTITY_GATE_ERROR_RE.test(ctx.error))
|
|
86835
87681
|
return null;
|
|
86836
|
-
const identityless = steps.filter(stepLacksIdentityAnchor);
|
|
87682
|
+
const identityless = steps.filter((step) => STEP_ACTIONS_NEEDING_ELEMENT.has(step.action) && stepLacksIdentityAnchor(step));
|
|
86837
87683
|
if (identityless.length === 0)
|
|
86838
87684
|
return null;
|
|
86839
87685
|
const isAssert = (step) => /^assert/i.test(step.action);
|
|
@@ -86844,7 +87690,8 @@ For a fix, return the COMPLETE steps array (not a diff).
|
|
|
86844
87690
|
healReason: "identity gate: an action step carries no identity locator (accessibility id / resource id / text); the recording lacks the anchors replay requires \u2014 re-record the step"
|
|
86845
87691
|
};
|
|
86846
87692
|
}
|
|
86847
|
-
const
|
|
87693
|
+
const identitylessOrders = new Set(identityless.map((step) => step.order));
|
|
87694
|
+
const pruned = steps.filter((step) => !identitylessOrders.has(step.order));
|
|
86848
87695
|
if (pruned.length === 0 || !pruned.some(isAssert) || !options.runNativeTest) {
|
|
86849
87696
|
return {
|
|
86850
87697
|
passed: false,
|
|
@@ -86852,6 +87699,14 @@ For a fix, return the COMPLETE steps array (not a diff).
|
|
|
86852
87699
|
healReason: "identity gate: the only assertion(s) carry no identity locator, so the test cannot prove its outcome as recorded \u2014 re-record with an identifiable anchor"
|
|
86853
87700
|
};
|
|
86854
87701
|
}
|
|
87702
|
+
const identityOracleRejection = (0, oracle_protection_js_1.rejectOracleWeakeningPatch)(steps, pruned);
|
|
87703
|
+
if (identityOracleRejection) {
|
|
87704
|
+
return {
|
|
87705
|
+
passed: false,
|
|
87706
|
+
verdict: "app_bug",
|
|
87707
|
+
healReason: `identity gate: pruning identity-less asserts would drop product oracles (${identityOracleRejection})`
|
|
87708
|
+
};
|
|
87709
|
+
}
|
|
86855
87710
|
const reindexed = pruned.map((step, order) => ({ ...step, order }));
|
|
86856
87711
|
try {
|
|
86857
87712
|
const verify = await options.runNativeTest({ steps: reindexed, target, testVariables: ctx.testVariables });
|
|
@@ -87108,7 +87963,12 @@ ${safeLastError}`);
|
|
|
87108
87963
|
${args.directive}`);
|
|
87109
87964
|
return parts.join("\n\n");
|
|
87110
87965
|
}
|
|
87111
|
-
function findFailingStepIndex(steps, error2) {
|
|
87966
|
+
function findFailingStepIndex(steps, error2, failedStepOrder) {
|
|
87967
|
+
if (typeof failedStepOrder === "number") {
|
|
87968
|
+
const exact = steps.findIndex((step) => step.order === failedStepOrder);
|
|
87969
|
+
if (exact >= 0)
|
|
87970
|
+
return exact;
|
|
87971
|
+
}
|
|
87112
87972
|
const withTarget = steps.map((step, index) => ({ step, index })).filter((entry) => Boolean(entry.step.target?.primary));
|
|
87113
87973
|
if (withTarget.length === 0)
|
|
87114
87974
|
return -1;
|
|
@@ -87144,9 +88004,13 @@ ${args.directive}`);
|
|
|
87144
88004
|
if (!runNativeTest || !options.selectorRegistryContext || !ctx.error)
|
|
87145
88005
|
return null;
|
|
87146
88006
|
const candidates = (0, selector_registry_context_js_1.collectMobileRegistryCandidates)(options.selectorRegistryContext);
|
|
87147
|
-
const failingIndex = findFailingStepIndex(steps, ctx.error);
|
|
88007
|
+
const failingIndex = findFailingStepIndex(steps, ctx.error, ctx.failedStepOrder);
|
|
87148
88008
|
if (failingIndex < 0)
|
|
87149
88009
|
return null;
|
|
88010
|
+
if ((0, failure_triage_js_1.isOracleStep)(steps[failingIndex])) {
|
|
88011
|
+
options.onProgress?.(`Registry preflight skipped: step ${steps[failingIndex].order} is an oracle (${steps[failingIndex].action}) \u2014 a registry selector cannot stand in for a product outcome`);
|
|
88012
|
+
return null;
|
|
88013
|
+
}
|
|
87150
88014
|
let tried = 0;
|
|
87151
88015
|
for (const candidate of candidates) {
|
|
87152
88016
|
if (tried >= REGISTRY_PREFLIGHT_MAX_CANDIDATES)
|
|
@@ -87156,6 +88020,8 @@ ${args.directive}`);
|
|
|
87156
88020
|
continue;
|
|
87157
88021
|
tried += 1;
|
|
87158
88022
|
const healedSteps = substituteStepTarget(steps, failingIndex, candidate, mapped.locatorBundle);
|
|
88023
|
+
if ((0, oracle_protection_js_1.rejectOracleWeakeningPatch)(steps, healedSteps))
|
|
88024
|
+
continue;
|
|
87159
88025
|
let verify;
|
|
87160
88026
|
try {
|
|
87161
88027
|
verify = await runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables });
|
|
@@ -87481,6 +88347,41 @@ ${args.directive}`);
|
|
|
87481
88347
|
async function executeMobileTestWithHealing2(steps, target, ctx, options) {
|
|
87482
88348
|
return timed("total", "heal-total", options.onProgress, () => executeMobileTestWithHealingImpl(steps, target, ctx, options));
|
|
87483
88349
|
}
|
|
88350
|
+
var TERMINAL_HEAL_VERDICTS = /* @__PURE__ */ new Set([
|
|
88351
|
+
"app_bug",
|
|
88352
|
+
"config_issue",
|
|
88353
|
+
"prereq_unmet",
|
|
88354
|
+
"state_stale",
|
|
88355
|
+
"test_invalid",
|
|
88356
|
+
"unhealable_dynamic"
|
|
88357
|
+
]);
|
|
88358
|
+
function escalateFailedHealToTriage(steps, ctx, lastError) {
|
|
88359
|
+
const post = (0, failure_triage_js_1.triageMobileFailure)({
|
|
88360
|
+
steps,
|
|
88361
|
+
error: lastError ?? ctx.error,
|
|
88362
|
+
logs: ctx.logs,
|
|
88363
|
+
tags: ctx.tags,
|
|
88364
|
+
// Heal already ran and failed. Bump past the locator-miss heal window
|
|
88365
|
+
// so a remaining strong-oracle fail becomes APP_BUG, not silent FAILED.
|
|
88366
|
+
failureCount: Math.max(3, (ctx.failureCount ?? 1) + 1),
|
|
88367
|
+
failedStepOrder: ctx.failedStepOrder,
|
|
88368
|
+
// Heal ran and failed: a still-missing element is no longer "maybe drift".
|
|
88369
|
+
postHeal: true,
|
|
88370
|
+
uiSuccessWithFailedWriteApi: (0, network_oracle_js_1.uiSuccessWithFailedWriteApi)({
|
|
88371
|
+
apiCalls: ctx.apiCalls,
|
|
88372
|
+
uiPathSucceeded: ctx.uiPathSucceeded
|
|
88373
|
+
})
|
|
88374
|
+
});
|
|
88375
|
+
const term = (0, failure_triage_js_1.triageToHealTerminal)(post);
|
|
88376
|
+
if (!term)
|
|
88377
|
+
return null;
|
|
88378
|
+
return {
|
|
88379
|
+
passed: false,
|
|
88380
|
+
verdict: term.verdict,
|
|
88381
|
+
healReason: term.healReason,
|
|
88382
|
+
extraTags: term.extraTags
|
|
88383
|
+
};
|
|
88384
|
+
}
|
|
87484
88385
|
async function executeMobileTestWithHealingImpl(steps, target, ctx, options) {
|
|
87485
88386
|
const invalidAuthSetup = detectInvalidMobileAuthSetup(steps, ctx.error);
|
|
87486
88387
|
if (invalidAuthSetup) {
|
|
@@ -87496,6 +88397,7 @@ ${args.directive}`);
|
|
|
87496
88397
|
logs: ctx.logs,
|
|
87497
88398
|
tags: ctx.tags,
|
|
87498
88399
|
failureCount: ctx.failureCount,
|
|
88400
|
+
failedStepOrder: ctx.failedStepOrder,
|
|
87499
88401
|
uiSuccessWithFailedWriteApi: networkBug
|
|
87500
88402
|
});
|
|
87501
88403
|
options.onProgress?.(`Pre-heal triage: ${triage.verdict} \u2014 ${triage.reason}`);
|
|
@@ -87553,6 +88455,17 @@ ${args.directive}`);
|
|
|
87553
88455
|
if (live.providerUnavailable) {
|
|
87554
88456
|
return await tryDeterministicProviderFallback({ steps, target, ctx, options }) ?? live;
|
|
87555
88457
|
}
|
|
88458
|
+
if (!live.passed && !TERMINAL_HEAL_VERDICTS.has(live.verdict)) {
|
|
88459
|
+
const escalated = escalateFailedHealToTriage(steps, ctx, live.healReason ?? ctx.error);
|
|
88460
|
+
if (escalated) {
|
|
88461
|
+
return {
|
|
88462
|
+
...live,
|
|
88463
|
+
...escalated,
|
|
88464
|
+
healedSteps: live.healedSteps,
|
|
88465
|
+
healedAppiumCode: live.healedAppiumCode
|
|
88466
|
+
};
|
|
88467
|
+
}
|
|
88468
|
+
}
|
|
87556
88469
|
return live;
|
|
87557
88470
|
} catch {
|
|
87558
88471
|
return {
|
|
@@ -87564,6 +88477,21 @@ ${args.directive}`);
|
|
|
87564
88477
|
};
|
|
87565
88478
|
}
|
|
87566
88479
|
}
|
|
88480
|
+
if (!script.passed && !TERMINAL_HEAL_VERDICTS.has(script.verdict)) {
|
|
88481
|
+
const escalated = escalateFailedHealToTriage(steps, ctx, script.lastError ?? script.healReason ?? ctx.error);
|
|
88482
|
+
if (escalated) {
|
|
88483
|
+
return {
|
|
88484
|
+
passed: false,
|
|
88485
|
+
verdict: escalated.verdict,
|
|
88486
|
+
healedSteps: script.healedSteps,
|
|
88487
|
+
healedAppiumCode: script.healedAppiumCode,
|
|
88488
|
+
healReason: escalated.healReason,
|
|
88489
|
+
extraTags: escalated.extraTags,
|
|
88490
|
+
confidence: script.confidence,
|
|
88491
|
+
removeTags: script.removeTags
|
|
88492
|
+
};
|
|
88493
|
+
}
|
|
88494
|
+
}
|
|
87567
88495
|
return {
|
|
87568
88496
|
passed: script.passed,
|
|
87569
88497
|
healedSteps: script.healedSteps,
|
|
@@ -87584,7 +88512,12 @@ The recorded flow may have expected a DIFFERENT starting screen than the one sho
|
|
|
87584
88512
|
1. Compare the recorded first step's expected screen to the LIVE tree.
|
|
87585
88513
|
2. If they differ, PREPEND the missing navigation step(s), authored from IDENTITY locators (resource-id, accessibility-id, or EXACT @text=) that are PRESENT in the LIVE tree \u2014 never a loose contains().
|
|
87586
88514
|
3. If a locator is simply wrong, swap it for a stronger identity locator present in the LIVE tree.
|
|
87587
|
-
3b. If the failing step ASSERTS
|
|
88515
|
+
3b. If the failing step ASSERTS a product outcome (empty-state CTA, confirmation, created/deleted row, validation text) that is absent from a healthy LIVE tree, return verdict "app_bug". Do NOT replace that assert with tab / header / nav chrome. Only retarget a locator when the SAME outcome copy is present in the live tree.
|
|
88516
|
+
## Strong oracle protection (non-negotiable)
|
|
88517
|
+
- NEVER remove or downgrade assertStateDelta, captureState, or assertText outcome proof to go green.
|
|
88518
|
+
- NEVER replace a durable outcome assert with mere assertVisible of a tab/header.
|
|
88519
|
+
- You MAY fix locators, waits, and navigation around those oracles.
|
|
88520
|
+
- If the app truly violates the oracle on a healthy UI, return verdict "app_bug" instead of stripping the assert.
|
|
87588
88521
|
4. If the prior failure says the foreground app changed outside the target app after a Login / Sign In tap, and the steps never enter email/password credentials, do NOT observe/tap Google/Apple/Facebook/SSO. For logout/session tests, return test_invalid or prereq_unmet because the recorded test lacks a real credential setup.
|
|
87589
88522
|
5. If you cannot yet SEE the screen you need, use "observe" to advance ONE step, then you will be re-shown the new live screen.
|
|
87590
88523
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
@@ -87593,7 +88526,7 @@ The recorded flow may have expected a DIFFERENT starting screen than the one sho
|
|
|
87593
88526
|
{"verdict":"observe","action":{"type":"back"}}
|
|
87594
88527
|
{"verdict":"observe","action":{"type":"relaunch"}}
|
|
87595
88528
|
{"verdict":"observe","action":{"type":"reset"}} \u2190 clears app data + cold-relaunches: use when the app is in the WRONG STATE for the recorded flow (e.g. signed in when the test expects the cleared first-launch screens, stale onboarding, poisoned local state)
|
|
87596
|
-
{"verdict":"app_bug","reason":"
|
|
88529
|
+
{"verdict":"app_bug","reason":"product defect with durable oracle evidence (wrong entity, validation bypass, wrong count, missing confirmation) \u2014 NOT ANR/System UI"}
|
|
87597
88530
|
{"verdict":"config_issue","reason":"429/rate limit/lockout/quota"}
|
|
87598
88531
|
{"verdict":"needs_device","reason":"cannot resolve even with live access"}
|
|
87599
88532
|
{"verdict":"prereq_unmet","reason":"required entity/state absent for the account"}
|
|
@@ -87844,6 +88777,7 @@ var require_browser_config = __commonJS({
|
|
|
87844
88777
|
var path_1 = require("path");
|
|
87845
88778
|
var os_1 = require("os");
|
|
87846
88779
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
88780
|
+
var google_signin_js_1 = require_google_signin();
|
|
87847
88781
|
exports2.IS_CLOUD = (0, shared_1.parseBoolFlag)(process.env.RUNNER_CLOUD);
|
|
87848
88782
|
exports2.CLOUD_ARGS = [
|
|
87849
88783
|
"--no-sandbox",
|
|
@@ -87949,6 +88883,10 @@ var require_browser_config = __commonJS({
|
|
|
87949
88883
|
(document.head || document.documentElement).appendChild(style);
|
|
87950
88884
|
|
|
87951
88885
|
// Layer 2: Web Animations API - make instant (preserves completion callbacks)
|
|
88886
|
+
try {
|
|
88887
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
88888
|
+
} catch {}
|
|
88889
|
+
|
|
87952
88890
|
const origAnimate = Element.prototype.animate;
|
|
87953
88891
|
Element.prototype.animate = function(keyframes, options) {
|
|
87954
88892
|
if (typeof options === 'object' && options !== null) {
|
|
@@ -88055,6 +88993,20 @@ var require_browser_config = __commonJS({
|
|
|
88055
88993
|
return null;
|
|
88056
88994
|
}
|
|
88057
88995
|
}
|
|
88996
|
+
function googleSsoCloudLaunch(env = process.env) {
|
|
88997
|
+
if (!(0, shared_1.parseBoolFlag)(env.RUNNER_CLOUD))
|
|
88998
|
+
return null;
|
|
88999
|
+
if (!(0, google_signin_js_1.loadGoogleSignInConfig)(env))
|
|
89000
|
+
return null;
|
|
89001
|
+
const executablePath = getFullChromiumPath();
|
|
89002
|
+
if (!executablePath)
|
|
89003
|
+
return null;
|
|
89004
|
+
return {
|
|
89005
|
+
executablePath,
|
|
89006
|
+
extraArgs: ["--headless=new"],
|
|
89007
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
89008
|
+
};
|
|
89009
|
+
}
|
|
88058
89010
|
function getMCPBrowserConfig2(opts) {
|
|
88059
89011
|
const userDataDir = opts?.forceIsolated ? null : getTestUserDataDir2();
|
|
88060
89012
|
const headless = isHeadless2();
|
|
@@ -88078,15 +89030,21 @@ var require_browser_config = __commonJS({
|
|
|
88078
89030
|
};
|
|
88079
89031
|
}
|
|
88080
89032
|
const headlessShellPath = getHeadlessShellPath();
|
|
88081
|
-
const
|
|
89033
|
+
const googleLaunch = googleSsoCloudLaunch();
|
|
89034
|
+
const useHeadlessShell = headless && headlessShellPath && !opts?.harPath && !googleLaunch;
|
|
89035
|
+
const cloud = (0, shared_1.parseBoolFlag)(process.env.RUNNER_CLOUD);
|
|
89036
|
+
const args = [
|
|
89037
|
+
...cloud ? exports2.CLOUD_ARGS : ["--disable-blink-features=AutomationControlled"],
|
|
89038
|
+
...googleLaunch?.extraArgs ?? []
|
|
89039
|
+
];
|
|
88082
89040
|
return {
|
|
88083
89041
|
browserName: "chromium",
|
|
88084
89042
|
isolated: true,
|
|
88085
89043
|
launchOptions: {
|
|
88086
|
-
headless,
|
|
88087
|
-
...useHeadlessShell ? { executablePath: headlessShellPath } : {},
|
|
89044
|
+
headless: googleLaunch ? false : headless,
|
|
89045
|
+
...googleLaunch ? { executablePath: googleLaunch.executablePath, ignoreDefaultArgs: googleLaunch.ignoreDefaultArgs } : useHeadlessShell ? { executablePath: headlessShellPath } : {},
|
|
88088
89046
|
channel: "",
|
|
88089
|
-
args
|
|
89047
|
+
args,
|
|
88090
89048
|
...slowMo > 0 ? { slowMo } : {}
|
|
88091
89049
|
},
|
|
88092
89050
|
contextOptions: {
|
|
@@ -88099,15 +89057,21 @@ var require_browser_config = __commonJS({
|
|
|
88099
89057
|
function getPlaywrightLaunchOptions2(withProfile = false, extraArgs) {
|
|
88100
89058
|
const headless = isHeadless2();
|
|
88101
89059
|
const slowMo = getSlowMoMs();
|
|
89060
|
+
const googleLaunch = googleSsoCloudLaunch();
|
|
88102
89061
|
const baseArgs = exports2.IS_CLOUD ? exports2.CLOUD_ARGS : [
|
|
88103
89062
|
"--disable-blink-features=AutomationControlled"
|
|
88104
89063
|
];
|
|
88105
|
-
const mergedArgs =
|
|
89064
|
+
const mergedArgs = [
|
|
89065
|
+
...baseArgs,
|
|
89066
|
+
...googleLaunch?.extraArgs ?? [],
|
|
89067
|
+
...extraArgs && extraArgs.length > 0 ? extraArgs : []
|
|
89068
|
+
];
|
|
88106
89069
|
return {
|
|
88107
|
-
headless,
|
|
89070
|
+
headless: googleLaunch ? false : headless,
|
|
88108
89071
|
// Use Chrome channel only when a real Chrome user profile is being loaded.
|
|
88109
89072
|
// In isolated mode, bundled Chromium avoids hard dependency on Chrome installation.
|
|
88110
89073
|
...exports2.IS_CLOUD || !withProfile ? {} : { channel: "chrome" },
|
|
89074
|
+
...googleLaunch ? { executablePath: googleLaunch.executablePath, ignoreDefaultArgs: googleLaunch.ignoreDefaultArgs } : {},
|
|
88111
89075
|
args: mergedArgs,
|
|
88112
89076
|
...slowMo > 0 ? { slowMo } : {}
|
|
88113
89077
|
};
|
|
@@ -91660,7 +92624,7 @@ var require_kill_tree = __commonJS({
|
|
|
91660
92624
|
var node_child_process_1 = require("child_process");
|
|
91661
92625
|
var node_util_1 = require("util");
|
|
91662
92626
|
var node_fs_1 = require("fs");
|
|
91663
|
-
var
|
|
92627
|
+
var execFileAsync5 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
91664
92628
|
var PS_TIMEOUT_MS = 2e3;
|
|
91665
92629
|
var PS_BUFFER = 1024 * 1024;
|
|
91666
92630
|
async function gracefulCloseWithTreeKill(options) {
|
|
@@ -91714,7 +92678,7 @@ var require_kill_tree = __commonJS({
|
|
|
91714
92678
|
timer.unref?.();
|
|
91715
92679
|
let stdout;
|
|
91716
92680
|
try {
|
|
91717
|
-
const result = await
|
|
92681
|
+
const result = await execFileAsync5("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
91718
92682
|
maxBuffer: PS_BUFFER,
|
|
91719
92683
|
signal: controller.signal
|
|
91720
92684
|
});
|
|
@@ -91752,7 +92716,7 @@ var require_kill_tree = __commonJS({
|
|
|
91752
92716
|
timer.unref?.();
|
|
91753
92717
|
let stdout;
|
|
91754
92718
|
try {
|
|
91755
|
-
const result = await
|
|
92719
|
+
const result = await execFileAsync5("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
91756
92720
|
maxBuffer: PS_BUFFER,
|
|
91757
92721
|
signal: controller.signal
|
|
91758
92722
|
});
|
|
@@ -91829,7 +92793,7 @@ var require_kill_tree = __commonJS({
|
|
|
91829
92793
|
timer.unref?.();
|
|
91830
92794
|
let stdout;
|
|
91831
92795
|
try {
|
|
91832
|
-
const result = await
|
|
92796
|
+
const result = await execFileAsync5("ps", ["-A", "-o", "pid=,ppid="], {
|
|
91833
92797
|
maxBuffer: PS_BUFFER,
|
|
91834
92798
|
signal: controller.signal
|
|
91835
92799
|
});
|
|
@@ -101405,13 +102369,12 @@ Every assertion in the generated test must be proven live.
|
|
|
101405
102369
|
**Frame**
|
|
101406
102370
|
- \`import { test, expect } from '@playwright/test'\` with tags [${tagLiteral}].
|
|
101407
102371
|
- Credentials: \`process.env.TEST_USER_EMAIL!\` / \`process.env.TEST_USER_PASSWORD!\` \u2014 never hardcode. ${emailVerifyArmed ? `Registration uses \`process.env.${test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV}!\` (the fresh real inbox address the runner mints per run \u2014 it RECEIVES the verification email; see the EMAIL VERIFICATION TEST section) with \`process.env.${test_inbox_broker_js_1.TEST_INBOX_PASSWORD_ENV}!\`. Do NOT register with an \`@example.com\` address \u2014 that domain receives no mail and email verification will hang.` : `Registration uses \`testuser-\${Date.now()}@example.com\`.`}
|
|
101408
|
-
- **Credential rotation (
|
|
101409
|
-
(a)
|
|
101410
|
-
(b)
|
|
101411
|
-
(
|
|
101412
|
-
|
|
101413
|
-
- **
|
|
101414
|
-
- **Restore-inline is FORBIDDEN for the stored user.** Never "change the stored user's password to a temp value, then change it back to the old one" as plain sequential statements. The change mutates the LIVE account immediately; if any later step (the second navigation, the restore submit, the success assertion) fails, the live password stays at the temp value while \`process.env.TEST_USER_PASSWORD\` still holds the old one \u2014 and every other auth test then fails to log in. The ONLY supported way to mutate the stored user's password is rotate-FORWARD via \`persistCredential\` (above): change it to a fresh value, verify success, and write that SAME final value back as the last statement. Do NOT restore the old password, and do NOT introduce a temp-then-revert sequence.
|
|
102372
|
+
- **Credential rotation (REQUIRED when the STORED user's password changes):** emit \`persistCredential\` whenever the scenario successfully changes the password (or other secret) of the STORED test user \u2014 the account identified by \`process.env.TEST_USER_EMAIL!\` / \`process.env.TEST_USER_PASSWORD!\` (the credentials listed under Browser Credentials above). This includes BOTH:
|
|
102373
|
+
(a) **In-session change password** \u2014 log in as the stored user, open *their own* settings \u2192 Change Password \u2192 submit \u2192 success assertion; AND
|
|
102374
|
+
(b) **Forgot-password / password-reset of the stored user** \u2014 request a reset for \`process.env.TEST_USER_EMAIL!\` (logged-out is fine), complete the emailed link/code, set a NEW password on the reset form, and assert success. You do NOT need a prior login with the old password \u2014 a successful reset of the stored account still MUST write the new password back so the Test Data page stays correct and later tests do not fail with stale credentials.
|
|
102375
|
+
When either case holds, generate a fresh value into a local const (\`const newPassword = \\\`Pw-\\\${Date.now()}-\\\${Math.random().toString(36).slice(2, 8)}!\\\`\`), fill it into the new-password / confirm fields, submit, verify success, THEN add: \`import { persistCredential } from '../setup/credentials';\` at the top and \`await persistCredential('TEST_USER_PASSWORD', newPassword, 'short reason');\` as the LAST statement in the test body (after the final assertion is fine only if the assertion does not depend on the write-back; prefer write-back immediately after the success proof). Also add the tag \`@mutates-credential:TEST_USER_PASSWORD\` to the test options array. Adjust the relative path of the import to match the spec's location (e.g. \`'../../setup/credentials'\` for deeply-nested specs).
|
|
102376
|
+
- **Do NOT emit \`persistCredential\` when:** (i) the scenario registers a fresh throwaway user (${emailVerifyArmed ? `e.g. the runner-minted \`process.env.${test_inbox_broker_js_1.TEST_INBOX_EMAIL_ENV}!\` inbox account` : `e.g. \`testuser-\${Date.now()}@example.com\``}) and rotates THAT user's password \u2014 the fresh user is throwaway, the stored credential is untouched; (ii) the scenario is a negative test verifying a bad-password / mismatched-confirm / empty-field / permission-denied case; (iii) the scenario only fills form validation paths without a successful submit; (iv) the scenario only requests a reset email without completing the reset and setting a new password. In all these cases simply omit the call.
|
|
102377
|
+
- **Restore-inline is FORBIDDEN for the stored user.** Never "change the stored user's password to a temp value, then change it back to the old one" as plain sequential statements. The change mutates the LIVE account immediately; if any later step (the second navigation, the restore submit, the success assertion) fails, the live password stays at the temp value while \`process.env.TEST_USER_PASSWORD\` still holds the old one \u2014 and every other auth test then fails to log in. The ONLY supported way to mutate the stored user's password is rotate-FORWARD via \`persistCredential\` (above): change it to a fresh value, verify success, and write that SAME final value back as the last write-back statement. Do NOT restore the old password, and do NOT introduce a temp-then-revert sequence.
|
|
101415
102378
|
- **Start every test from \`${scenarioEntryRoute}\`.** ${hasVerifiedRecipe ? "Your first body line is the injected `await __vqaLogin(page);` \u2014 login is handled for you. After it, reach feature pages by clicking visible nav or a direct goto to the scenario route." : `First line: \`await page.goto('${scenarioEntryRoute}', { waitUntil: 'domcontentloaded' })\`. You are logged OUT; if the route redirects, that proves you must authenticate FIRST via the visible login flow \u2014 never \`waitForURL\` on a post-login route before submitting credentials.`} Do NOT deep-link to \`/login\` or \`/dashboard/...\` before login; reach feature pages by clicking visible nav (sidebar / topbar / link / button) like a user would.
|
|
101416
102379
|
- ${hasVerifiedRecipe ? "Auth is proven inside `__vqaLogin` \u2014 do NOT add your own login or auth-proof assertion; focus your assertions on the feature outcome." : tags.includes("@needs-auth") ? "For `@needs-auth` scenarios, add one auth-proof assertion before feature steps: prefer Logout button, then avatar/menu, authenticated sidebar/nav, or a stable post-login heading. Never use user email, time-of-day greetings, display name text, personalized headings, or redirect alone." : "Use stable assertions tied to the current flow \u2014 avoid personalized or account-specific copy when a structural proof exists."}
|
|
101417
102380
|
- Always \`await\` on \`expect()\`. Never use \`test.tags()\` as a method \u2014 tags go in the options object.
|
|
@@ -102025,7 +102988,7 @@ Fix EVERY flagged locator, keep every other line exactly as-is, then submit the
|
|
|
102025
102988
|
}
|
|
102026
102989
|
const verifyCallsSinceLastFinish = verifyCallCount - verifyCallsAtLastFinishGenerate;
|
|
102027
102990
|
verifyCallsAtLastFinishGenerate = verifyCallCount;
|
|
102028
|
-
const bugWitnessEnabled = (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_BUG_WITNESS_ENABLED);
|
|
102991
|
+
const bugWitnessEnabled = (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_BUG_WITNESS_ENABLED, true);
|
|
102029
102992
|
const bugWitnessVerdict = bugWitnessEnabled ? resolveSuspectedBugVerdict(suspectedBugDeclared, bugDescription, verifyCallsSinceLastFinish, strengthVerdict?.signals) : null;
|
|
102030
102993
|
if (bugWitnessVerdict) {
|
|
102031
102994
|
strengthVerdict = bugWitnessVerdict;
|
|
@@ -319450,6 +320413,7 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319450
320413
|
var promises_1 = require("fs/promises");
|
|
319451
320414
|
var node_path_1 = require("path");
|
|
319452
320415
|
var browser_config_js_1 = require_browser_config();
|
|
320416
|
+
var google_signin_js_1 = require_google_signin();
|
|
319453
320417
|
var PLAYWRIGHT_MCP_VERSION = "0.0.68";
|
|
319454
320418
|
var INBOX_SECRET_ENV_KEYS = [
|
|
319455
320419
|
"VERIFICATION_INBOX_HOST",
|
|
@@ -319459,7 +320423,10 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319459
320423
|
"VERIFICATION_INBOX_PASSWORD",
|
|
319460
320424
|
"VERIFICATION_INBOX_ADDRESS",
|
|
319461
320425
|
"VERIFICATION_INBOX_MAILBOX",
|
|
319462
|
-
"VERIFICATION_INBOX_EXTRA_MAILBOXES"
|
|
320426
|
+
"VERIFICATION_INBOX_EXTRA_MAILBOXES",
|
|
320427
|
+
"GOOGLE_SIGNIN_PASSWORD",
|
|
320428
|
+
"GOOGLE_SIGNIN_EMAIL",
|
|
320429
|
+
"GOOGLE_SIGNIN_TOTP_SECRET"
|
|
319463
320430
|
];
|
|
319464
320431
|
function collectInboxSecretEnv() {
|
|
319465
320432
|
const out = {};
|
|
@@ -319500,12 +320467,13 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319500
320467
|
const body = cfg.format === "json" ? buildJsonConfig(cfg) : buildTomlConfig(cfg);
|
|
319501
320468
|
await (0, promises_1.writeFile)(cfg.configPath, body, { encoding: "utf8", mode: 384 });
|
|
319502
320469
|
if (!cfg.cdpEndpoint && browser_config_js_1.IS_CLOUD) {
|
|
320470
|
+
const launchOptions = (0, browser_config_js_1.getMCPBrowserConfig)({ forceIsolated: true }).launchOptions ?? {
|
|
320471
|
+
headless: true,
|
|
320472
|
+
args: [...browser_config_js_1.CLOUD_ARGS]
|
|
320473
|
+
};
|
|
319503
320474
|
const browserConfig = {
|
|
319504
320475
|
browser: {
|
|
319505
|
-
launchOptions
|
|
319506
|
-
headless: true,
|
|
319507
|
-
args: [...browser_config_js_1.CLOUD_ARGS]
|
|
319508
|
-
}
|
|
320476
|
+
launchOptions
|
|
319509
320477
|
}
|
|
319510
320478
|
};
|
|
319511
320479
|
await (0, promises_1.writeFile)(mcpBrowserConfigPath(cfg.configPath), JSON.stringify(browserConfig, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
@@ -319529,7 +320497,11 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319529
320497
|
];
|
|
319530
320498
|
if (cfg.includeCredentialServer && cfg.cdpEndpoint) {
|
|
319531
320499
|
const serverJs = (0, node_path_1.join)(__dirname, "credential-mcp-server.js");
|
|
319532
|
-
blocks.push("[mcp_servers.validateqa_credentials]", 'command = "node"', `args = ["${tomlEscape(serverJs)}"]`, "enabled = true", "startup_timeout_sec = 30", "tool_timeout_sec = 60", "", "[mcp_servers.validateqa_credentials.env]", `CDP_ENDPOINT = "${tomlEscape(cfg.cdpEndpoint)}"
|
|
320500
|
+
blocks.push("[mcp_servers.validateqa_credentials]", 'command = "node"', `args = ["${tomlEscape(serverJs)}"]`, "enabled = true", "startup_timeout_sec = 30", "tool_timeout_sec = 60", "", "[mcp_servers.validateqa_credentials.env]", `CDP_ENDPOINT = "${tomlEscape(cfg.cdpEndpoint)}"`);
|
|
320501
|
+
for (const [k, v] of Object.entries((0, google_signin_js_1.collectGoogleSignInEnv)())) {
|
|
320502
|
+
blocks.push(`${k} = "${tomlEscape(v)}"`);
|
|
320503
|
+
}
|
|
320504
|
+
blocks.push("");
|
|
319533
320505
|
}
|
|
319534
320506
|
if (cfg.includeDiscoveryServer) {
|
|
319535
320507
|
if (!cfg.runDir) {
|
|
@@ -319577,7 +320549,7 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319577
320549
|
type: "stdio",
|
|
319578
320550
|
command: "node",
|
|
319579
320551
|
args: [serverJs],
|
|
319580
|
-
env: { CDP_ENDPOINT: cfg.cdpEndpoint }
|
|
320552
|
+
env: { CDP_ENDPOINT: cfg.cdpEndpoint, ...(0, google_signin_js_1.collectGoogleSignInEnv)() }
|
|
319581
320553
|
};
|
|
319582
320554
|
}
|
|
319583
320555
|
if (cfg.includeDiscoveryServer) {
|
|
@@ -327784,6 +328756,7 @@ var require_discover_explorer = __commonJS({
|
|
|
327784
328756
|
var credential_tools_js_1 = require_credential_tools();
|
|
327785
328757
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
327786
328758
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
328759
|
+
var google_signin_js_1 = require_google_signin();
|
|
327787
328760
|
var discover_planner_js_1 = require_discover_planner();
|
|
327788
328761
|
var exploration_prompt_builder_js_1 = require_exploration_prompt_builder();
|
|
327789
328762
|
var ai_queue_js_1 = require_ai_queue();
|
|
@@ -328188,7 +329161,8 @@ NEVER assert: user-specific text (names, emails), counts/numbers, timestamps, ma
|
|
|
328188
329161
|
};
|
|
328189
329162
|
function buildSystemPrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, options) {
|
|
328190
329163
|
const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
328191
|
-
const { secrets:
|
|
329164
|
+
const { secrets: explorerSecretVarsRaw, publics: explorerPublicVarsRaw } = (0, credential_tools_js_1.partitionTestVariables)(browserCredentials);
|
|
329165
|
+
const explorerSecretVars = options?.inboxPrompt ? (0, google_signin_js_1.mergeGoogleSignInSecrets)(explorerSecretVarsRaw) : explorerSecretVarsRaw;
|
|
328192
329166
|
const hasCredentials = Object.keys(explorerSecretVars).length > 0;
|
|
328193
329167
|
const { DISCOVERY_SHOULD_REGISTER: _omitRegisterFlag, ...explorerPublicVars } = explorerPublicVarsRaw;
|
|
328194
329168
|
const explorerPublicLines = (0, credential_tools_js_1.renderPublicVariablesBlock)(explorerPublicVars);
|
|
@@ -328805,10 +329779,12 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
328805
329779
|
if (emailVerificationUnavailable) {
|
|
328806
329780
|
logs2.push("Email verification unavailable \u2014 the agent will ATTEMPT registration once; any flow requiring an emailed code/link will be reported BLOCKED.");
|
|
328807
329781
|
}
|
|
329782
|
+
const googleGuidance = (0, google_signin_js_1.googleSignInGuidanceOpts)();
|
|
328808
329783
|
const inboxPromptBlock = inboxState ? (0, inbox_tools_js_1.buildInboxPromptBlock)({
|
|
328809
329784
|
email: inboxState.email,
|
|
328810
329785
|
passwordKey: inboxState.passwordKey ?? "TEST_USER_PASSWORD",
|
|
328811
|
-
context: options?.mode
|
|
329786
|
+
context: options?.mode,
|
|
329787
|
+
...googleGuidance
|
|
328812
329788
|
}) : "";
|
|
328813
329789
|
try {
|
|
328814
329790
|
const { tools: mcpTools } = await mcpClient.listTools();
|
|
@@ -328829,7 +329805,8 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
328829
329805
|
}
|
|
328830
329806
|
const browserCredsForTools = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
328831
329807
|
const { secrets: explorerToolSecrets } = (0, credential_tools_js_1.partitionTestVariables)(browserCredsForTools);
|
|
328832
|
-
|
|
329808
|
+
const explorerToolSecretsWithGoogle = inboxState ? (0, google_signin_js_1.mergeGoogleSignInSecrets)(explorerToolSecrets) : explorerToolSecrets;
|
|
329809
|
+
if (Object.keys(explorerToolSecretsWithGoogle).length > 0) {
|
|
328833
329810
|
openaiTools.push(credential_tools_js_1.FILL_CREDENTIAL_TOOL, credential_tools_js_1.FILL_CREDENTIAL_FORM_TOOL);
|
|
328834
329811
|
if (options?.onCredentialUpdate) {
|
|
328835
329812
|
openaiTools.push(credential_tools_js_1.SET_CREDENTIAL_TOOL);
|
|
@@ -329469,8 +330446,12 @@ ${currentSummary}`;
|
|
|
329469
330446
|
let finishBouncedThisBatch = false;
|
|
329470
330447
|
let finishStreakCountedThisBatch = false;
|
|
329471
330448
|
const fullVarMapForGuard = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
329472
|
-
const { secrets:
|
|
329473
|
-
const
|
|
330449
|
+
const { secrets: partitionedSecretsForGuard } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMapForGuard);
|
|
330450
|
+
const secretsOnlyForGuard = inboxState ? (0, google_signin_js_1.mergeGoogleSignInSecrets)(partitionedSecretsForGuard) : partitionedSecretsForGuard;
|
|
330451
|
+
const allowedCredentialValues = [...new Set([
|
|
330452
|
+
...inboxState ? [inboxState.email, inboxState.baseEmail] : [],
|
|
330453
|
+
...inboxState ? (0, google_signin_js_1.googleSignInAllowedEmails)() : []
|
|
330454
|
+
].filter((value) => !!value))];
|
|
329474
330455
|
const secretsForLogging = Object.fromEntries(Object.entries(secretsOnlyForGuard).filter(([, value]) => !allowedCredentialValues.includes(value)));
|
|
329475
330456
|
for (const toolCall of assistantMessage.tool_calls) {
|
|
329476
330457
|
const toolName = toolCall.function.name;
|
|
@@ -330321,8 +331302,9 @@ Exploration complete: ${summary}`);
|
|
|
330321
331302
|
}
|
|
330322
331303
|
const fullVarMap = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
330323
331304
|
const { secrets: dispatchSecrets } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMap);
|
|
331305
|
+
const fillSecrets = inboxState ? (0, google_signin_js_1.mergeGoogleSignInSecrets)(dispatchSecrets) : dispatchSecrets;
|
|
330324
331306
|
try {
|
|
330325
|
-
const result = toolName === "fill_credential" ? await (0, credential_tools_js_1.handleFillCredential)(toolArgs, { mcpClient, credentials:
|
|
331307
|
+
const result = toolName === "fill_credential" ? await (0, credential_tools_js_1.handleFillCredential)(toolArgs, { mcpClient, credentials: fillSecrets, maxResultChars: MAX_TOOL_RESULT_CHARS }) : await (0, credential_tools_js_1.handleFillCredentialForm)(toolArgs, { mcpClient, credentials: fillSecrets, maxResultChars: MAX_TOOL_RESULT_CHARS });
|
|
330326
331308
|
if (result.didMutate) {
|
|
330327
331309
|
lastSnapshotText = null;
|
|
330328
331310
|
const codeLines = (0, exploration_parser_js_1.extractCodeLines)(result.content);
|
|
@@ -332362,7 +333344,7 @@ ${keyLines || "- (none provided)"}
|
|
|
332362
333344
|
## EXACTLY what to do
|
|
332363
333345
|
1. Navigate to ${baseUrl} and take a \`browser_snapshot\`.
|
|
332364
333346
|
2. Find the Sign Up / Register / Create account affordance (match by PURPOSE, in the app's own language) and open the signup form. If a reveal button hides the fields ("Sign up with Email"), click it first.
|
|
332365
|
-
3. Follow the "Account Registration (verification inbox is ENABLED)" steps below EXACTLY: call \`provision_identity\` for the email, type that email, fill the password with \`fill_credential\`, fill any other required fields with simple test values, and submit.
|
|
333347
|
+
3. Follow the "Account Registration (verification inbox is ENABLED)" steps below EXACTLY: call \`provision_identity\` for the email, type that email, fill the password with \`fill_credential\`, fill any other required fields with simple test values, and submit. If there is NO email+password signup field and only "Continue with Google" / "Sign up with Gmail", follow the Google / Gmail SSO fallback in that section (bare Google account email, \`fill_credential\` with \`GOOGLE_SIGNIN_PASSWORD\`, never the IMAP app password).
|
|
332366
333348
|
4. Complete email verification using the inbox tools (\`wait_for_verification\` \u2192 enter the code or \`open_verification_link\`). Re-poll if the email is slow; resend AT MOST once; NEVER register a second account.
|
|
332367
333349
|
5. Confirm you are signed in \u2014 a redirect to an authenticated route, or an in-place change to authenticated content. Take a \`browser_snapshot\`.
|
|
332368
333350
|
6. \`capture_page\` the authenticated landing page in its DEFAULT state, then call \`finish_exploration\`. Do this the MOMENT the account is verified and you are signed in.
|
|
@@ -332916,6 +333898,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
332916
333898
|
var grok_cli_js_1 = require_grok_cli();
|
|
332917
333899
|
var playwright_mcp_config_js_1 = require_playwright_mcp_config();
|
|
332918
333900
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
333901
|
+
var google_signin_js_1 = require_google_signin();
|
|
332919
333902
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
332920
333903
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
332921
333904
|
var agentic_survey_prompts_js_1 = require_agentic_survey_prompts();
|
|
@@ -333440,7 +334423,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
333440
334423
|
};
|
|
333441
334424
|
const surveyAgentsMd = inboxReg ? `${(0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)()}
|
|
333442
334425
|
|
|
333443
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey)}` : (0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)();
|
|
334426
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : (0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)();
|
|
333444
334427
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "AGENTS.md"), surveyAgentsMd, "utf8");
|
|
333445
334428
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_survey_prompts_js_1.buildSurveyBriefMd)(briefInput), "utf8");
|
|
333446
334429
|
log2("Wrote AGENTS.md + BRIEF.md");
|
|
@@ -334847,6 +335830,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
334847
335830
|
var playwright_native_js_1 = require_playwright_native();
|
|
334848
335831
|
var playwright_mcp_config_js_1 = require_playwright_mcp_config();
|
|
334849
335832
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
335833
|
+
var google_signin_js_1 = require_google_signin();
|
|
334850
335834
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
334851
335835
|
var grok_survey_runner_js_1 = require_grok_survey_runner();
|
|
334852
335836
|
var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
|
|
@@ -335140,7 +336124,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
335140
336124
|
};
|
|
335141
336125
|
const discoveryAgentsMd = inboxReg ? `${(0, agentic_discovery_prompts_js_1.buildDiscoveryAgentsMd)()}
|
|
335142
336126
|
|
|
335143
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey)}` : (0, agentic_discovery_prompts_js_1.buildDiscoveryAgentsMd)();
|
|
336127
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : (0, agentic_discovery_prompts_js_1.buildDiscoveryAgentsMd)();
|
|
335144
336128
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "AGENTS.md"), discoveryAgentsMd, "utf8");
|
|
335145
336129
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_discovery_prompts_js_1.buildDiscoveryBriefMd)(briefInput), "utf8");
|
|
335146
336130
|
await (0, promises_1.mkdir)(testsDirPath, { recursive: true });
|
|
@@ -336093,7 +337077,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
336093
337077
|
|
|
336094
337078
|
---
|
|
336095
337079
|
|
|
336096
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxRegV2.email, inboxRegV2.passwordKey)}` : "");
|
|
337080
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxRegV2.email, inboxRegV2.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : "");
|
|
336097
337081
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "AGENTS.md"), agentsMd, "utf8");
|
|
336098
337082
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_discovery_prompts_js_1.buildDiscoveryBriefMd)(briefInput), "utf8");
|
|
336099
337083
|
await (0, promises_1.mkdir)(testsDirPath, { recursive: true });
|
|
@@ -337065,6 +338049,7 @@ var require_claude_survey_runner = __commonJS({
|
|
|
337065
338049
|
var claude_stream_parser_js_1 = require_claude_stream_parser();
|
|
337066
338050
|
var playwright_mcp_config_js_1 = require_playwright_mcp_config();
|
|
337067
338051
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
338052
|
+
var google_signin_js_1 = require_google_signin();
|
|
337068
338053
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
337069
338054
|
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
337070
338055
|
var grok_survey_runner_js_1 = require_grok_survey_runner();
|
|
@@ -337165,7 +338150,7 @@ ${preview}`);
|
|
|
337165
338150
|
};
|
|
337166
338151
|
const surveyAgentsMd = inboxReg ? `${(0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)({ canvasMode })}
|
|
337167
338152
|
|
|
337168
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey)}` : (0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)({ canvasMode });
|
|
338153
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : (0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)({ canvasMode });
|
|
337169
338154
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "AGENTS.md"), surveyAgentsMd, "utf8");
|
|
337170
338155
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_survey_prompts_js_1.buildSurveyBriefMd)(briefInput), "utf8");
|
|
337171
338156
|
log2("Wrote AGENTS.md + BRIEF.md");
|
|
@@ -337875,6 +338860,7 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
337875
338860
|
var claude_cli_js_1 = require_claude_cli();
|
|
337876
338861
|
var playwright_mcp_config_js_1 = require_playwright_mcp_config();
|
|
337877
338862
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
338863
|
+
var google_signin_js_1 = require_google_signin();
|
|
337878
338864
|
var coverage_examples_content_js_1 = require_coverage_examples_content();
|
|
337879
338865
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
337880
338866
|
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
@@ -338043,7 +339029,7 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
338043
339029
|
};
|
|
338044
339030
|
const explorerContractMd = inboxReg ? `${(0, agentic_discovery_prompts_js_1.buildExplorerContractMd)(contractInput)}
|
|
338045
339031
|
|
|
338046
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey)}` : (0, agentic_discovery_prompts_js_1.buildExplorerContractMd)(contractInput);
|
|
339032
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : (0, agentic_discovery_prompts_js_1.buildExplorerContractMd)(contractInput);
|
|
338047
339033
|
await (0, promises_1.writeFile)(claudeMdPath, explorerContractMd, "utf8");
|
|
338048
339034
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_discovery_prompts_js_1.buildDiscoveryBriefMd)(briefInput), "utf8");
|
|
338049
339035
|
await (0, promises_1.mkdir)(testsDirPath, { recursive: true });
|
|
@@ -338915,6 +339901,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
338915
339901
|
appId,
|
|
338916
339902
|
platform: platform3,
|
|
338917
339903
|
appBrief: ctx.appBrief,
|
|
339904
|
+
// Optional project docs / knowledge facts — empty is valid; planner
|
|
339905
|
+
// relies on live explorer evidence when absent (bug capture does not
|
|
339906
|
+
// require documents).
|
|
339907
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
338918
339908
|
featureName: ctx.featureName,
|
|
338919
339909
|
existingTestNames: ctx.existingTestNames,
|
|
338920
339910
|
existingFeatureNames: ctx.existingFeatureNames,
|
|
@@ -349075,7 +350065,7 @@ var require_update = __commonJS({
|
|
|
349075
350065
|
var require_opts_arg = __commonJS({
|
|
349076
350066
|
"../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports2, module2) {
|
|
349077
350067
|
"use strict";
|
|
349078
|
-
var { promisify:
|
|
350068
|
+
var { promisify: promisify5 } = require("util");
|
|
349079
350069
|
var fs = require("fs");
|
|
349080
350070
|
var optsArg = (opts) => {
|
|
349081
350071
|
if (!opts)
|
|
@@ -349089,9 +350079,9 @@ var require_opts_arg = __commonJS({
|
|
|
349089
350079
|
else
|
|
349090
350080
|
throw new TypeError("invalid options argument");
|
|
349091
350081
|
opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
|
|
349092
|
-
opts.mkdirAsync =
|
|
350082
|
+
opts.mkdirAsync = promisify5(opts.mkdir);
|
|
349093
350083
|
opts.stat = opts.stat || opts.fs.stat || fs.stat;
|
|
349094
|
-
opts.statAsync =
|
|
350084
|
+
opts.statAsync = promisify5(opts.stat);
|
|
349095
350085
|
opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
|
|
349096
350086
|
opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
|
|
349097
350087
|
return opts;
|
|
@@ -353940,7 +354930,7 @@ var require_process_inventory = __commonJS({
|
|
|
353940
354930
|
var node_child_process_1 = require("child_process");
|
|
353941
354931
|
var node_os_1 = __importDefault(require("os"));
|
|
353942
354932
|
var node_util_1 = require("util");
|
|
353943
|
-
var
|
|
354933
|
+
var execFileAsync5 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
353944
354934
|
var DEFAULT_LIMIT = 60;
|
|
353945
354935
|
var DEFAULT_TIMEOUT_MS2 = 800;
|
|
353946
354936
|
var MAX_COMMAND_LENGTH = 320;
|
|
@@ -354002,7 +354992,7 @@ var require_process_inventory = __commonJS({
|
|
|
354002
354992
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
354003
354993
|
timer.unref?.();
|
|
354004
354994
|
try {
|
|
354005
|
-
const { stdout } = await
|
|
354995
|
+
const { stdout } = await execFileAsync5(command, args, {
|
|
354006
354996
|
maxBuffer: 1024 * 1024,
|
|
354007
354997
|
signal: controller.signal
|
|
354008
354998
|
});
|
|
@@ -355296,7 +356286,7 @@ var require_package4 = __commonJS({
|
|
|
355296
356286
|
"package.json"(exports2, module2) {
|
|
355297
356287
|
module2.exports = {
|
|
355298
356288
|
name: "@validate.qa/runner",
|
|
355299
|
-
version: "1.0.
|
|
356289
|
+
version: "1.0.93",
|
|
355300
356290
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
355301
356291
|
bin: {
|
|
355302
356292
|
"validate-runner": "dist/cli.js"
|
|
@@ -356161,11 +357151,7 @@ var SYSTEM_UI_DISMISS_LABELS = [
|
|
|
356161
357151
|
"OK",
|
|
356162
357152
|
"Got it",
|
|
356163
357153
|
"GOT IT",
|
|
356164
|
-
"Continue",
|
|
356165
|
-
"Close",
|
|
356166
357154
|
"Dismiss",
|
|
356167
|
-
"I understand",
|
|
356168
|
-
"Accept",
|
|
356169
357155
|
// Android 15+ / 16 KB page-size compatibility dialog primary action.
|
|
356170
357156
|
"OK, got it"
|
|
356171
357157
|
];
|
|
@@ -360092,7 +361078,11 @@ async function executeAssertVisible(sessionId, step, platform3, isCrossPlatform
|
|
|
360092
361078
|
tolerateAmbiguity: true
|
|
360093
361079
|
});
|
|
360094
361080
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/displayed`);
|
|
360095
|
-
if (!result2?.value)
|
|
361081
|
+
if (!result2?.value) {
|
|
361082
|
+
throw new Error(
|
|
361083
|
+
`Assertion failed: expected "${css}" to be visible, but the WEBVIEW element resolved and is not displayed`
|
|
361084
|
+
);
|
|
361085
|
+
}
|
|
360096
361086
|
});
|
|
360097
361087
|
return;
|
|
360098
361088
|
}
|
|
@@ -360104,7 +361094,9 @@ async function executeAssertVisible(sessionId, step, platform3, isCrossPlatform
|
|
|
360104
361094
|
});
|
|
360105
361095
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
360106
361096
|
if (!result?.value) {
|
|
360107
|
-
throw new Error(
|
|
361097
|
+
throw new Error(
|
|
361098
|
+
`Assertion failed: expected "${step.target?.primary ?? "element"}" to be visible, but it resolved and is not displayed`
|
|
361099
|
+
);
|
|
360108
361100
|
}
|
|
360109
361101
|
}
|
|
360110
361102
|
async function executeAssertText(sessionId, step, platform3, isCrossPlatform = false) {
|
|
@@ -362631,8 +363623,28 @@ function getMirrorSessionId(deviceId) {
|
|
|
362631
363623
|
}
|
|
362632
363624
|
|
|
362633
363625
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
363626
|
+
var import_node_child_process3 = require("child_process");
|
|
363627
|
+
var import_node_util3 = require("util");
|
|
362634
363628
|
var import_runner_core9 = __toESM(require_dist2());
|
|
362635
363629
|
init_dist();
|
|
363630
|
+
var execFileAsync4 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
363631
|
+
async function clearAndroidAppDataViaAdb(deviceId, appId) {
|
|
363632
|
+
const serial = deviceId?.trim();
|
|
363633
|
+
const pkg2 = appId?.trim();
|
|
363634
|
+
if (!serial || !pkg2 || !serial.startsWith("emulator-")) return false;
|
|
363635
|
+
try {
|
|
363636
|
+
const { stdout, stderr } = await execFileAsync4(
|
|
363637
|
+
"adb",
|
|
363638
|
+
["-s", serial, "shell", "pm", "clear", pkg2],
|
|
363639
|
+
{ timeout: 2e4, maxBuffer: 64 * 1024 }
|
|
363640
|
+
);
|
|
363641
|
+
const out = `${stdout ?? ""}
|
|
363642
|
+
${stderr ?? ""}`.toLowerCase();
|
|
363643
|
+
return out.includes("success");
|
|
363644
|
+
} catch {
|
|
363645
|
+
return false;
|
|
363646
|
+
}
|
|
363647
|
+
}
|
|
362636
363648
|
var APPIUM_SESSION_ID_PATTERN2 = /^[a-f0-9-]{1,64}$/i;
|
|
362637
363649
|
function isRecoverableAppiumSessionFailure(err) {
|
|
362638
363650
|
if (isAppiumDriverDeadError(err)) return true;
|
|
@@ -363031,7 +364043,17 @@ var MobileBridgeDriver = class {
|
|
|
363031
364043
|
throw new Error(`iOS simulator app-data reset failed: ${wiped.detail}`);
|
|
363032
364044
|
}
|
|
363033
364045
|
} else {
|
|
363034
|
-
await this.
|
|
364046
|
+
const adbFirst = await clearAndroidAppDataViaAdb(this.deviceId, this.appId);
|
|
364047
|
+
if (!adbFirst) {
|
|
364048
|
+
try {
|
|
364049
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
364050
|
+
} catch (clearErr) {
|
|
364051
|
+
const adbCleared = await clearAndroidAppDataViaAdb(this.deviceId, this.appId);
|
|
364052
|
+
if (!adbCleared) {
|
|
364053
|
+
throw clearErr;
|
|
364054
|
+
}
|
|
364055
|
+
}
|
|
364056
|
+
}
|
|
363035
364057
|
}
|
|
363036
364058
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
363037
364059
|
try {
|
|
@@ -364263,10 +365285,28 @@ function truncateAuditRequestBody(body) {
|
|
|
364263
365285
|
function isTerminalAuditStatus(status) {
|
|
364264
365286
|
return status === 400 || status === 404 || status === 409 || status === 413;
|
|
364265
365287
|
}
|
|
365288
|
+
var AuditDrainError = class extends Error {
|
|
365289
|
+
constructor(message, dropped) {
|
|
365290
|
+
super(message);
|
|
365291
|
+
this.dropped = dropped;
|
|
365292
|
+
this.name = "AuditDrainError";
|
|
365293
|
+
}
|
|
365294
|
+
};
|
|
365295
|
+
function auditDroppedFromDrainFailure(reason) {
|
|
365296
|
+
if (reason instanceof AuditDrainError) return reason.dropped;
|
|
365297
|
+
if (reason && typeof reason === "object" && "dropped" in reason) {
|
|
365298
|
+
const dropped = reason.dropped;
|
|
365299
|
+
if (typeof dropped === "number" && Number.isFinite(dropped)) {
|
|
365300
|
+
return Math.max(0, Math.floor(dropped));
|
|
365301
|
+
}
|
|
365302
|
+
}
|
|
365303
|
+
return 0;
|
|
365304
|
+
}
|
|
364266
365305
|
function createAuditFlusher(url, headers) {
|
|
364267
365306
|
const entries = [];
|
|
364268
365307
|
let byteEstimate = 0;
|
|
364269
365308
|
let flushInFlight = null;
|
|
365309
|
+
let droppedTotal = 0;
|
|
364270
365310
|
const doFlush = () => {
|
|
364271
365311
|
if (entries.length === 0) return Promise.resolve();
|
|
364272
365312
|
if (flushInFlight) return flushInFlight;
|
|
@@ -364289,9 +365329,16 @@ function createAuditFlusher(url, headers) {
|
|
|
364289
365329
|
body: JSON.stringify({ entries: toSend.map((e) => e.entry) }),
|
|
364290
365330
|
signal: AbortSignal.timeout(3e4)
|
|
364291
365331
|
});
|
|
364292
|
-
if (res.ok)
|
|
365332
|
+
if (res.ok) {
|
|
365333
|
+
const ack = await res.json().catch(() => null);
|
|
365334
|
+
if (typeof ack?.dropped === "number" && Number.isFinite(ack.dropped)) {
|
|
365335
|
+
droppedTotal += Math.max(0, Math.floor(ack.dropped));
|
|
365336
|
+
}
|
|
365337
|
+
continue;
|
|
365338
|
+
}
|
|
364293
365339
|
if (isTerminalAuditStatus(res.status)) {
|
|
364294
365340
|
console.warn(`[audit-flush] HTTP ${res.status} (terminal) - dropping ${toSend.length} audit entries`);
|
|
365341
|
+
droppedTotal += toSend.length;
|
|
364295
365342
|
continue;
|
|
364296
365343
|
}
|
|
364297
365344
|
entries.unshift(...toSend);
|
|
@@ -364331,6 +365378,7 @@ function createAuditFlusher(url, headers) {
|
|
|
364331
365378
|
droppedCount++;
|
|
364332
365379
|
}
|
|
364333
365380
|
console.warn(`[audit-flush] queue over ${AUDIT_QUEUE_MAX_BYTES} bytes - dropped ${droppedCount} oldest audit entries`);
|
|
365381
|
+
droppedTotal += droppedCount;
|
|
364334
365382
|
}
|
|
364335
365383
|
if (entries.length >= AUDIT_FLUSH_COUNT || byteEstimate >= AUDIT_FLUSH_BYTES) {
|
|
364336
365384
|
void doFlush();
|
|
@@ -364339,16 +365387,20 @@ function createAuditFlusher(url, headers) {
|
|
|
364339
365387
|
const drain = async () => {
|
|
364340
365388
|
for (let attempt = 1; attempt <= AUDIT_DRAIN_ATTEMPTS; attempt++) {
|
|
364341
365389
|
if (flushInFlight) await flushInFlight;
|
|
364342
|
-
if (entries.length === 0) return;
|
|
365390
|
+
if (entries.length === 0) return { dropped: droppedTotal };
|
|
364343
365391
|
await doFlush();
|
|
364344
|
-
if (entries.length === 0) return;
|
|
365392
|
+
if (entries.length === 0) return { dropped: droppedTotal };
|
|
364345
365393
|
if (attempt < AUDIT_DRAIN_ATTEMPTS) {
|
|
364346
365394
|
await new Promise((resolve2) => setTimeout(resolve2, AUDIT_DRAIN_DELAY_MS * attempt));
|
|
364347
365395
|
}
|
|
364348
365396
|
}
|
|
364349
365397
|
if (entries.length > 0) {
|
|
364350
|
-
throw new
|
|
365398
|
+
throw new AuditDrainError(
|
|
365399
|
+
`audit drain failed after ${AUDIT_DRAIN_ATTEMPTS} attempts - ${entries.length} entries could not be flushed to ${url}`,
|
|
365400
|
+
droppedTotal + entries.length
|
|
365401
|
+
);
|
|
364351
365402
|
}
|
|
365403
|
+
return { dropped: droppedTotal };
|
|
364352
365404
|
};
|
|
364353
365405
|
return { push, drain };
|
|
364354
365406
|
}
|
|
@@ -365597,7 +366649,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
365597
366649
|
log.warn("[stream] discovery-plan failed after 3 attempts - plan checkpoint skipped");
|
|
365598
366650
|
};
|
|
365599
366651
|
let discoveryResult;
|
|
365600
|
-
const buildFinalBody = (r, overrideError, auditWarning) => {
|
|
366652
|
+
const buildFinalBody = (r, overrideError, auditWarning, auditDroppedCount = 0) => {
|
|
365601
366653
|
const isNativeDiscovery = ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE";
|
|
365602
366654
|
const { exploreFinalMessages: _exploreFinalMessages, ...rest } = r;
|
|
365603
366655
|
void _exploreFinalMessages;
|
|
@@ -365610,6 +366662,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
365610
366662
|
summary: rest.summary,
|
|
365611
366663
|
mode: rest.mode,
|
|
365612
366664
|
error: overrideError ?? rest.error,
|
|
366665
|
+
auditDroppedCount,
|
|
365613
366666
|
// Resumable-pause checkpoint: native deep generation tripped the AI-overload breaker.
|
|
365614
366667
|
// Without forwarding these, the server's isDiscoveryPaused is false → the run finalizes
|
|
365615
366668
|
// COMPLETED/FAILED instead of PAUSED → the Resume affordance never appears for
|
|
@@ -365995,12 +367048,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
365995
367048
|
discoveryAuditWarning = `[audit] ${drainErr instanceof Error ? drainErr.message : String(drainErr)}`;
|
|
365996
367049
|
console.warn(`[runner] ${discoveryAuditWarning}`);
|
|
365997
367050
|
}
|
|
367051
|
+
const auditDroppedCount = drainOutcome.status === "fulfilled" ? drainOutcome.value.dropped : auditDroppedFromDrainFailure(drainOutcome.reason);
|
|
365998
367052
|
const harStatus = harOutcome.status === "fulfilled" ? harOutcome.value : null;
|
|
365999
367053
|
harPosted = decideHarPosted(harStatus !== null ? { status: harStatus } : null);
|
|
366000
367054
|
if (!harPosted) {
|
|
366001
367055
|
log.warn("[runner] discovery-har POST failed - final /discovery-result will carry HAR fallback");
|
|
366002
367056
|
}
|
|
366003
|
-
const finalBody = buildFinalBody(discoveryResult, void 0, discoveryAuditWarning || void 0);
|
|
367057
|
+
const finalBody = buildFinalBody(discoveryResult, void 0, discoveryAuditWarning || void 0, auditDroppedCount);
|
|
366004
367058
|
let postOk = false;
|
|
366005
367059
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
366006
367060
|
const timeoutMs = attempt === 0 ? 12e4 : 3e4;
|
|
@@ -366034,14 +367088,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366034
367088
|
const msg = err instanceof Error ? err.message : String(err);
|
|
366035
367089
|
log.fail(`Discovery FAILED: ${msg}`);
|
|
366036
367090
|
let drainMsg = "";
|
|
367091
|
+
let auditDroppedCount = 0;
|
|
366037
367092
|
try {
|
|
366038
|
-
await drainAudit();
|
|
367093
|
+
auditDroppedCount = (await drainAudit()).dropped;
|
|
366039
367094
|
} catch (drainErr) {
|
|
367095
|
+
auditDroppedCount = auditDroppedFromDrainFailure(drainErr);
|
|
366040
367096
|
drainMsg = `
|
|
366041
367097
|
[audit] ${drainErr instanceof Error ? drainErr.message : String(drainErr)}`;
|
|
366042
367098
|
console.warn(`[runner]${drainMsg}`);
|
|
366043
367099
|
}
|
|
366044
|
-
const errorBody = discoveryResult ? buildFinalBody(discoveryResult, msg, drainMsg || void 0) : prepareDiscoveryResultForPost({
|
|
367100
|
+
const errorBody = discoveryResult ? buildFinalBody(discoveryResult, msg, drainMsg || void 0, auditDroppedCount) : prepareDiscoveryResultForPost({
|
|
366045
367101
|
...explorePosted ? {} : { collectedPages: [], collectedTransitions: [] },
|
|
366046
367102
|
tests: [],
|
|
366047
367103
|
logs: `Fatal error: ${msg}${drainMsg}`,
|
|
@@ -366049,6 +367105,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366049
367105
|
summary: `Fatal error: ${msg}`,
|
|
366050
367106
|
exploreStats: { pagesDiscovered: 0, pagesVisited: 0, elementsFound: 0, transitionsFound: 0 },
|
|
366051
367107
|
duration: (Date.now() - startTime) / 1e3,
|
|
367108
|
+
auditDroppedCount,
|
|
366052
367109
|
mode: ctx.mode,
|
|
366053
367110
|
error: msg,
|
|
366054
367111
|
streamedTestCount: streamedTestKeys.size,
|
|
@@ -366414,6 +367471,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366414
367471
|
status: typeof c.status === "number" ? c.status : void 0
|
|
366415
367472
|
})) : void 0;
|
|
366416
367473
|
const uiPathSucceeded = Array.isArray(reproduce.stepResults) && reproduce.stepResults.filter((s) => s.status === "passed").length >= Math.max(1, reproduce.stepResults.length - 1);
|
|
367474
|
+
const failedStepOrder = Array.isArray(reproduce.stepResults) ? reproduce.stepResults.find((s) => s.status === "failed")?.stepOrder : void 0;
|
|
366417
367475
|
const heal = await (0, import_runner_core14.executeMobileTestWithHealing)(
|
|
366418
367476
|
mobileSteps,
|
|
366419
367477
|
healTarget,
|
|
@@ -366425,6 +367483,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366425
367483
|
testName: run2.testName,
|
|
366426
367484
|
tags: tagList,
|
|
366427
367485
|
failureCount,
|
|
367486
|
+
failedStepOrder,
|
|
366428
367487
|
apiCalls: reproduceApiCalls,
|
|
366429
367488
|
uiPathSucceeded
|
|
366430
367489
|
},
|