@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.mjs
CHANGED
|
@@ -2043,7 +2043,8 @@ var init_project_key = __esm({
|
|
|
2043
2043
|
"reviews",
|
|
2044
2044
|
"admin",
|
|
2045
2045
|
"login",
|
|
2046
|
-
"admin-view"
|
|
2046
|
+
"admin-view",
|
|
2047
|
+
"mcp"
|
|
2047
2048
|
]);
|
|
2048
2049
|
PROJECT_KEY_MIN_LENGTH = 2;
|
|
2049
2050
|
PROJECT_KEY_MAX_LENGTH = 12;
|
|
@@ -3134,22 +3135,23 @@ function labelForTarget(target) {
|
|
|
3134
3135
|
return nonEmpty2(target?.locatorBundle?.accessibilityId) ?? nonEmpty2(target?.locatorBundle?.text) ?? nonEmpty2(target?.locatorBundle?.contentDesc) ?? nonEmpty2(target?.primary);
|
|
3135
3136
|
}
|
|
3136
3137
|
function semanticTarget(label, platform3, base2) {
|
|
3137
|
-
const
|
|
3138
|
+
const textXPath = mobileTextXPath(label, platform3);
|
|
3139
|
+
const primary = nonEmpty2(base2?.primary) ?? textXPath;
|
|
3138
3140
|
return {
|
|
3139
3141
|
primary,
|
|
3140
3142
|
fallbacks: uniq([
|
|
3143
|
+
...base2?.fallbacks ?? [],
|
|
3144
|
+
textXPath,
|
|
3141
3145
|
`~${label}`,
|
|
3142
|
-
label
|
|
3143
|
-
base2?.primary,
|
|
3144
|
-
...base2?.fallbacks ?? []
|
|
3146
|
+
label
|
|
3145
3147
|
]).filter((value) => value !== primary),
|
|
3146
3148
|
screenName: base2?.screenName ?? "HealedAssertion",
|
|
3147
3149
|
locatorBundle: {
|
|
3148
|
-
accessibilityId: label,
|
|
3149
|
-
xpath:
|
|
3150
|
-
className: "",
|
|
3151
|
-
text: label,
|
|
3152
|
-
contentDesc: label,
|
|
3150
|
+
accessibilityId: nonEmpty2(base2?.locatorBundle?.accessibilityId) ?? label,
|
|
3151
|
+
xpath: nonEmpty2(base2?.locatorBundle?.xpath) ?? textXPath,
|
|
3152
|
+
className: base2?.locatorBundle?.className ?? "",
|
|
3153
|
+
text: nonEmpty2(base2?.locatorBundle?.text) ?? label,
|
|
3154
|
+
contentDesc: nonEmpty2(base2?.locatorBundle?.contentDesc) ?? label,
|
|
3153
3155
|
...base2?.locatorBundle?.resourceId ? { resourceId: base2.locatorBundle.resourceId } : {}
|
|
3154
3156
|
}
|
|
3155
3157
|
};
|
|
@@ -3327,6 +3329,26 @@ var init_mobile_step_healer = __esm({
|
|
|
3327
3329
|
});
|
|
3328
3330
|
|
|
3329
3331
|
// ../shared/dist/mobile-cross-platform.js
|
|
3332
|
+
function androidActivityPackageOf(owner) {
|
|
3333
|
+
const lastDot = owner.lastIndexOf(".");
|
|
3334
|
+
if (lastDot <= 0)
|
|
3335
|
+
return null;
|
|
3336
|
+
return /^[A-Z]/.test(owner.slice(lastDot + 1)) ? owner.slice(0, lastDot) : null;
|
|
3337
|
+
}
|
|
3338
|
+
function mobileScreenOwnerBelongsToApp(owner, targetAppId) {
|
|
3339
|
+
const ownerId = owner.trim();
|
|
3340
|
+
const appId = targetAppId.trim();
|
|
3341
|
+
if (!ownerId || !appId)
|
|
3342
|
+
return false;
|
|
3343
|
+
if (ownerId === appId)
|
|
3344
|
+
return true;
|
|
3345
|
+
if (ownerId.startsWith(`${appId}.`))
|
|
3346
|
+
return true;
|
|
3347
|
+
const activityPackage = androidActivityPackageOf(ownerId);
|
|
3348
|
+
if (activityPackage && appId.startsWith(`${activityPackage}.`))
|
|
3349
|
+
return true;
|
|
3350
|
+
return false;
|
|
3351
|
+
}
|
|
3330
3352
|
function isMobilePlatform(value) {
|
|
3331
3353
|
return value === "IOS" || value === "ANDROID";
|
|
3332
3354
|
}
|
|
@@ -6579,6 +6601,7 @@ __export(dist_exports, {
|
|
|
6579
6601
|
mobileCredentialPlaceholder: () => mobileCredentialPlaceholder,
|
|
6580
6602
|
mobileLocatorBundleHasIdentity: () => mobileLocatorBundleHasIdentity,
|
|
6581
6603
|
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
6604
|
+
mobileScreenOwnerBelongsToApp: () => mobileScreenOwnerBelongsToApp,
|
|
6582
6605
|
mobileWanderTraceEnabled: () => mobileWanderTraceEnabled,
|
|
6583
6606
|
normalizeControlName: () => normalizeControlName,
|
|
6584
6607
|
normalizeTtsModel: () => normalizeTtsModel,
|
|
@@ -8537,6 +8560,127 @@ var require_scrub_credentials = __commonJS({
|
|
|
8537
8560
|
}
|
|
8538
8561
|
});
|
|
8539
8562
|
|
|
8563
|
+
// ../runner-core/dist/services/google-signin.js
|
|
8564
|
+
var require_google_signin = __commonJS({
|
|
8565
|
+
"../runner-core/dist/services/google-signin.js"(exports) {
|
|
8566
|
+
"use strict";
|
|
8567
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8568
|
+
exports.GOOGLE_SIGNIN_ENV_KEYS = exports.GOOGLE_SIGNIN_PASSWORD_KEY = void 0;
|
|
8569
|
+
exports.loadGoogleSignInConfig = loadGoogleSignInConfig;
|
|
8570
|
+
exports.mergeGoogleSignInSecrets = mergeGoogleSignInSecrets;
|
|
8571
|
+
exports.googleSignInGuidanceOpts = googleSignInGuidanceOpts;
|
|
8572
|
+
exports.googleSignInAllowedEmails = googleSignInAllowedEmails;
|
|
8573
|
+
exports.collectGoogleSignInEnv = collectGoogleSignInEnv;
|
|
8574
|
+
exports.generateTotp = generateTotp;
|
|
8575
|
+
exports.buildGoogleSignInPromptSection = buildGoogleSignInPromptSection;
|
|
8576
|
+
var node_crypto_1 = __require("crypto");
|
|
8577
|
+
exports.GOOGLE_SIGNIN_PASSWORD_KEY = "GOOGLE_SIGNIN_PASSWORD";
|
|
8578
|
+
exports.GOOGLE_SIGNIN_ENV_KEYS = [
|
|
8579
|
+
"GOOGLE_SIGNIN_PASSWORD",
|
|
8580
|
+
"GOOGLE_SIGNIN_EMAIL",
|
|
8581
|
+
"GOOGLE_SIGNIN_TOTP_SECRET"
|
|
8582
|
+
];
|
|
8583
|
+
function trimEnv(env, name) {
|
|
8584
|
+
const value = env[name]?.trim();
|
|
8585
|
+
return value ? value : void 0;
|
|
8586
|
+
}
|
|
8587
|
+
function loadGoogleSignInConfig(env = process.env) {
|
|
8588
|
+
const password = trimEnv(env, "GOOGLE_SIGNIN_PASSWORD");
|
|
8589
|
+
if (!password)
|
|
8590
|
+
return null;
|
|
8591
|
+
const email = (trimEnv(env, "GOOGLE_SIGNIN_EMAIL") ?? trimEnv(env, "VERIFICATION_INBOX_ADDRESS"))?.toLowerCase();
|
|
8592
|
+
if (!email || !email.includes("@"))
|
|
8593
|
+
return null;
|
|
8594
|
+
const totpSecret = trimEnv(env, "GOOGLE_SIGNIN_TOTP_SECRET");
|
|
8595
|
+
return {
|
|
8596
|
+
email,
|
|
8597
|
+
password,
|
|
8598
|
+
passwordKey: exports.GOOGLE_SIGNIN_PASSWORD_KEY,
|
|
8599
|
+
totpSecret,
|
|
8600
|
+
totpAvailable: !!totpSecret
|
|
8601
|
+
};
|
|
8602
|
+
}
|
|
8603
|
+
function mergeGoogleSignInSecrets(credentials, env = process.env) {
|
|
8604
|
+
const cfg = loadGoogleSignInConfig(env);
|
|
8605
|
+
if (!cfg)
|
|
8606
|
+
return credentials;
|
|
8607
|
+
if (credentials[exports.GOOGLE_SIGNIN_PASSWORD_KEY])
|
|
8608
|
+
return credentials;
|
|
8609
|
+
return { ...credentials, [exports.GOOGLE_SIGNIN_PASSWORD_KEY]: cfg.password };
|
|
8610
|
+
}
|
|
8611
|
+
function googleSignInGuidanceOpts(env = process.env) {
|
|
8612
|
+
const cfg = loadGoogleSignInConfig(env);
|
|
8613
|
+
if (!cfg)
|
|
8614
|
+
return void 0;
|
|
8615
|
+
return {
|
|
8616
|
+
googleEmail: cfg.email,
|
|
8617
|
+
googlePasswordKey: cfg.passwordKey,
|
|
8618
|
+
googleTotpAvailable: cfg.totpAvailable
|
|
8619
|
+
};
|
|
8620
|
+
}
|
|
8621
|
+
function googleSignInAllowedEmails(env = process.env) {
|
|
8622
|
+
const cfg = loadGoogleSignInConfig(env);
|
|
8623
|
+
return cfg ? [cfg.email] : [];
|
|
8624
|
+
}
|
|
8625
|
+
function collectGoogleSignInEnv(env = process.env) {
|
|
8626
|
+
const out = {};
|
|
8627
|
+
for (const key of exports.GOOGLE_SIGNIN_ENV_KEYS) {
|
|
8628
|
+
const value = env[key]?.trim();
|
|
8629
|
+
if (value)
|
|
8630
|
+
out[key] = value;
|
|
8631
|
+
}
|
|
8632
|
+
return out;
|
|
8633
|
+
}
|
|
8634
|
+
function generateTotp(secret, atMs = Date.now(), stepSeconds = 30, digits = 6) {
|
|
8635
|
+
const key = decodeBase32(secret);
|
|
8636
|
+
const counter = Math.floor(atMs / 1e3 / stepSeconds);
|
|
8637
|
+
const buf = Buffer.alloc(8);
|
|
8638
|
+
buf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
8639
|
+
buf.writeUInt32BE(counter >>> 0, 4);
|
|
8640
|
+
const hmac = (0, node_crypto_1.createHmac)("sha1", key).update(buf).digest();
|
|
8641
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
8642
|
+
const bin = (hmac[offset] & 127) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3];
|
|
8643
|
+
const otp = bin % 10 ** digits;
|
|
8644
|
+
return String(otp).padStart(digits, "0");
|
|
8645
|
+
}
|
|
8646
|
+
function decodeBase32(input) {
|
|
8647
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
8648
|
+
const cleaned = input.toUpperCase().replace(/[\s=-]+/g, "");
|
|
8649
|
+
let bits = 0;
|
|
8650
|
+
let value = 0;
|
|
8651
|
+
const out = [];
|
|
8652
|
+
for (const ch of cleaned) {
|
|
8653
|
+
const idx = alphabet.indexOf(ch);
|
|
8654
|
+
if (idx < 0)
|
|
8655
|
+
continue;
|
|
8656
|
+
value = value << 5 | idx;
|
|
8657
|
+
bits += 5;
|
|
8658
|
+
if (bits >= 8) {
|
|
8659
|
+
bits -= 8;
|
|
8660
|
+
out.push(value >>> bits & 255);
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
8663
|
+
return Buffer.from(out);
|
|
8664
|
+
}
|
|
8665
|
+
function buildGoogleSignInPromptSection(opts, variant) {
|
|
8666
|
+
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).";
|
|
8667
|
+
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).";
|
|
8668
|
+
const totp = variant === "cli" ? totpCli : totpNative;
|
|
8669
|
+
const fill = variant === "cli" ? `fill_credential with credentialKey \`${opts.googlePasswordKey}\`` : `\`fill_credential\` (credentialKey \`${opts.googlePasswordKey}\`)`;
|
|
8670
|
+
const plainProvision = variant === "cli" ? "`inbox_provision_identity` with `plain: true`" : "`provision_identity` with `plain: true`";
|
|
8671
|
+
return `
|
|
8672
|
+
Google / Gmail SSO fallback (SECOND choice \u2014 email+password signup is FIRST):
|
|
8673
|
+
- 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.
|
|
8674
|
+
- 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).
|
|
8675
|
+
- 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.
|
|
8676
|
+
- Click Next.${totp}
|
|
8677
|
+
- Click Continue / Allow on any consent screen, then return to the app tab.
|
|
8678
|
+
- 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.
|
|
8679
|
+
- 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.`;
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
});
|
|
8683
|
+
|
|
8540
8684
|
// ../runner-core/dist/services/credential-tools.js
|
|
8541
8685
|
var require_credential_tools = __commonJS({
|
|
8542
8686
|
"../runner-core/dist/services/credential-tools.js"(exports) {
|
|
@@ -8555,6 +8699,7 @@ var require_credential_tools = __commonJS({
|
|
|
8555
8699
|
exports.mcpResultIsError = mcpResultIsError;
|
|
8556
8700
|
exports.aggressiveScrubCredentials = aggressiveScrubCredentials;
|
|
8557
8701
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
8702
|
+
var google_signin_js_1 = require_google_signin();
|
|
8558
8703
|
exports.FILL_CREDENTIAL_TOOL = {
|
|
8559
8704
|
type: "function",
|
|
8560
8705
|
function: {
|
|
@@ -8565,7 +8710,7 @@ var require_credential_tools = __commonJS({
|
|
|
8565
8710
|
properties: {
|
|
8566
8711
|
credentialKey: {
|
|
8567
8712
|
type: "string",
|
|
8568
|
-
description: 'The credential variable name (e.g. "TEST_USER_EMAIL", "TEST_USER_PASSWORD", "
|
|
8713
|
+
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.'
|
|
8569
8714
|
},
|
|
8570
8715
|
element: {
|
|
8571
8716
|
type: "string",
|
|
@@ -8588,7 +8733,7 @@ var require_credential_tools = __commonJS({
|
|
|
8588
8733
|
type: "function",
|
|
8589
8734
|
function: {
|
|
8590
8735
|
name: "set_credential",
|
|
8591
|
-
description: 'Persist a NEW value for a stored credential after a test has successfully rotated it (
|
|
8736
|
+
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.',
|
|
8592
8737
|
parameters: {
|
|
8593
8738
|
type: "object",
|
|
8594
8739
|
properties: {
|
|
@@ -8602,7 +8747,7 @@ var require_credential_tools = __commonJS({
|
|
|
8602
8747
|
},
|
|
8603
8748
|
reason: {
|
|
8604
8749
|
type: "string",
|
|
8605
|
-
description: 'Short human-readable explanation of why the credential changed (e.g. "User changed password from settings page"). Surfaced in the notification.'
|
|
8750
|
+
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.'
|
|
8606
8751
|
}
|
|
8607
8752
|
},
|
|
8608
8753
|
required: ["credentialKey", "newValue"]
|
|
@@ -8664,12 +8809,13 @@ SECURITY RULES:
|
|
|
8664
8809
|
- 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".)
|
|
8665
8810
|
- 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.
|
|
8666
8811
|
${opts.canRotate ? `
|
|
8667
|
-
CREDENTIAL ROTATION (
|
|
8668
|
-
-
|
|
8669
|
-
(1)
|
|
8670
|
-
(2)
|
|
8671
|
-
|
|
8672
|
-
-
|
|
8812
|
+
CREDENTIAL ROTATION (required when the STORED user's secret changes \u2014 read carefully):
|
|
8813
|
+
- 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:
|
|
8814
|
+
(1) **In-session change password** \u2014 log in as the stored user, open settings \u2192 Change Password, submit a new password, verify success; OR
|
|
8815
|
+
(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.
|
|
8816
|
+
- 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.
|
|
8817
|
+
- Proof required before calling: success toast / confirmed reset state / re-login with the new value works.
|
|
8818
|
+
- 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.
|
|
8673
8819
|
- DO NOT call \`set_credential\` on a negative test (one that verifies a bad/mismatched password is rejected, or a permission-denied case).
|
|
8674
8820
|
- DO NOT call it before submit + verify \u2014 a partial failure would corrupt the stored credential.
|
|
8675
8821
|
- 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.
|
|
@@ -8830,6 +8976,14 @@ CREDENTIAL ROTATION (rare \u2014 read this carefully):
|
|
|
8830
8976
|
didMutate: false
|
|
8831
8977
|
};
|
|
8832
8978
|
}
|
|
8979
|
+
if (key === google_signin_js_1.GOOGLE_SIGNIN_PASSWORD_KEY) {
|
|
8980
|
+
return {
|
|
8981
|
+
content: JSON.stringify({
|
|
8982
|
+
error: "set_credential: GOOGLE_SIGNIN_PASSWORD is a runner-operator secret and cannot be persisted to project Test Data"
|
|
8983
|
+
}),
|
|
8984
|
+
didMutate: false
|
|
8985
|
+
};
|
|
8986
|
+
}
|
|
8833
8987
|
if (!(key in ctx.credentials)) {
|
|
8834
8988
|
const available = Object.keys(ctx.credentials).filter((k) => ctx.credentials[k]).join(", ") || "(none configured)";
|
|
8835
8989
|
return {
|
|
@@ -74091,7 +74245,7 @@ var require_inbox_tools = __commonJS({
|
|
|
74091
74245
|
"../runner-core/dist/services/inbox-tools.js"(exports) {
|
|
74092
74246
|
"use strict";
|
|
74093
74247
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74094
|
-
exports.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = exports.INBOX_TOOL_NAMES = exports.INBOX_TOOLS = exports.OPEN_VERIFICATION_LINK_TOOL = exports.GET_VERIFICATION_LINK_TOOL = exports.EXTRACT_VERIFICATION_CODE_TOOL = exports.READ_VERIFICATION_EMAIL_TOOL = exports.WAIT_FOR_VERIFICATION_TOOL = exports.CHECK_INBOX_TOOL = exports.PROVISION_IDENTITY_TOOL = void 0;
|
|
74248
|
+
exports.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = exports.INBOX_TOOL_NAMES = exports.INBOX_TOOLS = exports.PROVISION_GOOGLE_TOTP_TOOL = exports.OPEN_VERIFICATION_LINK_TOOL = exports.GET_VERIFICATION_LINK_TOOL = exports.EXTRACT_VERIFICATION_CODE_TOOL = exports.READ_VERIFICATION_EMAIL_TOOL = exports.WAIT_FOR_VERIFICATION_TOOL = exports.CHECK_INBOX_TOOL = exports.PROVISION_IDENTITY_TOOL = void 0;
|
|
74095
74249
|
exports.createInboxRunState = createInboxRunState;
|
|
74096
74250
|
exports.dispatchInboxTool = dispatchInboxTool;
|
|
74097
74251
|
exports.isPublicHttpUrl = isPublicHttpUrl;
|
|
@@ -74106,6 +74260,7 @@ var require_inbox_tools = __commonJS({
|
|
|
74106
74260
|
exports.clearThrowawayRegisterSeed = clearThrowawayRegisterSeed;
|
|
74107
74261
|
exports.buildInboxAgentsGuidance = buildInboxAgentsGuidance;
|
|
74108
74262
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
74263
|
+
var google_signin_js_1 = require_google_signin();
|
|
74109
74264
|
function createInboxRunState(config, opts = {}) {
|
|
74110
74265
|
const baseEmail = config.address;
|
|
74111
74266
|
const tag = opts.tag !== void 0 ? opts.tag : opts.tagSeed ? (0, verification_inbox_js_1.deriveProjectInboxTag)(opts.tagSeed, opts.projectScope) : null;
|
|
@@ -74227,6 +74382,14 @@ var require_inbox_tools = __commonJS({
|
|
|
74227
74382
|
}
|
|
74228
74383
|
}
|
|
74229
74384
|
};
|
|
74385
|
+
exports.PROVISION_GOOGLE_TOTP_TOOL = {
|
|
74386
|
+
type: "function",
|
|
74387
|
+
function: {
|
|
74388
|
+
name: "provision_google_totp",
|
|
74389
|
+
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.",
|
|
74390
|
+
parameters: { type: "object", properties: {} }
|
|
74391
|
+
}
|
|
74392
|
+
};
|
|
74230
74393
|
exports.INBOX_TOOLS = [
|
|
74231
74394
|
exports.PROVISION_IDENTITY_TOOL,
|
|
74232
74395
|
exports.CHECK_INBOX_TOOL,
|
|
@@ -74234,7 +74397,8 @@ var require_inbox_tools = __commonJS({
|
|
|
74234
74397
|
exports.READ_VERIFICATION_EMAIL_TOOL,
|
|
74235
74398
|
exports.EXTRACT_VERIFICATION_CODE_TOOL,
|
|
74236
74399
|
exports.GET_VERIFICATION_LINK_TOOL,
|
|
74237
|
-
exports.OPEN_VERIFICATION_LINK_TOOL
|
|
74400
|
+
exports.OPEN_VERIFICATION_LINK_TOOL,
|
|
74401
|
+
exports.PROVISION_GOOGLE_TOTP_TOOL
|
|
74238
74402
|
];
|
|
74239
74403
|
exports.INBOX_TOOL_NAMES = new Set(exports.INBOX_TOOLS.map((t) => t.function.name));
|
|
74240
74404
|
var DEFAULT_MAX_RESULT_CHARS = 8e3;
|
|
@@ -74246,7 +74410,7 @@ var require_inbox_tools = __commonJS({
|
|
|
74246
74410
|
}
|
|
74247
74411
|
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.";
|
|
74248
74412
|
async function dispatchInboxTool(toolName, toolArgs, ctx) {
|
|
74249
|
-
if (ctx.state.authDead) {
|
|
74413
|
+
if (ctx.state.authDead && toolName !== "provision_google_totp") {
|
|
74250
74414
|
return err(`${toolName} skipped: ${INBOX_AUTH_DEAD_GUIDANCE}`);
|
|
74251
74415
|
}
|
|
74252
74416
|
try {
|
|
@@ -74265,6 +74429,8 @@ var require_inbox_tools = __commonJS({
|
|
|
74265
74429
|
return await handleGetLink(toolArgs, ctx);
|
|
74266
74430
|
case "open_verification_link":
|
|
74267
74431
|
return await handleOpenLink(toolArgs, ctx);
|
|
74432
|
+
case "provision_google_totp":
|
|
74433
|
+
return handleProvisionGoogleTotp();
|
|
74268
74434
|
default:
|
|
74269
74435
|
return err(`inbox dispatch: unknown tool "${toolName}"`);
|
|
74270
74436
|
}
|
|
@@ -74440,6 +74606,20 @@ var require_inbox_tools = __commonJS({
|
|
|
74440
74606
|
didMutate: !failed
|
|
74441
74607
|
};
|
|
74442
74608
|
}
|
|
74609
|
+
function handleProvisionGoogleTotp(env = process.env) {
|
|
74610
|
+
const cfg = (0, google_signin_js_1.loadGoogleSignInConfig)(env);
|
|
74611
|
+
if (!cfg) {
|
|
74612
|
+
return err("Google SSO is not configured on this runner (GOOGLE_SIGNIN_PASSWORD unset). Prefer email-field signup, or report Google SSO as BLOCKED.");
|
|
74613
|
+
}
|
|
74614
|
+
if (!cfg.totpSecret) {
|
|
74615
|
+
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).");
|
|
74616
|
+
}
|
|
74617
|
+
return ok({
|
|
74618
|
+
code: (0, google_signin_js_1.generateTotp)(cfg.totpSecret),
|
|
74619
|
+
digits: 6,
|
|
74620
|
+
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."
|
|
74621
|
+
});
|
|
74622
|
+
}
|
|
74443
74623
|
function requireProvisioned(ctx) {
|
|
74444
74624
|
if (ctx.state.provisioned)
|
|
74445
74625
|
return null;
|
|
@@ -74574,7 +74754,7 @@ var require_inbox_tools = __commonJS({
|
|
|
74574
74754
|
${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.
|
|
74575
74755
|
|
|
74576
74756
|
Steps:
|
|
74577
|
-
1. Navigate to the Sign Up / Register page (click the link in the UI).
|
|
74757
|
+
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.
|
|
74578
74758
|
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.
|
|
74579
74759
|
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.
|
|
74580
74760
|
- 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.
|
|
@@ -74592,6 +74772,11 @@ Rules:
|
|
|
74592
74772
|
- 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.
|
|
74593
74773
|
- 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.
|
|
74594
74774
|
- 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).
|
|
74775
|
+
${opts.googleEmail && opts.googlePasswordKey ? (0, google_signin_js_1.buildGoogleSignInPromptSection)({
|
|
74776
|
+
googleEmail: opts.googleEmail,
|
|
74777
|
+
googlePasswordKey: opts.googlePasswordKey,
|
|
74778
|
+
googleTotpAvailable: opts.googleTotpAvailable
|
|
74779
|
+
}, "native") : ""}
|
|
74595
74780
|
`;
|
|
74596
74781
|
}
|
|
74597
74782
|
exports.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK = `## EMAIL VERIFICATION UNAVAILABLE \u2014 FINAL OVERRIDE (supersedes any instruction above)
|
|
@@ -74746,11 +74931,12 @@ This project asked to REGISTER a new account, but this runner CANNOT receive ema
|
|
|
74746
74931
|
}
|
|
74747
74932
|
return false;
|
|
74748
74933
|
}
|
|
74749
|
-
function buildInboxAgentsGuidance(email, passwordKey) {
|
|
74934
|
+
function buildInboxAgentsGuidance(email, passwordKey, google) {
|
|
74935
|
+
const googleBlock = google ? (0, google_signin_js_1.buildGoogleSignInPromptSection)(google, "cli") : "";
|
|
74750
74936
|
return `## Account registration & email verification (ENABLED)
|
|
74751
74937
|
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.
|
|
74752
74938
|
|
|
74753
|
-
1. Go to the Sign Up / Register page.
|
|
74939
|
+
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.
|
|
74754
74940
|
2. Call \`inbox_provision_identity\` \u2192 it returns the email to register with (\`${email}\`). Type that email into the email field.
|
|
74755
74941
|
- 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.
|
|
74756
74942
|
3. Fill the password with the \`${passwordKey}\` credential (use fill_credential). Submit the signup form.
|
|
@@ -74761,6 +74947,7 @@ This run registers a new account using our ONE real, monitored inbox (the addres
|
|
|
74761
74947
|
- 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.
|
|
74762
74948
|
- Otherwise call \`inbox_read_email\` to read the body.
|
|
74763
74949
|
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.
|
|
74950
|
+
${googleBlock}
|
|
74764
74951
|
`;
|
|
74765
74952
|
}
|
|
74766
74953
|
}
|
|
@@ -75744,7 +75931,7 @@ Rules:
|
|
|
75744
75931
|
- Register ONCE per run. If signup says the account ALREADY exists, log in with that email + the \`${opts.passwordKey}\` password instead of registering again.
|
|
75745
75932
|
- Only call the verification tools AFTER submitting the signup form; pass the app as \`senderMatch\` (the inbox is shared).`;
|
|
75746
75933
|
}
|
|
75747
|
-
function buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, appBrief, targetAppId, existingScreens, incremental) {
|
|
75934
|
+
function buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, appBrief, targetAppId, existingScreens, incremental, knowledgeContext) {
|
|
75748
75935
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
75749
75936
|
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
75750
75937
|
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.
|
|
@@ -75789,12 +75976,18 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
|
|
|
75789
75976
|
## Application Brief
|
|
75790
75977
|
${appBrief}
|
|
75791
75978
|
Use this to prioritize which screens and flows to cover.` : "";
|
|
75979
|
+
const facts = knowledgeContext?.trim();
|
|
75980
|
+
const knowledgeBlock = facts ? `
|
|
75981
|
+
|
|
75982
|
+
## Known Facts (project documents \u2014 optional)
|
|
75983
|
+
${facts.slice(0, 4e3)}
|
|
75984
|
+
Prioritize exploring surfaces related to these facts when visible. If absent, explore solely from live UI. Never invent screens from facts alone.` : "";
|
|
75792
75985
|
const existingScreensBlock = mode === "survey" && incremental && existingScreens && existingScreens.length > 0 ? `
|
|
75793
75986
|
|
|
75794
75987
|
## ALREADY DISCOVERED SCREENS (skip these; find new)
|
|
75795
75988
|
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.
|
|
75796
75989
|
${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.name}"` : ""}${s.screenType ? ` [${s.screenType}]` : ""}`).join("\n")}` : "";
|
|
75797
|
-
return `${mode === "survey" ? surveyBody : deepBody}${briefBlock}${existingScreensBlock}`;
|
|
75990
|
+
return `${mode === "survey" ? surveyBody : deepBody}${briefBlock}${knowledgeBlock}${existingScreensBlock}`;
|
|
75798
75991
|
}
|
|
75799
75992
|
function buildKickoffMessage(mode) {
|
|
75800
75993
|
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.";
|
|
@@ -76194,7 +76387,7 @@ ${existingScreens.slice(0, 100).map((s) => `- ${s.screenId}${s.name ? ` "${s.nam
|
|
|
76194
76387
|
if (authTracker.required) {
|
|
76195
76388
|
log2(`Authenticated survey completion gate enabled (${ctx.target?.resetPolicy ?? "AUTO"}): the run must reach a structurally observed product surface before planning.`);
|
|
76196
76389
|
}
|
|
76197
|
-
const systemPrompt = buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, ctx.appBrief, targetAppId, existingScreens, ctx.incremental) + (!registerRequested ? buildMobileCredentialPromptBlock(credentialSecrets) : "") + (inboxEnabled && inboxState ? buildMobileInboxPromptBlock({
|
|
76390
|
+
const systemPrompt = buildSystemPrompt(mode, platform3, enableRecordJourney, maxIterations, ctx.appBrief, targetAppId, existingScreens, ctx.incremental, ctx.knowledgeContext) + (!registerRequested ? buildMobileCredentialPromptBlock(credentialSecrets) : "") + (inboxEnabled && inboxState ? buildMobileInboxPromptBlock({
|
|
76198
76391
|
email: inboxState.email,
|
|
76199
76392
|
passwordKey: inboxState.passwordKey ?? "TEST_USER_PASSWORD",
|
|
76200
76393
|
context: mode
|
|
@@ -80362,8 +80555,10 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80362
80555
|
exports.inferScenarioKind = inferScenarioKind;
|
|
80363
80556
|
exports.inferScenarioKinds = inferScenarioKinds;
|
|
80364
80557
|
exports.isStrongOracleKind = isStrongOracleKind;
|
|
80558
|
+
exports.kindRequiresStateDelta = kindRequiresStateDelta;
|
|
80365
80559
|
exports.formatRequiredScenarioKindsPrompt = formatRequiredScenarioKindsPrompt;
|
|
80366
80560
|
exports.formatCoverageGapLog = formatCoverageGapLog;
|
|
80561
|
+
exports.synthesizeScenariosForMissingKinds = synthesizeScenariosForMissingKinds;
|
|
80367
80562
|
exports.ALL_SCENARIO_KINDS = [
|
|
80368
80563
|
"smoke_nav",
|
|
80369
80564
|
"happy_submit",
|
|
@@ -80374,7 +80569,9 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80374
80569
|
"auth_session",
|
|
80375
80570
|
"navigation_integrity",
|
|
80376
80571
|
"permission_system",
|
|
80377
|
-
"double_submit"
|
|
80572
|
+
"double_submit",
|
|
80573
|
+
"search_filter",
|
|
80574
|
+
"list_refresh"
|
|
80378
80575
|
];
|
|
80379
80576
|
var DELETE_RE = /\b(delete|remove|archive|trash)\b/i;
|
|
80380
80577
|
var EMAIL_RE = /\b(e-?mail|email address)\b/i;
|
|
@@ -80386,7 +80583,12 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80386
80583
|
var BADGE_LABEL_RE = /\b(badge|count|open invoices|unread|notifications?)\b/i;
|
|
80387
80584
|
var PERMISSION_RE = /\b(camera|microphone|location|photos?|permission|allow access|not now)\b/i;
|
|
80388
80585
|
var SETTINGS_RE = /\b(settings|preferences|profile|account)\b/i;
|
|
80389
|
-
var TAB_RE = /\b(tab|home|
|
|
80586
|
+
var TAB_RE = /\b(tab|home|inbox|feed|dashboard)\b/i;
|
|
80587
|
+
var SEARCH_RE = /\b(search|filter|find|query)\b/i;
|
|
80588
|
+
var EMPTY_STATE_RE = /\b(no items|nothing here|empty|get started|add first|create first|no results)\b/i;
|
|
80589
|
+
var EMPTY_CTA_RE = /\b(add first|create first|get started|add item|new item)\b/i;
|
|
80590
|
+
var REFRESH_RE = /\b(pull to refresh|pull-to-refresh|refresh|updated:)\b/i;
|
|
80591
|
+
var FAVORITE_RE = /\b(favorite|favourite|heart|bookmark|star|like)\b/i;
|
|
80390
80592
|
function elementText(el) {
|
|
80391
80593
|
return [
|
|
80392
80594
|
el.label ?? "",
|
|
@@ -80424,6 +80626,10 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80424
80626
|
let hasPrimarySubmit = false;
|
|
80425
80627
|
let hasTabOrDrawerNav = false;
|
|
80426
80628
|
let hasPermissionCopy = false;
|
|
80629
|
+
let hasSearchField = false;
|
|
80630
|
+
let hasEmptyStateCta = false;
|
|
80631
|
+
let hasPullToRefresh = false;
|
|
80632
|
+
let hasFavoriteToggle = false;
|
|
80427
80633
|
for (const state2 of screens) {
|
|
80428
80634
|
const labels = state2.elements.map(elementText);
|
|
80429
80635
|
const blob = [state2.screenName ?? "", state2.screenType ?? "", ...state2.observations ?? [], ...labels].join(" ");
|
|
@@ -80455,6 +80661,17 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80455
80661
|
}
|
|
80456
80662
|
if (PERMISSION_RE.test(blob) || state2.screenType === "PERMISSION")
|
|
80457
80663
|
hasPermissionCopy = true;
|
|
80664
|
+
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 ?? ""))) {
|
|
80665
|
+
hasSearchField = true;
|
|
80666
|
+
}
|
|
80667
|
+
if (EMPTY_STATE_RE.test(blob) && (EMPTY_CTA_RE.test(blob) || labels.some((t) => EMPTY_CTA_RE.test(t)))) {
|
|
80668
|
+
hasEmptyStateCta = true;
|
|
80669
|
+
}
|
|
80670
|
+
if (REFRESH_RE.test(blob))
|
|
80671
|
+
hasPullToRefresh = true;
|
|
80672
|
+
if (FAVORITE_RE.test(blob) || state2.elements.some((el) => FAVORITE_RE.test(elementText(el)) && el.actionable !== false)) {
|
|
80673
|
+
hasFavoriteToggle = true;
|
|
80674
|
+
}
|
|
80458
80675
|
}
|
|
80459
80676
|
return {
|
|
80460
80677
|
hasListWithRowActions,
|
|
@@ -80466,7 +80683,11 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80466
80683
|
hasDestructiveConfirm,
|
|
80467
80684
|
hasPrimarySubmit,
|
|
80468
80685
|
hasTabOrDrawerNav,
|
|
80469
|
-
hasPermissionCopy
|
|
80686
|
+
hasPermissionCopy,
|
|
80687
|
+
hasSearchField,
|
|
80688
|
+
hasEmptyStateCta,
|
|
80689
|
+
hasPullToRefresh,
|
|
80690
|
+
hasFavoriteToggle
|
|
80470
80691
|
};
|
|
80471
80692
|
}
|
|
80472
80693
|
function requiredScenarioKinds(signals) {
|
|
@@ -80483,13 +80704,13 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80483
80704
|
if (signals.hasNumericBadge) {
|
|
80484
80705
|
kinds.add("count_delta");
|
|
80485
80706
|
}
|
|
80486
|
-
if (signals.hasSettingsToggles) {
|
|
80707
|
+
if (signals.hasSettingsToggles || signals.hasFavoriteToggle) {
|
|
80487
80708
|
kinds.add("persistence_roundtrip");
|
|
80488
80709
|
}
|
|
80489
80710
|
if (signals.hasSignOut) {
|
|
80490
80711
|
kinds.add("auth_session");
|
|
80491
80712
|
}
|
|
80492
|
-
if (signals.hasTabOrDrawerNav) {
|
|
80713
|
+
if (signals.hasTabOrDrawerNav || signals.hasEmptyStateCta) {
|
|
80493
80714
|
kinds.add("navigation_integrity");
|
|
80494
80715
|
}
|
|
80495
80716
|
if (signals.hasPermissionCopy) {
|
|
@@ -80498,6 +80719,12 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80498
80719
|
if (signals.hasPrimarySubmit && signals.hasFormFields) {
|
|
80499
80720
|
kinds.add("double_submit");
|
|
80500
80721
|
}
|
|
80722
|
+
if (signals.hasSearchField) {
|
|
80723
|
+
kinds.add("search_filter");
|
|
80724
|
+
}
|
|
80725
|
+
if (signals.hasPullToRefresh) {
|
|
80726
|
+
kinds.add("list_refresh");
|
|
80727
|
+
}
|
|
80501
80728
|
return exports.ALL_SCENARIO_KINDS.filter((k) => kinds.has(k));
|
|
80502
80729
|
}
|
|
80503
80730
|
function coverageGaps(signals, plannedKinds) {
|
|
@@ -80512,6 +80739,12 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80512
80739
|
if (/\b(delete|remove|wrong row|entity|archive)\b/.test(text) && /\b(list|row|item|record)\b/.test(text)) {
|
|
80513
80740
|
return "crud_identity";
|
|
80514
80741
|
}
|
|
80742
|
+
if (/\b(pull.?to.?refresh|refresh list|list refresh|after refresh)\b/.test(text)) {
|
|
80743
|
+
return "list_refresh";
|
|
80744
|
+
}
|
|
80745
|
+
if (/\b(search|filter results|query)\b/.test(text)) {
|
|
80746
|
+
return "search_filter";
|
|
80747
|
+
}
|
|
80515
80748
|
if (/\b(badge|count|unread|open invoices|\+\d|increment|decrement)\b/.test(text)) {
|
|
80516
80749
|
return "count_delta";
|
|
80517
80750
|
}
|
|
@@ -80521,7 +80754,7 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80521
80754
|
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)) {
|
|
80522
80755
|
return "auth_session";
|
|
80523
80756
|
}
|
|
80524
|
-
if (/\b(tab|navigate|navigation|back|deep link|empty.?state)\b/.test(text)) {
|
|
80757
|
+
if (/\b(tab|navigate|navigation|back|deep link|empty.?state|empty cta|dead route)\b/.test(text)) {
|
|
80525
80758
|
return "navigation_integrity";
|
|
80526
80759
|
}
|
|
80527
80760
|
if (/\b(permission|camera|microphone|location|allow)\b/.test(text)) {
|
|
@@ -80544,6 +80777,9 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80544
80777
|
function isStrongOracleKind(kind) {
|
|
80545
80778
|
return kind !== "smoke_nav" && kind !== "navigation_integrity";
|
|
80546
80779
|
}
|
|
80780
|
+
function kindRequiresStateDelta(kind) {
|
|
80781
|
+
return kind === "crud_identity" || kind === "count_delta" || kind === "persistence_roundtrip" || kind === "double_submit" || kind === "list_refresh" || kind === "search_filter";
|
|
80782
|
+
}
|
|
80547
80783
|
function formatRequiredScenarioKindsPrompt(signals) {
|
|
80548
80784
|
const required = requiredScenarioKinds(signals);
|
|
80549
80785
|
if (required.length === 0)
|
|
@@ -80570,17 +80806,280 @@ var require_scenario_taxonomy = __commonJS({
|
|
|
80570
80806
|
return "- permission_system: permission prompt or settings path behaves honestly (allow/deny/settings)";
|
|
80571
80807
|
case "double_submit":
|
|
80572
80808
|
return "- double_submit: single user intent must not double-create (assert count +1 not +2, or single confirmation)";
|
|
80809
|
+
case "search_filter":
|
|
80810
|
+
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";
|
|
80811
|
+
case "list_refresh":
|
|
80812
|
+
return "- list_refresh: capture list ids/count, pull-to-refresh or tap Refresh, assert list still coherent (not permanently wiped) OR honest empty + recoverable";
|
|
80573
80813
|
default:
|
|
80574
80814
|
return `- ${kind}`;
|
|
80575
80815
|
}
|
|
80576
80816
|
});
|
|
80577
|
-
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");
|
|
80817
|
+
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");
|
|
80578
80818
|
}
|
|
80579
80819
|
function formatCoverageGapLog(gaps) {
|
|
80580
80820
|
if (gaps.length === 0)
|
|
80581
80821
|
return "Scenario taxonomy: all required kinds covered by plan";
|
|
80582
80822
|
return `Scenario taxonomy coverage gaps (required but not planned): ${gaps.join(", ")}`;
|
|
80583
80823
|
}
|
|
80824
|
+
function synthesizeScenariosForMissingKinds(input) {
|
|
80825
|
+
const feature = input.featureName.trim() || "Feature";
|
|
80826
|
+
const used = input.existingNames ?? /* @__PURE__ */ new Set();
|
|
80827
|
+
const maxNew = Math.max(1, input.maxNew ?? 6);
|
|
80828
|
+
const pages = input.targetPages.length > 0 ? input.targetPages : [input.entryRoute];
|
|
80829
|
+
const entry = input.entryRoute || pages[0] || "";
|
|
80830
|
+
const out = [];
|
|
80831
|
+
for (const kind of input.gaps) {
|
|
80832
|
+
if (out.length >= maxNew)
|
|
80833
|
+
break;
|
|
80834
|
+
if (kind === "smoke_nav")
|
|
80835
|
+
continue;
|
|
80836
|
+
const draft = scenarioStubForKind(kind, feature, entry, pages);
|
|
80837
|
+
if (!draft)
|
|
80838
|
+
continue;
|
|
80839
|
+
let name = draft.name;
|
|
80840
|
+
let n = 2;
|
|
80841
|
+
while (used.has(name.toLowerCase())) {
|
|
80842
|
+
name = `${draft.name} (${n})`;
|
|
80843
|
+
n += 1;
|
|
80844
|
+
if (n > 20)
|
|
80845
|
+
break;
|
|
80846
|
+
}
|
|
80847
|
+
if (used.has(name.toLowerCase()))
|
|
80848
|
+
continue;
|
|
80849
|
+
used.add(name.toLowerCase());
|
|
80850
|
+
out.push({ ...draft, name });
|
|
80851
|
+
}
|
|
80852
|
+
return out;
|
|
80853
|
+
}
|
|
80854
|
+
function scenarioStubForKind(kind, feature, entryRoute, targetPages) {
|
|
80855
|
+
const base2 = {
|
|
80856
|
+
featureGroup: feature,
|
|
80857
|
+
entryRoute,
|
|
80858
|
+
startFromLanding: true,
|
|
80859
|
+
targetPages: [...targetPages],
|
|
80860
|
+
evidence: {
|
|
80861
|
+
observedLocators: [],
|
|
80862
|
+
observedTransitions: []
|
|
80863
|
+
},
|
|
80864
|
+
prerequisites: [],
|
|
80865
|
+
isAISuggested: false
|
|
80866
|
+
};
|
|
80867
|
+
const step = (action, target) => ({ action, target });
|
|
80868
|
+
const outcome = (type, description) => ({ type, description, reliability: "medium" });
|
|
80869
|
+
switch (kind) {
|
|
80870
|
+
case "crud_identity":
|
|
80871
|
+
return {
|
|
80872
|
+
...base2,
|
|
80873
|
+
name: `${feature} - Delete entity proves identity removed`,
|
|
80874
|
+
type: "HAPPY_PATH",
|
|
80875
|
+
priority: 1,
|
|
80876
|
+
variant: "happy",
|
|
80877
|
+
goal: "Capture list/entity identity, delete or remove that entity, prove the identity set changed (not merely that the list screen is still visible)",
|
|
80878
|
+
steps: [
|
|
80879
|
+
step("Launch the app and sign in if the product requires a session", entryRoute),
|
|
80880
|
+
step(`Navigate to the ${feature} list or collection screen`),
|
|
80881
|
+
step("Capture visible row/entity identifiers into a state bag before mutation"),
|
|
80882
|
+
step("Perform delete/remove/archive on one specific observed entity"),
|
|
80883
|
+
step("Assert state delta: that entity identity is gone or the wrong neighbor was not removed")
|
|
80884
|
+
],
|
|
80885
|
+
expectedOutcomes: [
|
|
80886
|
+
outcome("element-hidden", "Target entity identity no longer present in the list"),
|
|
80887
|
+
outcome("element-visible", "List or collection still reachable after mutation")
|
|
80888
|
+
]
|
|
80889
|
+
};
|
|
80890
|
+
case "count_delta":
|
|
80891
|
+
return {
|
|
80892
|
+
...base2,
|
|
80893
|
+
name: `${feature} - Count or badge changes after action`,
|
|
80894
|
+
type: "HAPPY_PATH",
|
|
80895
|
+
priority: 1,
|
|
80896
|
+
variant: "happy",
|
|
80897
|
+
goal: "Capture a numeric badge or count, perform a product action, assert the number changed as expected",
|
|
80898
|
+
steps: [
|
|
80899
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80900
|
+
step(`Navigate to the ${feature} surface showing a count or badge`),
|
|
80901
|
+
step("Capture the numeric count/badge into a state bag"),
|
|
80902
|
+
step("Perform an action that should change the count (add, remove, mark read, complete)"),
|
|
80903
|
+
step("Assert state delta: count increased or decreased as expected")
|
|
80904
|
+
],
|
|
80905
|
+
expectedOutcomes: [
|
|
80906
|
+
outcome("element-visible", "Count or badge reflects the post-action value")
|
|
80907
|
+
]
|
|
80908
|
+
};
|
|
80909
|
+
case "persistence_roundtrip":
|
|
80910
|
+
return {
|
|
80911
|
+
...base2,
|
|
80912
|
+
name: `${feature} - Change persists after leave and return`,
|
|
80913
|
+
type: "HAPPY_PATH",
|
|
80914
|
+
priority: 1,
|
|
80915
|
+
variant: "happy",
|
|
80916
|
+
goal: "Change a setting, toggle, or favorite; leave the screen; return; prove the value persisted or honestly reset",
|
|
80917
|
+
steps: [
|
|
80918
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80919
|
+
step(`Navigate to the ${feature} settings or toggle surface`),
|
|
80920
|
+
step("Capture current field/toggle/favorite state"),
|
|
80921
|
+
step("Change a durable control (toggle, text field, favorite) and save if required"),
|
|
80922
|
+
step("Navigate away to another tab or parent screen"),
|
|
80923
|
+
step("Return to the same surface"),
|
|
80924
|
+
step("Assert state delta: value persisted (or product honestly shows it did not)")
|
|
80925
|
+
],
|
|
80926
|
+
expectedOutcomes: [
|
|
80927
|
+
outcome("element-visible", "Changed value is still reflected after round-trip")
|
|
80928
|
+
]
|
|
80929
|
+
};
|
|
80930
|
+
case "negative_validation":
|
|
80931
|
+
return {
|
|
80932
|
+
...base2,
|
|
80933
|
+
name: `${feature} - Invalid or empty input is rejected`,
|
|
80934
|
+
type: "EDGE_CASE",
|
|
80935
|
+
priority: 2,
|
|
80936
|
+
variant: "validation",
|
|
80937
|
+
goal: "Submit empty or malformed input and assert error feedback or blocked submit \u2014 never invent success",
|
|
80938
|
+
steps: [
|
|
80939
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80940
|
+
step(`Navigate to the ${feature} form`),
|
|
80941
|
+
step("Leave required fields empty or type structurally invalid input (e.g. email-like field without a domain)"),
|
|
80942
|
+
step("Attempt submit"),
|
|
80943
|
+
step("Assert validation error is visible OR submit remains blocked on the form")
|
|
80944
|
+
],
|
|
80945
|
+
expectedOutcomes: [
|
|
80946
|
+
outcome("element-visible", "Validation error or blocked submit state is visible")
|
|
80947
|
+
]
|
|
80948
|
+
};
|
|
80949
|
+
case "happy_submit":
|
|
80950
|
+
return {
|
|
80951
|
+
...base2,
|
|
80952
|
+
name: `${feature} - Happy path durable submit`,
|
|
80953
|
+
type: "HAPPY_PATH",
|
|
80954
|
+
priority: 1,
|
|
80955
|
+
variant: "happy",
|
|
80956
|
+
goal: "Complete the primary form/action and assert a durable product result (new row, saved state, confirmation that outlives a toast)",
|
|
80957
|
+
steps: [
|
|
80958
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80959
|
+
step(`Navigate to the ${feature} form or primary action`),
|
|
80960
|
+
step("Fill required fields with run-scoped values"),
|
|
80961
|
+
step("Submit or save"),
|
|
80962
|
+
step("Assert durable outcome (new row, success state, or persisted values)")
|
|
80963
|
+
],
|
|
80964
|
+
expectedOutcomes: [
|
|
80965
|
+
outcome("element-visible", "Durable success result is visible")
|
|
80966
|
+
]
|
|
80967
|
+
};
|
|
80968
|
+
case "auth_session":
|
|
80969
|
+
return {
|
|
80970
|
+
...base2,
|
|
80971
|
+
name: `${feature} - Session proof after sign-in or sign-out`,
|
|
80972
|
+
type: "HAPPY_PATH",
|
|
80973
|
+
priority: 1,
|
|
80974
|
+
variant: "happy",
|
|
80975
|
+
goal: "Prove authenticated session (profile/sign-out) or that sign-out leaves only logged-out chrome",
|
|
80976
|
+
steps: [
|
|
80977
|
+
step("Launch the app", entryRoute),
|
|
80978
|
+
step("Sign in with test credentials if not already authenticated"),
|
|
80979
|
+
step("Open account/profile or use a signed-in-only control"),
|
|
80980
|
+
step("Assert Sign out / profile identity is available, or complete sign-out and assert login chrome")
|
|
80981
|
+
],
|
|
80982
|
+
expectedOutcomes: [
|
|
80983
|
+
outcome("element-visible", "Signed-in or signed-out durable chrome matches the action")
|
|
80984
|
+
]
|
|
80985
|
+
};
|
|
80986
|
+
case "navigation_integrity":
|
|
80987
|
+
return {
|
|
80988
|
+
...base2,
|
|
80989
|
+
name: `${feature} - Navigation reaches intended destination`,
|
|
80990
|
+
type: "USER_JOURNEY",
|
|
80991
|
+
priority: 2,
|
|
80992
|
+
variant: "happy",
|
|
80993
|
+
goal: "Follow in-app navigation (tab, back, empty-state CTA, overflow) and assert the intended product destination \u2014 not hub chrome after wandering",
|
|
80994
|
+
steps: [
|
|
80995
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
80996
|
+
step(`Navigate into ${feature} via the observed entry path`),
|
|
80997
|
+
step("If an empty-state CTA is present, tap it and assert the destination form/error"),
|
|
80998
|
+
step("Use back/up if nested and assert the parent screen family")
|
|
80999
|
+
],
|
|
81000
|
+
expectedOutcomes: [
|
|
81001
|
+
outcome("screen-visible", "Intended destination screen family is visible")
|
|
81002
|
+
]
|
|
81003
|
+
};
|
|
81004
|
+
case "permission_system":
|
|
81005
|
+
return {
|
|
81006
|
+
...base2,
|
|
81007
|
+
name: `${feature} - Permission prompt behaves honestly`,
|
|
81008
|
+
type: "EDGE_CASE",
|
|
81009
|
+
priority: 2,
|
|
81010
|
+
variant: "happy",
|
|
81011
|
+
goal: "Trigger permission flow and assert allow/deny/settings path is honest",
|
|
81012
|
+
steps: [
|
|
81013
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
81014
|
+
step(`Navigate to the ${feature} permission-related surface`),
|
|
81015
|
+
step("Trigger the permission request"),
|
|
81016
|
+
step("Assert system or in-app permission UI appears and deny/allow path is reachable")
|
|
81017
|
+
],
|
|
81018
|
+
expectedOutcomes: [
|
|
81019
|
+
outcome("element-visible", "Permission or settings path is visible")
|
|
81020
|
+
]
|
|
81021
|
+
};
|
|
81022
|
+
case "double_submit":
|
|
81023
|
+
return {
|
|
81024
|
+
...base2,
|
|
81025
|
+
name: `${feature} - Single submit does not double-create`,
|
|
81026
|
+
type: "EDGE_CASE",
|
|
81027
|
+
priority: 2,
|
|
81028
|
+
variant: "bug",
|
|
81029
|
+
goal: "Submit once and assert count +1 (not +2) or a single confirmation",
|
|
81030
|
+
steps: [
|
|
81031
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
81032
|
+
step(`Navigate to the ${feature} submit flow`),
|
|
81033
|
+
step("Capture pre-submit count or confirmation state"),
|
|
81034
|
+
step("Tap primary submit only once"),
|
|
81035
|
+
step("Assert state delta: single creation (count +1 or one confirmation id)")
|
|
81036
|
+
],
|
|
81037
|
+
expectedOutcomes: [
|
|
81038
|
+
outcome("element-visible", "Exactly one durable result from a single submit")
|
|
81039
|
+
]
|
|
81040
|
+
};
|
|
81041
|
+
case "search_filter":
|
|
81042
|
+
return {
|
|
81043
|
+
...base2,
|
|
81044
|
+
name: `${feature} - Search filters the visible result set`,
|
|
81045
|
+
type: "HAPPY_PATH",
|
|
81046
|
+
priority: 1,
|
|
81047
|
+
variant: "happy",
|
|
81048
|
+
goal: "Type a distinctive query and assert the result set filters; clear/empty query restores or shows honest empty state",
|
|
81049
|
+
steps: [
|
|
81050
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
81051
|
+
step(`Navigate to the ${feature} search/filter surface`),
|
|
81052
|
+
step("Capture the unfiltered list identifiers"),
|
|
81053
|
+
step("Type a distinctive query into the search field"),
|
|
81054
|
+
step("Assert state delta: visible results changed (filtered or empty-state)")
|
|
81055
|
+
],
|
|
81056
|
+
expectedOutcomes: [
|
|
81057
|
+
outcome("element-visible", "Filtered results or empty-state copy matches the query")
|
|
81058
|
+
]
|
|
81059
|
+
};
|
|
81060
|
+
case "list_refresh":
|
|
81061
|
+
return {
|
|
81062
|
+
...base2,
|
|
81063
|
+
name: `${feature} - Pull to refresh keeps list coherent`,
|
|
81064
|
+
type: "HAPPY_PATH",
|
|
81065
|
+
priority: 1,
|
|
81066
|
+
variant: "happy",
|
|
81067
|
+
goal: "Capture list state, pull-to-refresh or tap Refresh, assert list remains coherent (not permanently wiped) or honest empty",
|
|
81068
|
+
steps: [
|
|
81069
|
+
step("Launch the app and sign in if needed", entryRoute),
|
|
81070
|
+
step(`Navigate to the ${feature} list with refresh`),
|
|
81071
|
+
step("Capture list identifiers before refresh"),
|
|
81072
|
+
step("Pull to refresh or activate Refresh"),
|
|
81073
|
+
step("Assert state delta: list still has coherent content or an honest empty state")
|
|
81074
|
+
],
|
|
81075
|
+
expectedOutcomes: [
|
|
81076
|
+
outcome("element-visible", "List after refresh is coherent (not permanently destroyed without recovery)")
|
|
81077
|
+
]
|
|
81078
|
+
};
|
|
81079
|
+
default:
|
|
81080
|
+
return null;
|
|
81081
|
+
}
|
|
81082
|
+
}
|
|
80584
81083
|
}
|
|
80585
81084
|
});
|
|
80586
81085
|
|
|
@@ -80669,10 +81168,15 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
80669
81168
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
80670
81169
|
var scenario_taxonomy_js_1 = require_scenario_taxonomy();
|
|
80671
81170
|
var coverage_map_js_1 = require_coverage_map();
|
|
81171
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
80672
81172
|
function isTargetAppScreenId(screenId, appId) {
|
|
80673
81173
|
if (!appId)
|
|
80674
81174
|
return true;
|
|
80675
|
-
|
|
81175
|
+
const hashIndex = screenId.indexOf("#");
|
|
81176
|
+
const owner = (hashIndex >= 0 ? screenId.slice(0, hashIndex) : screenId).trim();
|
|
81177
|
+
if (!owner)
|
|
81178
|
+
return false;
|
|
81179
|
+
return (0, shared_1.mobileScreenOwnerBelongsToApp)(owner, appId);
|
|
80676
81180
|
}
|
|
80677
81181
|
function targetAppScreens(graph, appId) {
|
|
80678
81182
|
return [...graph.screens.values()].filter((state2) => isTargetAppScreenId(state2.screenId, appId));
|
|
@@ -80814,12 +81318,19 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
80814
81318
|
Citing observed labels / accessibility-ids in \`expectedOutcomes\` (per Rule 12) is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
80815
81319
|
|
|
80816
81320
|
## Coverage Strategy
|
|
80817
|
-
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 12) + 1 EDGE_CASE (empty/invalid submission)
|
|
80818
|
-
- 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)
|
|
81321
|
+
- 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.
|
|
81322
|
+
- 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.
|
|
80819
81323
|
- 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.
|
|
80820
|
-
- 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.
|
|
81324
|
+
- 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).
|
|
81325
|
+
- For list counts / badges: plan before/after count or badge checks when those numbers were observed \u2014 not "Navigate up is visible".
|
|
81326
|
+
- For search/filter fields: plan a filter scenario with a distinctive query and assert the visible result set changes.
|
|
81327
|
+
- For pull-to-refresh / Refresh: capture list state, refresh, assert list remains coherent (not permanently wiped) or honest empty.
|
|
81328
|
+
- 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.
|
|
81329
|
+
- For settings/toggles/favorites: change value, leave screen (navigate away), return, assert value persisted or honestly reset.
|
|
80821
81330
|
- 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"
|
|
80822
81331
|
- For navigation: multi-screen user journeys within the feature scope (do NOT drift into unrelated features)
|
|
81332
|
+
- **Smoke budget:** at most ONE pure smoke/navigation-only scenario per feature. Prefer required strong kinds when UI signals support them.
|
|
81333
|
+
- **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.
|
|
80823
81334
|
|
|
80824
81335
|
## Output JSON Schema
|
|
80825
81336
|
{
|
|
@@ -81110,6 +81621,13 @@ Each entry is exactly one of:
|
|
|
81110
81621
|
if (context.appBrief) {
|
|
81111
81622
|
sections.push(`## Application Brief
|
|
81112
81623
|
${context.appBrief.slice(0, 500)}`);
|
|
81624
|
+
}
|
|
81625
|
+
const knownFacts = context.knowledgeContext?.trim();
|
|
81626
|
+
if (knownFacts) {
|
|
81627
|
+
sections.push(`## Known Facts
|
|
81628
|
+
${knownFacts.slice(0, 8e3)}
|
|
81629
|
+
|
|
81630
|
+
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.`);
|
|
81113
81631
|
}
|
|
81114
81632
|
const targetBits = [];
|
|
81115
81633
|
if (context.platform)
|
|
@@ -81548,14 +82066,46 @@ ${constraints.join("\n")}`);
|
|
|
81548
82066
|
}
|
|
81549
82067
|
return normalized;
|
|
81550
82068
|
}
|
|
82069
|
+
function buildMobileTaxonomyScenarios(input, gaps) {
|
|
82070
|
+
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
82071
|
+
if (orderedScreens.length === 0)
|
|
82072
|
+
return [];
|
|
82073
|
+
const entryScreenId = orderedScreens[0].screenId;
|
|
82074
|
+
const targetPages = orderedScreens.slice(0, 8).map((s) => s.screenId);
|
|
82075
|
+
const featureName = input.featureName?.trim() || nonGenericScreenName(orderedScreens[0], input.appId) || inferMobileFeatureGroupName(orderedScreens.slice(0, 3), input.appId) || "App";
|
|
82076
|
+
const signals = (0, scenario_taxonomy_js_1.extractUiSignals)(input.graph, input.appId);
|
|
82077
|
+
const missing = gaps && gaps.length > 0 ? gaps : (0, scenario_taxonomy_js_1.requiredScenarioKinds)(signals).filter((k) => k !== "smoke_nav");
|
|
82078
|
+
if (missing.length === 0) {
|
|
82079
|
+
return (0, scenario_taxonomy_js_1.synthesizeScenariosForMissingKinds)({
|
|
82080
|
+
gaps: ["navigation_integrity", "happy_submit"],
|
|
82081
|
+
featureName,
|
|
82082
|
+
entryRoute: entryScreenId,
|
|
82083
|
+
targetPages,
|
|
82084
|
+
existingNames: new Set((input.existingTestNames ?? []).map((n) => n.toLowerCase())),
|
|
82085
|
+
maxNew: Math.min(3, Math.max(1, input.testBudget || 3))
|
|
82086
|
+
});
|
|
82087
|
+
}
|
|
82088
|
+
return (0, scenario_taxonomy_js_1.synthesizeScenariosForMissingKinds)({
|
|
82089
|
+
gaps: missing,
|
|
82090
|
+
featureName,
|
|
82091
|
+
entryRoute: entryScreenId,
|
|
82092
|
+
targetPages,
|
|
82093
|
+
existingNames: new Set((input.existingTestNames ?? []).map((n) => n.toLowerCase())),
|
|
82094
|
+
maxNew: Math.min(6, Math.max(2, input.testBudget || 6))
|
|
82095
|
+
});
|
|
82096
|
+
}
|
|
81551
82097
|
function buildMobileFallbackScenarios(input) {
|
|
81552
82098
|
if (input.mode === "survey")
|
|
81553
82099
|
return [];
|
|
81554
|
-
if (input.featureName?.trim())
|
|
81555
|
-
return
|
|
82100
|
+
if (input.featureName?.trim()) {
|
|
82101
|
+
return buildMobileTaxonomyScenarios(input);
|
|
82102
|
+
}
|
|
81556
82103
|
const orderedScreens = plannableTargetAppScreens(input.graph, input.appId).filter((state2) => state2.visited || state2.firstSeenIteration === 0).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
|
|
81557
82104
|
if (orderedScreens.length === 0)
|
|
81558
82105
|
return [];
|
|
82106
|
+
const taxonomy = buildMobileTaxonomyScenarios(input);
|
|
82107
|
+
if (taxonomy.length > 0)
|
|
82108
|
+
return taxonomy;
|
|
81559
82109
|
const entryScreenId = orderedScreens[0].screenId;
|
|
81560
82110
|
const usedNames = new Set((input.existingTestNames ?? []).map((name) => name.toLowerCase()));
|
|
81561
82111
|
const scenarios = [];
|
|
@@ -81848,6 +82398,7 @@ ${constraints.join("\n")}`);
|
|
|
81848
82398
|
userPromptOverride: formatMobileEvidenceForPlanner(input.graph, {
|
|
81849
82399
|
mode: input.mode,
|
|
81850
82400
|
appBrief: input.appBrief,
|
|
82401
|
+
knowledgeContext: input.knowledgeContext,
|
|
81851
82402
|
appId: input.appId,
|
|
81852
82403
|
platform: input.platform,
|
|
81853
82404
|
featureName: input.featureName,
|
|
@@ -81880,44 +82431,65 @@ ${constraints.join("\n")}`);
|
|
|
81880
82431
|
if (guestWallFilter.dropped > 0) {
|
|
81881
82432
|
mobileAdaptationLogs.push(`Dropped ${guestWallFilter.dropped} guest-wall scenario(s) \u2014 explore session was authenticated; re-exec would not see sign-in gates`);
|
|
81882
82433
|
}
|
|
81883
|
-
|
|
82434
|
+
let plannedScenarios = guestWallFilter.scenarios;
|
|
81884
82435
|
let taxonomyGapLogs = [];
|
|
81885
82436
|
let taxonomyCoverageGaps = result.coverageGaps ?? [];
|
|
81886
|
-
if (input.mode !== "survey" &&
|
|
82437
|
+
if (input.mode !== "survey" && input.graph.screens.size > 0) {
|
|
81887
82438
|
const signals = (0, scenario_taxonomy_js_1.extractUiSignals)(input.graph, input.appId);
|
|
81888
82439
|
const plannedKinds = (0, scenario_taxonomy_js_1.inferScenarioKinds)(plannedScenarios);
|
|
81889
|
-
|
|
81890
|
-
|
|
81891
|
-
|
|
81892
|
-
appId: input.appId,
|
|
81893
|
-
plannedScenarios
|
|
81894
|
-
});
|
|
81895
|
-
taxonomyGapLogs = [(0, scenario_taxonomy_js_1.formatCoverageGapLog)(gaps), coverageMap.summary];
|
|
81896
|
-
if (coverageMap.warnings.length > 0) {
|
|
81897
|
-
taxonomyGapLogs.push(...coverageMap.warnings.slice(0, 12));
|
|
82440
|
+
let gaps = (0, scenario_taxonomy_js_1.coverageGaps)(signals, plannedKinds);
|
|
82441
|
+
if (plannedScenarios.length === 0) {
|
|
82442
|
+
gaps = (0, scenario_taxonomy_js_1.requiredScenarioKinds)(signals).filter((k) => k !== "smoke_nav");
|
|
81898
82443
|
}
|
|
81899
82444
|
if (gaps.length > 0) {
|
|
81900
|
-
|
|
81901
|
-
|
|
81902
|
-
|
|
81903
|
-
...
|
|
81904
|
-
|
|
81905
|
-
|
|
81906
|
-
|
|
81907
|
-
|
|
81908
|
-
|
|
81909
|
-
|
|
81910
|
-
|
|
81911
|
-
|
|
81912
|
-
|
|
81913
|
-
|
|
81914
|
-
|
|
81915
|
-
|
|
81916
|
-
|
|
81917
|
-
|
|
81918
|
-
|
|
82445
|
+
const synthesized = buildMobileTaxonomyScenarios(input, gaps);
|
|
82446
|
+
if (synthesized.length > 0) {
|
|
82447
|
+
const before = plannedScenarios.length;
|
|
82448
|
+
plannedScenarios = [...plannedScenarios, ...synthesized];
|
|
82449
|
+
mobileAdaptationLogs.push(`Taxonomy synthesis: added ${synthesized.length} scenario(s) for missing kinds [${gaps.join(", ")}] (plan had ${before})`);
|
|
82450
|
+
gaps = (0, scenario_taxonomy_js_1.coverageGaps)(signals, (0, scenario_taxonomy_js_1.inferScenarioKinds)(plannedScenarios));
|
|
82451
|
+
}
|
|
82452
|
+
}
|
|
82453
|
+
if (plannedScenarios.length > 0) {
|
|
82454
|
+
const coverageMap = (0, coverage_map_js_1.buildMobileCoverageMap)({
|
|
82455
|
+
graph: input.graph,
|
|
82456
|
+
appId: input.appId,
|
|
82457
|
+
plannedScenarios
|
|
82458
|
+
});
|
|
82459
|
+
taxonomyGapLogs = [(0, scenario_taxonomy_js_1.formatCoverageGapLog)(gaps), coverageMap.summary];
|
|
82460
|
+
if (coverageMap.warnings.length > 0) {
|
|
82461
|
+
taxonomyGapLogs.push(...coverageMap.warnings.slice(0, 12));
|
|
82462
|
+
}
|
|
82463
|
+
if (gaps.length > 0 || coverageMap.warnings.length > 0) {
|
|
82464
|
+
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}`);
|
|
82465
|
+
const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
82466
|
+
taxonomyCoverageGaps = [
|
|
82467
|
+
...taxonomyCoverageGaps,
|
|
82468
|
+
...gaps.map((kind) => ({
|
|
82469
|
+
scenarioName: `[taxonomy] ${kind}`,
|
|
82470
|
+
reason: `Required scenario kind "${kind}" not represented in the planned set`,
|
|
82471
|
+
suggestion: `Add a ${kind} scenario backed by observed controls, or document impossibility`,
|
|
82472
|
+
unmetPrerequisites: [],
|
|
82473
|
+
detectedAt
|
|
82474
|
+
})),
|
|
82475
|
+
...coverageMap.warnings.filter((w) => /smoke-only|no strong-oracle/i.test(w)).map((w) => ({
|
|
82476
|
+
scenarioName: "[coverage-map] strong-oracle gap",
|
|
82477
|
+
reason: w,
|
|
82478
|
+
suggestion: "Author durable assertStateDelta/assertText outcomes for this kind",
|
|
82479
|
+
unmetPrerequisites: [],
|
|
82480
|
+
detectedAt
|
|
82481
|
+
}))
|
|
82482
|
+
];
|
|
82483
|
+
}
|
|
81919
82484
|
}
|
|
81920
82485
|
}
|
|
82486
|
+
if (input.mode !== "survey" && input.featureName?.trim()) {
|
|
82487
|
+
const deepFeature = input.featureName.trim();
|
|
82488
|
+
plannedScenarios = plannedScenarios.map((s) => ({
|
|
82489
|
+
...s,
|
|
82490
|
+
featureGroup: s.featureGroup?.trim() || deepFeature
|
|
82491
|
+
}));
|
|
82492
|
+
}
|
|
81921
82493
|
if (input.mode !== "survey" && plannedScenarios.length === 0 && input.graph.screens.size > 0) {
|
|
81922
82494
|
const fallbackScenarios = buildMobileFallbackScenarios(input);
|
|
81923
82495
|
if (fallbackScenarios.length > 0) {
|
|
@@ -81928,7 +82500,7 @@ ${constraints.join("\n")}`);
|
|
|
81928
82500
|
logs: [
|
|
81929
82501
|
...result.logs,
|
|
81930
82502
|
...mobileAdaptationLogs,
|
|
81931
|
-
`Mobile fallback: synthesized ${fallbackScenarios.length}
|
|
82503
|
+
`Mobile fallback: synthesized ${fallbackScenarios.length} taxonomy/bug-capture scenario(s) from native UI signals`
|
|
81932
82504
|
],
|
|
81933
82505
|
fallbackUsed: true,
|
|
81934
82506
|
fallbackReason: result.fallbackReason ?? "Mobile planner produced no scenarios"
|
|
@@ -81972,6 +82544,25 @@ ${constraints.join("\n")}`);
|
|
|
81972
82544
|
fallbackReason: result.fallbackReason ?? plannerGroupSelection.reason ?? `Planner omitted ${supplementedSurveyRouteCount} plannable mobile screen(s)`
|
|
81973
82545
|
};
|
|
81974
82546
|
}
|
|
82547
|
+
if (mobileFeatureGroups.length === 0 && input.graph.screens.size > 0) {
|
|
82548
|
+
const appId = input.appId?.trim();
|
|
82549
|
+
const owners = [...new Set([...input.graph.screens.values()].map((state2) => {
|
|
82550
|
+
const hashIndex = state2.screenId.indexOf("#");
|
|
82551
|
+
return (hashIndex >= 0 ? state2.screenId.slice(0, hashIndex) : state2.screenId).trim();
|
|
82552
|
+
}).filter(Boolean))];
|
|
82553
|
+
const inAppOwners = appId ? owners.filter((owner) => (0, shared_1.mobileScreenOwnerBelongsToApp)(owner, appId)) : owners;
|
|
82554
|
+
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).`;
|
|
82555
|
+
return {
|
|
82556
|
+
...result,
|
|
82557
|
+
scenarios: plannedScenarios,
|
|
82558
|
+
featureGroups: mobileFeatureGroups,
|
|
82559
|
+
coverageGaps: taxonomyCoverageGaps.length > 0 ? taxonomyCoverageGaps : result.coverageGaps,
|
|
82560
|
+
logs: [...result.logs, ...mobileAdaptationLogs, ...taxonomyGapLogs, `\u26A0 ${reason}`],
|
|
82561
|
+
warning: result.warning ?? reason,
|
|
82562
|
+
fallbackUsed: true,
|
|
82563
|
+
fallbackReason: result.fallbackReason ?? reason
|
|
82564
|
+
};
|
|
82565
|
+
}
|
|
81975
82566
|
return {
|
|
81976
82567
|
...result,
|
|
81977
82568
|
scenarios: plannedScenarios,
|
|
@@ -82040,6 +82631,7 @@ var require_anr_env = __commonJS({
|
|
|
82040
82631
|
exports.isAnrOrSystemUiEnvironmentFailure = isAnrOrSystemUiEnvironmentFailure;
|
|
82041
82632
|
exports.stepLooksLikeAnrChromeAssert = stepLooksLikeAnrChromeAssert;
|
|
82042
82633
|
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;
|
|
82634
|
+
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;
|
|
82043
82635
|
function textLooksLikeAnrOrSystemUi(text) {
|
|
82044
82636
|
if (!text || typeof text !== "string")
|
|
82045
82637
|
return false;
|
|
@@ -82048,7 +82640,7 @@ var require_anr_env = __commonJS({
|
|
|
82048
82640
|
function isAnrOrSystemUiEnvironmentFailure(error2, logs2) {
|
|
82049
82641
|
const blob = `${error2 ?? ""}
|
|
82050
82642
|
${logs2 ?? ""}`;
|
|
82051
|
-
if (
|
|
82643
|
+
if (SYSTEM_FREEZE_RE.test(blob))
|
|
82052
82644
|
return true;
|
|
82053
82645
|
if (/\bcold-boot\b/i.test(blob) && /\b(froze|hang|anr|unresponsive)\b/i.test(blob))
|
|
82054
82646
|
return true;
|
|
@@ -82070,13 +82662,48 @@ var require_oracle_quality = __commonJS({
|
|
|
82070
82662
|
"../runner-core/dist/services/mobile/oracle-quality.js"(exports) {
|
|
82071
82663
|
"use strict";
|
|
82072
82664
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
82665
|
+
exports.isToastOrAckOutcomeText = isToastOrAckOutcomeText;
|
|
82666
|
+
exports.isAuthPrimaryFeatureName = isAuthPrimaryFeatureName;
|
|
82667
|
+
exports.isStandaloneAuthValidationScenario = isStandaloneAuthValidationScenario;
|
|
82668
|
+
exports.textLooksLikeProductOutcome = textLooksLikeProductOutcome;
|
|
82669
|
+
exports.textLooksLikeChromeSurface = textLooksLikeChromeSurface;
|
|
82073
82670
|
exports.assessStrongOracleQuality = assessStrongOracleQuality;
|
|
82671
|
+
exports.assessScenarioClaimCoherence = assessScenarioClaimCoherence;
|
|
82074
82672
|
var anr_env_js_1 = require_anr_env();
|
|
82673
|
+
var scenario_taxonomy_js_1 = require_scenario_taxonomy();
|
|
82075
82674
|
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;
|
|
82076
82675
|
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;
|
|
82676
|
+
var TOAST_ACK_EXACT_RE = /^(saved!?|success!?|successfully (saved|deleted|updated|created)!?|deleted!?|removed!?|done!?|ok!?|updated!?|created!?)$/i;
|
|
82677
|
+
function isToastOrAckOutcomeText(text) {
|
|
82678
|
+
const t = text.trim();
|
|
82679
|
+
if (!t)
|
|
82680
|
+
return false;
|
|
82681
|
+
if (TOAST_ACK_EXACT_RE.test(t))
|
|
82682
|
+
return true;
|
|
82683
|
+
const parts = t.split(/\s+/).filter(Boolean);
|
|
82684
|
+
return parts.length > 0 && parts.every((part) => TOAST_ACK_EXACT_RE.test(part));
|
|
82685
|
+
}
|
|
82686
|
+
function isAuthPrimaryFeatureName(name) {
|
|
82687
|
+
const n = (name ?? "").trim();
|
|
82688
|
+
if (!n)
|
|
82689
|
+
return false;
|
|
82690
|
+
return /\b(auth|authentication|sign[\s-]*in|log[\s-]*in|login|session|onboarding|account)\b/i.test(n);
|
|
82691
|
+
}
|
|
82692
|
+
function isStandaloneAuthValidationScenario(scenario) {
|
|
82693
|
+
const text = `${scenario.name ?? ""} ${scenario.goal ?? ""} ${scenario.variant ?? ""}`.toLowerCase();
|
|
82694
|
+
if (!/\b(sign\s*in|log\s*in|login|credentials?|password|email)\b/.test(text))
|
|
82695
|
+
return false;
|
|
82696
|
+
return /\b(empty|invalid|wrong password|malformed|required|validation|reject|blocked)\b/.test(text) || scenario.variant === "validation";
|
|
82697
|
+
}
|
|
82077
82698
|
function stepBlob(step) {
|
|
82078
82699
|
return [step.description, step.assertion, step.value, step.target?.primary].filter(Boolean).join(" ");
|
|
82079
82700
|
}
|
|
82701
|
+
function textLooksLikeProductOutcome(text) {
|
|
82702
|
+
return PRODUCT_OUTCOME_TEXT_RE.test(text);
|
|
82703
|
+
}
|
|
82704
|
+
function textLooksLikeChromeSurface(text) {
|
|
82705
|
+
return CHROME_ONLY_RE.test(text);
|
|
82706
|
+
}
|
|
82080
82707
|
function isChromeOnlyVisible(step) {
|
|
82081
82708
|
if (step.action !== "assertVisible")
|
|
82082
82709
|
return false;
|
|
@@ -82113,6 +82740,8 @@ var require_oracle_quality = __commonJS({
|
|
|
82113
82740
|
if (/^(email|password|username|name|query|search)$/i.test((s.assertion ?? "").trim())) {
|
|
82114
82741
|
return false;
|
|
82115
82742
|
}
|
|
82743
|
+
if (isToastOrAckOutcomeText(s.assertion ?? s.value ?? ""))
|
|
82744
|
+
return false;
|
|
82116
82745
|
return blob.length >= 3;
|
|
82117
82746
|
});
|
|
82118
82747
|
const hasStrongValidationVisible = steps.some((s) => {
|
|
@@ -82120,7 +82749,12 @@ var require_oracle_quality = __commonJS({
|
|
|
82120
82749
|
return false;
|
|
82121
82750
|
if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s) || isChromeOnlyVisible(s))
|
|
82122
82751
|
return false;
|
|
82123
|
-
|
|
82752
|
+
const blob = stepBlob(s);
|
|
82753
|
+
if (isToastOrAckOutcomeText(s.assertion ?? s.value ?? ""))
|
|
82754
|
+
return false;
|
|
82755
|
+
if (isToastOrAckOutcomeText(blob))
|
|
82756
|
+
return false;
|
|
82757
|
+
return PRODUCT_OUTCOME_TEXT_RE.test(blob);
|
|
82124
82758
|
});
|
|
82125
82759
|
const strong = hasStateDelta || hasStrongAssertText || hasStrongValidationVisible;
|
|
82126
82760
|
let reason;
|
|
@@ -82145,6 +82779,70 @@ var require_oracle_quality = __commonJS({
|
|
|
82145
82779
|
reason
|
|
82146
82780
|
};
|
|
82147
82781
|
}
|
|
82782
|
+
function assessScenarioClaimCoherence(scenario, steps) {
|
|
82783
|
+
const variant = scenario.variant === "happy" || scenario.variant === "validation" || scenario.variant === "bug" ? scenario.variant : void 0;
|
|
82784
|
+
const kind = (0, scenario_taxonomy_js_1.inferScenarioKind)({
|
|
82785
|
+
type: scenario.type || "HAPPY_PATH",
|
|
82786
|
+
variant,
|
|
82787
|
+
name: scenario.name ?? "",
|
|
82788
|
+
goal: scenario.goal ?? ""
|
|
82789
|
+
});
|
|
82790
|
+
const quality = assessStrongOracleQuality(steps);
|
|
82791
|
+
const text = `${scenario.name ?? ""} ${scenario.goal ?? ""}`.toLowerCase();
|
|
82792
|
+
const featureGroup = scenario.featureGroup ?? "";
|
|
82793
|
+
if (isStandaloneAuthValidationScenario(scenario) && featureGroup && !isAuthPrimaryFeatureName(featureGroup)) {
|
|
82794
|
+
return {
|
|
82795
|
+
coherent: false,
|
|
82796
|
+
kind,
|
|
82797
|
+
discard: true,
|
|
82798
|
+
reason: `auth credential validation scenario under non-auth feature "${featureGroup}" \u2014 login is setup only`
|
|
82799
|
+
};
|
|
82800
|
+
}
|
|
82801
|
+
if (quality.anrOnly) {
|
|
82802
|
+
return {
|
|
82803
|
+
coherent: false,
|
|
82804
|
+
kind,
|
|
82805
|
+
discard: true,
|
|
82806
|
+
reason: quality.reason
|
|
82807
|
+
};
|
|
82808
|
+
}
|
|
82809
|
+
if ((0, scenario_taxonomy_js_1.kindRequiresStateDelta)(kind) && !quality.hasStateDelta) {
|
|
82810
|
+
const namesStateClaim = /\b(count|delta|delete|removed|persist|roundtrip|wrong row|entity|double.?submit|fires?:?\s*2|refresh)\b/i.test(text);
|
|
82811
|
+
return {
|
|
82812
|
+
coherent: false,
|
|
82813
|
+
kind,
|
|
82814
|
+
// Discard when the name/kind clearly promised state proof; otherwise demote.
|
|
82815
|
+
discard: namesStateClaim || kind === "crud_identity" || kind === "count_delta" || kind === "list_refresh",
|
|
82816
|
+
reason: `scenario kind ${kind} claims durable state change but steps lack assertStateDelta (${quality.reason})`
|
|
82817
|
+
};
|
|
82818
|
+
}
|
|
82819
|
+
if (/\b(empty.?state|empty cta|dead route|404|not found|broken blank)\b/i.test(text)) {
|
|
82820
|
+
const hasProductDestination = steps.some((s) => {
|
|
82821
|
+
if (s.action !== "assertVisible" && s.action !== "assertText")
|
|
82822
|
+
return false;
|
|
82823
|
+
if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s))
|
|
82824
|
+
return false;
|
|
82825
|
+
const blob = stepBlob(s);
|
|
82826
|
+
if (CHROME_ONLY_RE.test(blob) && !PRODUCT_OUTCOME_TEXT_RE.test(blob))
|
|
82827
|
+
return false;
|
|
82828
|
+
return PRODUCT_OUTCOME_TEXT_RE.test(blob) || /\b(404|not found|empty|no items|blank|error)\b/i.test(blob);
|
|
82829
|
+
});
|
|
82830
|
+
if (!hasProductDestination && !quality.hasStateDelta) {
|
|
82831
|
+
return {
|
|
82832
|
+
coherent: false,
|
|
82833
|
+
kind,
|
|
82834
|
+
discard: false,
|
|
82835
|
+
reason: "empty-state/dead-route claim without destination or empty-state product assertion"
|
|
82836
|
+
};
|
|
82837
|
+
}
|
|
82838
|
+
}
|
|
82839
|
+
return {
|
|
82840
|
+
coherent: true,
|
|
82841
|
+
kind,
|
|
82842
|
+
discard: false,
|
|
82843
|
+
reason: "scenario claim matches step oracles"
|
|
82844
|
+
};
|
|
82845
|
+
}
|
|
82148
82846
|
}
|
|
82149
82847
|
});
|
|
82150
82848
|
|
|
@@ -82662,7 +83360,18 @@ ${hybridSkeleton.join("\n")}`;
|
|
|
82662
83360
|
function hasStableId(element) {
|
|
82663
83361
|
return !!nonEmpty3(element.accessibilityId) || !!nonEmpty3(element.resourceId);
|
|
82664
83362
|
}
|
|
82665
|
-
function
|
|
83363
|
+
function buildKnownFactsPromptBlock(knowledgeContext) {
|
|
83364
|
+
const text = knowledgeContext?.trim();
|
|
83365
|
+
if (!text)
|
|
83366
|
+
return "";
|
|
83367
|
+
return `
|
|
83368
|
+
|
|
83369
|
+
## Known Facts (project documents \u2014 optional)
|
|
83370
|
+
${text.slice(0, 6e3)}
|
|
83371
|
+
|
|
83372
|
+
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.`;
|
|
83373
|
+
}
|
|
83374
|
+
function buildSystemPrompt(platform3, targetAppId, maxIterations = MAX_SCENARIO_ITERATIONS, knowledgeContext) {
|
|
82666
83375
|
const platformLabel = platform3 === "IOS" ? "iOS" : "Android";
|
|
82667
83376
|
const targetLine = targetAppId ? ` Target app package/bundle: ${targetAppId}.` : "";
|
|
82668
83377
|
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.
|
|
@@ -82686,14 +83395,14 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
|
|
|
82686
83395
|
- 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.
|
|
82687
83396
|
- 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".
|
|
82688
83397
|
- 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.
|
|
82689
|
-
- Do NOT assert bottom-nav tab labels alone
|
|
83398
|
+
- Do NOT assert bottom-nav tab labels alone or notification badge counts ("N unread") as the scenario outcome.
|
|
82690
83399
|
- 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.
|
|
82691
83400
|
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.
|
|
82692
83401
|
|
|
82693
83402
|
## Rules
|
|
82694
83403
|
- Re-snapshot before every assertion so the ref is fresh.
|
|
82695
83404
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
82696
|
-
- Assert only DURABLE
|
|
83405
|
+
- 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.
|
|
82697
83406
|
- 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.
|
|
82698
83407
|
- 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.
|
|
82699
83408
|
- 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.
|
|
@@ -82706,7 +83415,7 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
|
|
|
82706
83415
|
- 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.
|
|
82707
83416
|
- 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.
|
|
82708
83417
|
|
|
82709
|
-
Max iterations: ${maxIterations}. You will be warned near the end
|
|
83418
|
+
Max iterations: ${maxIterations}. You will be warned near the end.` + buildKnownFactsPromptBlock(knowledgeContext);
|
|
82710
83419
|
}
|
|
82711
83420
|
function buildGenerateCredentialPromptBlock(secrets) {
|
|
82712
83421
|
const keys = Object.keys(secrets).filter((k) => secrets[k] && secrets[k].length > 0);
|
|
@@ -82796,7 +83505,7 @@ Start by calling mobile_snapshot, then execute ONLY the steps needed for this go
|
|
|
82796
83505
|
}
|
|
82797
83506
|
}
|
|
82798
83507
|
async function runScenarioLoop(args) {
|
|
82799
|
-
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch } = args;
|
|
83508
|
+
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch, knowledgeContext } = args;
|
|
82800
83509
|
const log2 = (line) => emitLog(redactCredentialValues(line, redactionCredentials));
|
|
82801
83510
|
const actions = [];
|
|
82802
83511
|
const stateBag = /* @__PURE__ */ new Map();
|
|
@@ -83174,7 +83883,7 @@ ${lines.join("\n")}`
|
|
|
83174
83883
|
(0, mobile_driver_errors_js_1.rethrowIfTerminalMobileDriverError)(err);
|
|
83175
83884
|
log2(` seed snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
83176
83885
|
}
|
|
83177
|
-
const systemPrompt = buildSystemPrompt(platform3, targetAppId, maxIterations) + buildGenerateCredentialPromptBlock(credentials);
|
|
83886
|
+
const systemPrompt = buildSystemPrompt(platform3, targetAppId, maxIterations, knowledgeContext) + buildGenerateCredentialPromptBlock(credentials);
|
|
83178
83887
|
const kickoff = buildScenarioKickoff(scenario);
|
|
83179
83888
|
const recentExchanges = [];
|
|
83180
83889
|
const hasStoredCredentials = Object.values(credentials).some((v) => typeof v === "string" && v.length > 0);
|
|
@@ -85123,6 +85832,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85123
85832
|
const t = text.trim();
|
|
85124
85833
|
if (!t)
|
|
85125
85834
|
return false;
|
|
85835
|
+
if ((0, oracle_quality_js_1.isToastOrAckOutcomeText)(t))
|
|
85836
|
+
return true;
|
|
85126
85837
|
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)) {
|
|
85127
85838
|
return true;
|
|
85128
85839
|
}
|
|
@@ -85228,6 +85939,30 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85228
85939
|
}
|
|
85229
85940
|
const ephemeral = modelAsserts.filter((a) => isEphemeralContentAssertText(assertIdentityText(a)));
|
|
85230
85941
|
const durable = modelAsserts.filter((a) => !isEphemeralContentAssertText(assertIdentityText(a)));
|
|
85942
|
+
const chromeOnlyVisible = (a) => {
|
|
85943
|
+
if (a.type !== "ASSERT_VISIBLE")
|
|
85944
|
+
return false;
|
|
85945
|
+
const blob = assertIdentityText(a);
|
|
85946
|
+
if ((0, oracle_quality_js_1.isToastOrAckOutcomeText)(blob))
|
|
85947
|
+
return true;
|
|
85948
|
+
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);
|
|
85949
|
+
};
|
|
85950
|
+
if (durable.length > 0 && durable.every(chromeOnlyVisible) && !actions.some((a) => a.type === "ASSERT_STATE_DELTA")) {
|
|
85951
|
+
return {
|
|
85952
|
+
ok: false,
|
|
85953
|
+
severity: "downgrade",
|
|
85954
|
+
reason: "only landing/chrome visibility after interaction \u2014 not a durable product outcome",
|
|
85955
|
+
signals: {
|
|
85956
|
+
apiTest: false,
|
|
85957
|
+
interacted: actions.some((a) => a.type === "TAP" || a.type === "TYPE"),
|
|
85958
|
+
mutated: actions.some((a) => a.type === "TYPE" || a.type === "CLEAR" || a.type === "TAP"),
|
|
85959
|
+
navOnly: false,
|
|
85960
|
+
hasOutcomeProof: false,
|
|
85961
|
+
errorBanner: false,
|
|
85962
|
+
terminal: null
|
|
85963
|
+
}
|
|
85964
|
+
};
|
|
85965
|
+
}
|
|
85231
85966
|
if (ephemeral.length > 0 && durable.length === 0) {
|
|
85232
85967
|
return {
|
|
85233
85968
|
ok: false,
|
|
@@ -85322,10 +86057,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85322
86057
|
}
|
|
85323
86058
|
const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
|
|
85324
86059
|
const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
|
|
86060
|
+
const bugWitnessEnabled = (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_BUG_WITNESS_ENABLED, true);
|
|
85325
86061
|
const bugLooksLikeAnr = (0, anr_env_js_1.textLooksLikeAnrOrSystemUi)(bugDescription);
|
|
85326
|
-
const bugWitnessHonored =
|
|
86062
|
+
const bugWitnessHonored = bugWitnessEnabled && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0 && !bugLooksLikeAnr;
|
|
85327
86063
|
if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
|
|
85328
|
-
log2(" suspectedBug declared but NOT honored: " + (bugLooksLikeAnr ? "ANR/System UI freeze is environment noise, not a product bug-witness." :
|
|
86064
|
+
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."));
|
|
85329
86065
|
}
|
|
85330
86066
|
const strengthVerdict = bugWitnessHonored ? {
|
|
85331
86067
|
...baseStrengthVerdict,
|
|
@@ -85355,30 +86091,48 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85355
86091
|
if (selectorLintEnforced && verified) {
|
|
85356
86092
|
log2(` selector-quality lint (enforce): ${selectorQuality.summary.high} high-severity locator issue(s) \u2014 tagging @needs-review.`);
|
|
85357
86093
|
}
|
|
85358
|
-
const
|
|
86094
|
+
const strengthBlocked = !strengthVerdict.ok || strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug" || !verified || coldResetUnavailable;
|
|
86095
|
+
const claimCoherence = (0, oracle_quality_js_1.assessScenarioClaimCoherence)({
|
|
86096
|
+
name: scenario.name,
|
|
86097
|
+
goal: scenario.goal,
|
|
86098
|
+
type: scenario.type,
|
|
86099
|
+
variant: scenario.variant,
|
|
86100
|
+
featureGroup: scenario.featureGroup
|
|
86101
|
+
}, compiled.steps);
|
|
86102
|
+
const oracleQualityEarly = (0, oracle_quality_js_1.assessStrongOracleQuality)(compiled.steps);
|
|
86103
|
+
if (oracleQualityEarly.anrOnly || claimCoherence.discard) {
|
|
86104
|
+
log2(` discarding incoherent/ANR recording: ${claimCoherence.discard ? claimCoherence.reason : oracleQualityEarly.reason}`);
|
|
86105
|
+
return null;
|
|
86106
|
+
}
|
|
86107
|
+
const reviewTag = verified && !selectorLintEnforced && claimCoherence.coherent ? "@verified" : "@needs-review";
|
|
85359
86108
|
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
85360
86109
|
if (strengthVerdict.severity === "downgrade")
|
|
85361
86110
|
tags.push("@smoke");
|
|
86111
|
+
if (strengthVerdict.severity === "route-bug" && !tags.includes("@bug-found")) {
|
|
86112
|
+
tags.push("@bug-found");
|
|
86113
|
+
}
|
|
85362
86114
|
if (coldResetUnavailable)
|
|
85363
86115
|
tags.push("@warm-state");
|
|
85364
|
-
const strengthBlocked = !strengthVerdict.ok || strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug" || !verified || coldResetUnavailable;
|
|
85365
86116
|
{
|
|
85366
86117
|
const kind = (0, scenario_taxonomy_js_1.inferScenarioKind)(scenario);
|
|
85367
|
-
const oracleQuality =
|
|
85368
|
-
if (oracleQuality.anrOnly) {
|
|
85369
|
-
log2(` discarding ANR-chrome-only recording: ${oracleQuality.reason}`);
|
|
85370
|
-
return null;
|
|
85371
|
-
}
|
|
86118
|
+
const oracleQuality = oracleQualityEarly;
|
|
85372
86119
|
if (oracleQuality.unpairedCaptures > 0) {
|
|
85373
86120
|
log2(` oracle quality: ${oracleQuality.reason}`);
|
|
85374
86121
|
}
|
|
85375
|
-
|
|
86122
|
+
if (!claimCoherence.coherent) {
|
|
86123
|
+
log2(` scenario claim weak: ${claimCoherence.reason}`);
|
|
86124
|
+
if (!tags.includes("@smoke"))
|
|
86125
|
+
tags.push("@smoke");
|
|
86126
|
+
}
|
|
86127
|
+
const strong = claimCoherence.coherent && verified && !strengthBlocked && strengthVerdict.severity === "none" && oracleQuality.strong && ((0, scenario_taxonomy_js_1.isStrongOracleKind)(kind) || oracleQuality.hasStateDelta || oracleQuality.hasStrongAssertText);
|
|
85376
86128
|
if (strong)
|
|
85377
86129
|
tags.push("@strong-oracle");
|
|
85378
86130
|
else if (!tags.includes("@smoke")) {
|
|
85379
86131
|
tags.push("@smoke");
|
|
85380
86132
|
if (!oracleQuality.strong) {
|
|
85381
86133
|
log2(` tagged @smoke (not strong-oracle): ${oracleQuality.reason}`);
|
|
86134
|
+
} else if (!claimCoherence.coherent) {
|
|
86135
|
+
log2(` tagged @smoke (claim incoherent): ${claimCoherence.reason}`);
|
|
85382
86136
|
}
|
|
85383
86137
|
}
|
|
85384
86138
|
}
|
|
@@ -85398,11 +86152,9 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85398
86152
|
playwrightCode: "",
|
|
85399
86153
|
verified,
|
|
85400
86154
|
tags,
|
|
85401
|
-
// Per-test
|
|
85402
|
-
//
|
|
85403
|
-
//
|
|
85404
|
-
// every test under the deep-dived feature. The scenario's own group is the
|
|
85405
|
-
// correct home; the runner forwards it as `suiteName`.
|
|
86155
|
+
// Per-test suite hint. Server deep mode overrides suite to ctx.featureName
|
|
86156
|
+
// so leaf features receive their own tests; full-mode multi-group still uses
|
|
86157
|
+
// featureGroup when present.
|
|
85406
86158
|
...scenario.featureGroup?.trim() ? { suiteName: scenario.featureGroup.trim() } : {},
|
|
85407
86159
|
// Carry the planner scenario identity so incremental/resume can match an
|
|
85408
86160
|
// already-generated scenario reliably (scenarioRef.name === the plan's
|
|
@@ -85569,7 +86321,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
85569
86321
|
credentials: credentialSecrets,
|
|
85570
86322
|
redactionCredentials: credentialValuesForRedaction,
|
|
85571
86323
|
log: log2,
|
|
85572
|
-
allowRelaunch: (ctx.target?.resetPolicy ?? "AUTO") !== "PRESERVE_APP_DATA"
|
|
86324
|
+
allowRelaunch: (ctx.target?.resetPolicy ?? "AUTO") !== "PRESERVE_APP_DATA",
|
|
86325
|
+
knowledgeContext: ctx.knowledgeContext
|
|
85573
86326
|
});
|
|
85574
86327
|
consecutiveInfraFailures = 0;
|
|
85575
86328
|
const test = assembleTest({
|
|
@@ -85634,13 +86387,23 @@ var require_failure_triage = __commonJS({
|
|
|
85634
86387
|
"../runner-core/dist/services/mobile/failure-triage.js"(exports) {
|
|
85635
86388
|
"use strict";
|
|
85636
86389
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86390
|
+
exports.isOracleStep = isOracleStep;
|
|
85637
86391
|
exports.hasStrongMobileOracle = hasStrongMobileOracle;
|
|
85638
86392
|
exports.isStrongOracleStep = isStrongOracleStep;
|
|
85639
86393
|
exports.listStrongOracleKinds = listStrongOracleKinds;
|
|
85640
86394
|
exports.triageMobileFailure = triageMobileFailure;
|
|
85641
86395
|
exports.triageToHealTerminal = triageToHealTerminal;
|
|
85642
86396
|
var anr_env_js_1 = require_anr_env();
|
|
85643
|
-
var
|
|
86397
|
+
var oracle_quality_js_1 = require_oracle_quality();
|
|
86398
|
+
var STRONG_ACTIONS = /* @__PURE__ */ new Set(["assertStateDelta", "assertText"]);
|
|
86399
|
+
var ORACLE_ACTIONS = /* @__PURE__ */ new Set([
|
|
86400
|
+
"assertVisible",
|
|
86401
|
+
"assertText",
|
|
86402
|
+
"assertStateDelta"
|
|
86403
|
+
]);
|
|
86404
|
+
function isOracleStep(step) {
|
|
86405
|
+
return ORACLE_ACTIONS.has(step.action);
|
|
86406
|
+
}
|
|
85644
86407
|
var CHROME_VISIBLE_RE = /\b(tab|home|settings|back|menu|navigation|nav bar|bottom nav|status bar)\b/i;
|
|
85645
86408
|
function hasStrongMobileOracle(steps) {
|
|
85646
86409
|
return steps.some(isStrongOracleStep);
|
|
@@ -85656,23 +86419,31 @@ var require_failure_triage = __commonJS({
|
|
|
85656
86419
|
step.target?.primary ?? "",
|
|
85657
86420
|
...step.target?.fallbacks ?? []
|
|
85658
86421
|
].join(" ");
|
|
85659
|
-
if (
|
|
86422
|
+
if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(step))
|
|
86423
|
+
return false;
|
|
86424
|
+
if ((0, oracle_quality_js_1.isToastOrAckOutcomeText)(step.assertion ?? step.value ?? ""))
|
|
86425
|
+
return false;
|
|
86426
|
+
if ((0, oracle_quality_js_1.textLooksLikeProductOutcome)(blob))
|
|
85660
86427
|
return true;
|
|
85661
|
-
|
|
85662
|
-
if (
|
|
86428
|
+
const anchor = [step.target?.primary ?? "", ...step.target?.fallbacks ?? []].join(" ").trim();
|
|
86429
|
+
if (!anchor)
|
|
85663
86430
|
return false;
|
|
85664
|
-
|
|
86431
|
+
if (CHROME_VISIBLE_RE.test(blob) || (0, oracle_quality_js_1.textLooksLikeChromeSurface)(blob))
|
|
86432
|
+
return false;
|
|
86433
|
+
return blob.trim().length >= 3;
|
|
85665
86434
|
}
|
|
85666
86435
|
function listStrongOracleKinds(steps) {
|
|
85667
86436
|
const kinds = /* @__PURE__ */ new Set();
|
|
86437
|
+
const hasDelta = steps.some((s) => s.action === "assertStateDelta");
|
|
85668
86438
|
for (const step of steps) {
|
|
85669
86439
|
if (step.action === "assertStateDelta")
|
|
85670
86440
|
kinds.add("assertStateDelta");
|
|
85671
86441
|
else if (step.action === "assertText")
|
|
85672
86442
|
kinds.add("assertText");
|
|
85673
|
-
else if (step.action === "captureState")
|
|
85674
|
-
|
|
85675
|
-
|
|
86443
|
+
else if (step.action === "captureState") {
|
|
86444
|
+
if (hasDelta)
|
|
86445
|
+
kinds.add("captureState");
|
|
86446
|
+
} else if (isStrongOracleStep(step))
|
|
85676
86447
|
kinds.add(step.action);
|
|
85677
86448
|
}
|
|
85678
86449
|
return [...kinds];
|
|
@@ -85694,6 +86465,8 @@ ${logs2 ?? ""}`.trim();
|
|
|
85694
86465
|
return true;
|
|
85695
86466
|
if (/Missing mobile test variable/i.test(text))
|
|
85696
86467
|
return true;
|
|
86468
|
+
if (/entry.?screen drift|screen drift|started on an unexpected screen/i.test(text))
|
|
86469
|
+
return true;
|
|
85697
86470
|
if (/\b(quota|rate.?limit|429|lockout)\b/i.test(text))
|
|
85698
86471
|
return true;
|
|
85699
86472
|
if (/\b(device (?:offline|disconnected|not found)|session (?:not started|terminated|deleted))\b/i.test(text)) {
|
|
@@ -85714,7 +86487,10 @@ ${logs2 ?? ""}`.trim();
|
|
|
85714
86487
|
return /not found|no such element|No identity locator|unable to locate/i.test(text);
|
|
85715
86488
|
}
|
|
85716
86489
|
function looksLikeOracleFailure(text) {
|
|
85717
|
-
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);
|
|
86490
|
+
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);
|
|
86491
|
+
}
|
|
86492
|
+
function looksLikeUnpairedStateDelta(text) {
|
|
86493
|
+
return /assertStateDelta[^\n]*no prior captureState/i.test(text);
|
|
85718
86494
|
}
|
|
85719
86495
|
function triageMobileFailure(input) {
|
|
85720
86496
|
const failureCount = Math.max(1, input.failureCount ?? 1);
|
|
@@ -85723,11 +86499,20 @@ ${logs2 ?? ""}`.trim();
|
|
|
85723
86499
|
const prefix = leadingPrefix(input.error);
|
|
85724
86500
|
const strongKinds = listStrongOracleKinds(input.steps);
|
|
85725
86501
|
const hasStrong = strongKinds.length > 0 || tags.some((t) => t === "@strong-oracle" || t === "strong-oracle");
|
|
86502
|
+
const failureText = (input.error ?? "").trim() || text;
|
|
86503
|
+
const failedStep = typeof input.failedStepOrder === "number" ? input.steps.find((s) => s.order === input.failedStepOrder) : void 0;
|
|
86504
|
+
const failedStepIsOracle = !!failedStep && isOracleStep(failedStep);
|
|
86505
|
+
const postHeal = input.postHeal === true;
|
|
86506
|
+
const locatorDrift = looksLikeLocatorMiss(failureText) && !failedStepIsOracle;
|
|
86507
|
+
const explicitOracleFailure = looksLikeOracleFailure(failureText);
|
|
86508
|
+
const missingOutcome = failedStepIsOracle && looksLikeLocatorMiss(failureText);
|
|
85726
86509
|
const evidence = {
|
|
85727
86510
|
hasStrongOracle: hasStrong,
|
|
85728
86511
|
strongOracleKinds: strongKinds,
|
|
85729
86512
|
failureCount,
|
|
85730
|
-
errorPrefix: prefix
|
|
86513
|
+
errorPrefix: prefix,
|
|
86514
|
+
...failedStep ? { failedStepAction: failedStep.action } : {},
|
|
86515
|
+
...failedStep ? { failedOnOracleStep: failedStepIsOracle } : {}
|
|
85731
86516
|
};
|
|
85732
86517
|
if ((0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(input.error, input.logs)) {
|
|
85733
86518
|
return {
|
|
@@ -85792,11 +86577,47 @@ ${logs2 ?? ""}`.trim();
|
|
|
85792
86577
|
evidence
|
|
85793
86578
|
};
|
|
85794
86579
|
}
|
|
85795
|
-
if (
|
|
85796
|
-
|
|
86580
|
+
if (looksLikeUnpairedStateDelta(failureText)) {
|
|
86581
|
+
return {
|
|
86582
|
+
verdict: "test_invalid",
|
|
86583
|
+
reason: "assertStateDelta has no prior captureState for its key \u2014 the test cannot prove a delta",
|
|
86584
|
+
shouldHeal: false,
|
|
86585
|
+
healVerdict: "test_invalid",
|
|
86586
|
+
evidence
|
|
86587
|
+
};
|
|
86588
|
+
}
|
|
86589
|
+
if (hasStrong && strongKinds.includes("assertStateDelta") && /assertStateDelta/i.test(failureText) && explicitOracleFailure && !locatorDrift) {
|
|
86590
|
+
return {
|
|
86591
|
+
verdict: "app_bug_candidate",
|
|
86592
|
+
reason: "assertStateDelta oracle failed \u2014 durable product state did not match expected outcome",
|
|
86593
|
+
shouldHeal: false,
|
|
86594
|
+
healVerdict: "app_bug",
|
|
86595
|
+
evidence
|
|
86596
|
+
};
|
|
86597
|
+
}
|
|
86598
|
+
if (hasStrong && (strongKinds.includes("assertText") || strongKinds.includes("assertVisible")) && explicitOracleFailure && !locatorDrift) {
|
|
86599
|
+
return {
|
|
86600
|
+
verdict: "app_bug_candidate",
|
|
86601
|
+
reason: `Strong oracle (${strongKinds.join(", ") || "tagged"}) failed with product-shaped error`,
|
|
86602
|
+
shouldHeal: false,
|
|
86603
|
+
healVerdict: "app_bug",
|
|
86604
|
+
evidence
|
|
86605
|
+
};
|
|
86606
|
+
}
|
|
86607
|
+
if (hasStrong && missingOutcome && postHeal) {
|
|
86608
|
+
return {
|
|
86609
|
+
verdict: "app_bug_candidate",
|
|
86610
|
+
reason: `Strong oracle (${strongKinds.join(", ") || "tagged"}) outcome never appeared and heal could not locate it`,
|
|
86611
|
+
shouldHeal: false,
|
|
86612
|
+
healVerdict: "app_bug",
|
|
86613
|
+
evidence
|
|
86614
|
+
};
|
|
86615
|
+
}
|
|
86616
|
+
if (hasStrong && failureCount >= 2) {
|
|
86617
|
+
if ((locatorDrift || missingOutcome) && !postHeal) {
|
|
85797
86618
|
return {
|
|
85798
86619
|
verdict: "needs_heal",
|
|
85799
|
-
reason: "Strong oracle present but
|
|
86620
|
+
reason: "Strong oracle present but the element was not found \u2014 attempt heal first",
|
|
85800
86621
|
shouldHeal: true,
|
|
85801
86622
|
evidence
|
|
85802
86623
|
};
|
|
@@ -85943,7 +86764,7 @@ var require_oracle_protection = __commonJS({
|
|
|
85943
86764
|
return `heal patch dropped assertText outcome proof (${before.assertTexts.length} \u2192 ${after.assertTexts.length})`;
|
|
85944
86765
|
}
|
|
85945
86766
|
if (lost.length > 0) {
|
|
85946
|
-
const softKept = lost.every((t) => after.assertTexts.some((a) => a
|
|
86767
|
+
const softKept = lost.every((t) => after.assertTexts.some((a) => a === t || a.includes(t)));
|
|
85947
86768
|
if (!softKept) {
|
|
85948
86769
|
return `heal patch replaced assertText expectations (${lost.slice(0, 2).join("; ")}) \u2014 outcome must stay`;
|
|
85949
86770
|
}
|
|
@@ -85969,6 +86790,23 @@ var require_oracle_protection = __commonJS({
|
|
|
85969
86790
|
return `heal patch downgraded strong oracle "${key.slice(0, 60)}" to assertVisible`;
|
|
85970
86791
|
}
|
|
85971
86792
|
}
|
|
86793
|
+
const OUTCOME_LANG = /\b(error|invalid|required|empty|saved|success|created|updated|deleted|removed|failed|missing)\b/i;
|
|
86794
|
+
const CHROME_TARGET = /\b(tab|home|settings|back|menu|navigation|nav bar|bottom nav|status bar)\b/i;
|
|
86795
|
+
for (const orig of original) {
|
|
86796
|
+
if (orig.action !== "assertVisible" || !(0, failure_triage_js_1.isStrongOracleStep)(orig))
|
|
86797
|
+
continue;
|
|
86798
|
+
const origBlob = [orig.assertion, orig.description, orig.target?.primary].filter(Boolean).join(" ");
|
|
86799
|
+
if (!OUTCOME_LANG.test(origBlob))
|
|
86800
|
+
continue;
|
|
86801
|
+
const key = normalizeText(orig.description ?? orig.assertion ?? "");
|
|
86802
|
+
const match = patched.find((p) => p.action === "assertVisible" && normalizeText(p.description ?? p.assertion ?? "") === key);
|
|
86803
|
+
if (!match)
|
|
86804
|
+
continue;
|
|
86805
|
+
const newTarget = match.target?.primary ?? "";
|
|
86806
|
+
if (CHROME_TARGET.test(newTarget) && !OUTCOME_LANG.test(newTarget)) {
|
|
86807
|
+
return `heal patch retargeted outcome assert "${key.slice(0, 60)}" onto chrome`;
|
|
86808
|
+
}
|
|
86809
|
+
}
|
|
85972
86810
|
return null;
|
|
85973
86811
|
}
|
|
85974
86812
|
function oraclesPreserved(original, patched) {
|
|
@@ -86059,14 +86897,14 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
86059
86897
|
isVerifiedFix = false;
|
|
86060
86898
|
const anrEnv = (0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(heal.healReason ?? args.finalError ?? args.reproduceError, typeof args.finalError === "string" ? args.finalError : void 0);
|
|
86061
86899
|
const isConfigIssue = heal.verdict === "config_issue" || heal.verdict === "app_bug" && anrEnv;
|
|
86062
|
-
const isAppBug = !anrEnv &&
|
|
86900
|
+
const isAppBug = !anrEnv && heal.verdict === "app_bug";
|
|
86063
86901
|
const extraTags = new Set((heal.extraTags ?? []).filter((t) => typeof t === "string" && t.length > 0));
|
|
86064
86902
|
if (secretLeak)
|
|
86065
86903
|
extraTags.add("@needs-review");
|
|
86066
|
-
if (
|
|
86067
|
-
extraTags.add("@needs-review");
|
|
86904
|
+
if (isAppBug)
|
|
86068
86905
|
extraTags.add("@bug-found");
|
|
86069
|
-
|
|
86906
|
+
if (oracleWeakening && !anrEnv)
|
|
86907
|
+
extraTags.add("@needs-review");
|
|
86070
86908
|
if (anrEnv) {
|
|
86071
86909
|
extraTags.delete("@bug-found");
|
|
86072
86910
|
extraTags.add("@needs-review");
|
|
@@ -86090,7 +86928,7 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
86090
86928
|
} else if (secretLeak) {
|
|
86091
86929
|
error2 = `HEAL_SECRET_LEAK: healed steps contained a literal secret (${secretLeak}); patch dropped, tagged @needs-review`;
|
|
86092
86930
|
} else if (oracleWeakening && !anrEnv) {
|
|
86093
|
-
error2 = `
|
|
86931
|
+
error2 = `HEAL_ORACLE_REFUSED: heal attempted to weaken a strong oracle (${oracleWeakening}); patch refused, tagged @needs-review`;
|
|
86094
86932
|
} else if (isConfigIssue) {
|
|
86095
86933
|
const reason = heal.healReason ?? args.finalError ?? args.reproduceError ?? "environment/rate-limit blocker";
|
|
86096
86934
|
error2 = anrEnv ? `CONFIG_ISSUE: ANR / System UI freeze (not a product assertion failure) \u2014 ${reason}` : `CONFIG_ISSUE: ${reason}`;
|
|
@@ -86547,7 +87385,7 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
86547
87385
|
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.
|
|
86548
87386
|
2. STEP RESULTS: which steps passed before the failure.
|
|
86549
87387
|
3. TREE: does the current screen match what the failed step expects?
|
|
86550
|
-
3b.
|
|
87388
|
+
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.
|
|
86551
87389
|
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.
|
|
86552
87390
|
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.
|
|
86553
87391
|
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.
|
|
@@ -86798,6 +87636,14 @@ For a fix, return the COMPLETE steps array (not a diff).
|
|
|
86798
87636
|
function isIdentityEqualityXpath(value) {
|
|
86799
87637
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
86800
87638
|
}
|
|
87639
|
+
var STEP_ACTIONS_NEEDING_ELEMENT = /* @__PURE__ */ new Set([
|
|
87640
|
+
"tap",
|
|
87641
|
+
"type",
|
|
87642
|
+
"clear",
|
|
87643
|
+
"waitForElement",
|
|
87644
|
+
"assertVisible",
|
|
87645
|
+
"assertText"
|
|
87646
|
+
]);
|
|
86801
87647
|
function stepLacksIdentityAnchor(step) {
|
|
86802
87648
|
const metadata = step.metadata;
|
|
86803
87649
|
if (metadata && (metadata.appContext === "WEBVIEW" || typeof metadata.webCss === "string"))
|
|
@@ -86838,7 +87684,7 @@ For a fix, return the COMPLETE steps array (not a diff).
|
|
|
86838
87684
|
async function tryIdentityGateStaticRepair(steps, target, ctx, options) {
|
|
86839
87685
|
if (!ctx.error || !IDENTITY_GATE_ERROR_RE.test(ctx.error))
|
|
86840
87686
|
return null;
|
|
86841
|
-
const identityless = steps.filter(stepLacksIdentityAnchor);
|
|
87687
|
+
const identityless = steps.filter((step) => STEP_ACTIONS_NEEDING_ELEMENT.has(step.action) && stepLacksIdentityAnchor(step));
|
|
86842
87688
|
if (identityless.length === 0)
|
|
86843
87689
|
return null;
|
|
86844
87690
|
const isAssert = (step) => /^assert/i.test(step.action);
|
|
@@ -86849,7 +87695,8 @@ For a fix, return the COMPLETE steps array (not a diff).
|
|
|
86849
87695
|
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"
|
|
86850
87696
|
};
|
|
86851
87697
|
}
|
|
86852
|
-
const
|
|
87698
|
+
const identitylessOrders = new Set(identityless.map((step) => step.order));
|
|
87699
|
+
const pruned = steps.filter((step) => !identitylessOrders.has(step.order));
|
|
86853
87700
|
if (pruned.length === 0 || !pruned.some(isAssert) || !options.runNativeTest) {
|
|
86854
87701
|
return {
|
|
86855
87702
|
passed: false,
|
|
@@ -86857,6 +87704,14 @@ For a fix, return the COMPLETE steps array (not a diff).
|
|
|
86857
87704
|
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"
|
|
86858
87705
|
};
|
|
86859
87706
|
}
|
|
87707
|
+
const identityOracleRejection = (0, oracle_protection_js_1.rejectOracleWeakeningPatch)(steps, pruned);
|
|
87708
|
+
if (identityOracleRejection) {
|
|
87709
|
+
return {
|
|
87710
|
+
passed: false,
|
|
87711
|
+
verdict: "app_bug",
|
|
87712
|
+
healReason: `identity gate: pruning identity-less asserts would drop product oracles (${identityOracleRejection})`
|
|
87713
|
+
};
|
|
87714
|
+
}
|
|
86860
87715
|
const reindexed = pruned.map((step, order) => ({ ...step, order }));
|
|
86861
87716
|
try {
|
|
86862
87717
|
const verify = await options.runNativeTest({ steps: reindexed, target, testVariables: ctx.testVariables });
|
|
@@ -87113,7 +87968,12 @@ ${safeLastError}`);
|
|
|
87113
87968
|
${args.directive}`);
|
|
87114
87969
|
return parts.join("\n\n");
|
|
87115
87970
|
}
|
|
87116
|
-
function findFailingStepIndex(steps, error2) {
|
|
87971
|
+
function findFailingStepIndex(steps, error2, failedStepOrder) {
|
|
87972
|
+
if (typeof failedStepOrder === "number") {
|
|
87973
|
+
const exact = steps.findIndex((step) => step.order === failedStepOrder);
|
|
87974
|
+
if (exact >= 0)
|
|
87975
|
+
return exact;
|
|
87976
|
+
}
|
|
87117
87977
|
const withTarget = steps.map((step, index) => ({ step, index })).filter((entry) => Boolean(entry.step.target?.primary));
|
|
87118
87978
|
if (withTarget.length === 0)
|
|
87119
87979
|
return -1;
|
|
@@ -87149,9 +88009,13 @@ ${args.directive}`);
|
|
|
87149
88009
|
if (!runNativeTest || !options.selectorRegistryContext || !ctx.error)
|
|
87150
88010
|
return null;
|
|
87151
88011
|
const candidates = (0, selector_registry_context_js_1.collectMobileRegistryCandidates)(options.selectorRegistryContext);
|
|
87152
|
-
const failingIndex = findFailingStepIndex(steps, ctx.error);
|
|
88012
|
+
const failingIndex = findFailingStepIndex(steps, ctx.error, ctx.failedStepOrder);
|
|
87153
88013
|
if (failingIndex < 0)
|
|
87154
88014
|
return null;
|
|
88015
|
+
if ((0, failure_triage_js_1.isOracleStep)(steps[failingIndex])) {
|
|
88016
|
+
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`);
|
|
88017
|
+
return null;
|
|
88018
|
+
}
|
|
87155
88019
|
let tried = 0;
|
|
87156
88020
|
for (const candidate of candidates) {
|
|
87157
88021
|
if (tried >= REGISTRY_PREFLIGHT_MAX_CANDIDATES)
|
|
@@ -87161,6 +88025,8 @@ ${args.directive}`);
|
|
|
87161
88025
|
continue;
|
|
87162
88026
|
tried += 1;
|
|
87163
88027
|
const healedSteps = substituteStepTarget(steps, failingIndex, candidate, mapped.locatorBundle);
|
|
88028
|
+
if ((0, oracle_protection_js_1.rejectOracleWeakeningPatch)(steps, healedSteps))
|
|
88029
|
+
continue;
|
|
87164
88030
|
let verify;
|
|
87165
88031
|
try {
|
|
87166
88032
|
verify = await runNativeTest({ steps: healedSteps, target, testVariables: ctx.testVariables });
|
|
@@ -87486,6 +88352,41 @@ ${args.directive}`);
|
|
|
87486
88352
|
async function executeMobileTestWithHealing2(steps, target, ctx, options) {
|
|
87487
88353
|
return timed("total", "heal-total", options.onProgress, () => executeMobileTestWithHealingImpl(steps, target, ctx, options));
|
|
87488
88354
|
}
|
|
88355
|
+
var TERMINAL_HEAL_VERDICTS = /* @__PURE__ */ new Set([
|
|
88356
|
+
"app_bug",
|
|
88357
|
+
"config_issue",
|
|
88358
|
+
"prereq_unmet",
|
|
88359
|
+
"state_stale",
|
|
88360
|
+
"test_invalid",
|
|
88361
|
+
"unhealable_dynamic"
|
|
88362
|
+
]);
|
|
88363
|
+
function escalateFailedHealToTriage(steps, ctx, lastError) {
|
|
88364
|
+
const post = (0, failure_triage_js_1.triageMobileFailure)({
|
|
88365
|
+
steps,
|
|
88366
|
+
error: lastError ?? ctx.error,
|
|
88367
|
+
logs: ctx.logs,
|
|
88368
|
+
tags: ctx.tags,
|
|
88369
|
+
// Heal already ran and failed. Bump past the locator-miss heal window
|
|
88370
|
+
// so a remaining strong-oracle fail becomes APP_BUG, not silent FAILED.
|
|
88371
|
+
failureCount: Math.max(3, (ctx.failureCount ?? 1) + 1),
|
|
88372
|
+
failedStepOrder: ctx.failedStepOrder,
|
|
88373
|
+
// Heal ran and failed: a still-missing element is no longer "maybe drift".
|
|
88374
|
+
postHeal: true,
|
|
88375
|
+
uiSuccessWithFailedWriteApi: (0, network_oracle_js_1.uiSuccessWithFailedWriteApi)({
|
|
88376
|
+
apiCalls: ctx.apiCalls,
|
|
88377
|
+
uiPathSucceeded: ctx.uiPathSucceeded
|
|
88378
|
+
})
|
|
88379
|
+
});
|
|
88380
|
+
const term = (0, failure_triage_js_1.triageToHealTerminal)(post);
|
|
88381
|
+
if (!term)
|
|
88382
|
+
return null;
|
|
88383
|
+
return {
|
|
88384
|
+
passed: false,
|
|
88385
|
+
verdict: term.verdict,
|
|
88386
|
+
healReason: term.healReason,
|
|
88387
|
+
extraTags: term.extraTags
|
|
88388
|
+
};
|
|
88389
|
+
}
|
|
87489
88390
|
async function executeMobileTestWithHealingImpl(steps, target, ctx, options) {
|
|
87490
88391
|
const invalidAuthSetup = detectInvalidMobileAuthSetup(steps, ctx.error);
|
|
87491
88392
|
if (invalidAuthSetup) {
|
|
@@ -87501,6 +88402,7 @@ ${args.directive}`);
|
|
|
87501
88402
|
logs: ctx.logs,
|
|
87502
88403
|
tags: ctx.tags,
|
|
87503
88404
|
failureCount: ctx.failureCount,
|
|
88405
|
+
failedStepOrder: ctx.failedStepOrder,
|
|
87504
88406
|
uiSuccessWithFailedWriteApi: networkBug
|
|
87505
88407
|
});
|
|
87506
88408
|
options.onProgress?.(`Pre-heal triage: ${triage.verdict} \u2014 ${triage.reason}`);
|
|
@@ -87558,6 +88460,17 @@ ${args.directive}`);
|
|
|
87558
88460
|
if (live.providerUnavailable) {
|
|
87559
88461
|
return await tryDeterministicProviderFallback({ steps, target, ctx, options }) ?? live;
|
|
87560
88462
|
}
|
|
88463
|
+
if (!live.passed && !TERMINAL_HEAL_VERDICTS.has(live.verdict)) {
|
|
88464
|
+
const escalated = escalateFailedHealToTriage(steps, ctx, live.healReason ?? ctx.error);
|
|
88465
|
+
if (escalated) {
|
|
88466
|
+
return {
|
|
88467
|
+
...live,
|
|
88468
|
+
...escalated,
|
|
88469
|
+
healedSteps: live.healedSteps,
|
|
88470
|
+
healedAppiumCode: live.healedAppiumCode
|
|
88471
|
+
};
|
|
88472
|
+
}
|
|
88473
|
+
}
|
|
87561
88474
|
return live;
|
|
87562
88475
|
} catch {
|
|
87563
88476
|
return {
|
|
@@ -87569,6 +88482,21 @@ ${args.directive}`);
|
|
|
87569
88482
|
};
|
|
87570
88483
|
}
|
|
87571
88484
|
}
|
|
88485
|
+
if (!script.passed && !TERMINAL_HEAL_VERDICTS.has(script.verdict)) {
|
|
88486
|
+
const escalated = escalateFailedHealToTriage(steps, ctx, script.lastError ?? script.healReason ?? ctx.error);
|
|
88487
|
+
if (escalated) {
|
|
88488
|
+
return {
|
|
88489
|
+
passed: false,
|
|
88490
|
+
verdict: escalated.verdict,
|
|
88491
|
+
healedSteps: script.healedSteps,
|
|
88492
|
+
healedAppiumCode: script.healedAppiumCode,
|
|
88493
|
+
healReason: escalated.healReason,
|
|
88494
|
+
extraTags: escalated.extraTags,
|
|
88495
|
+
confidence: script.confidence,
|
|
88496
|
+
removeTags: script.removeTags
|
|
88497
|
+
};
|
|
88498
|
+
}
|
|
88499
|
+
}
|
|
87572
88500
|
return {
|
|
87573
88501
|
passed: script.passed,
|
|
87574
88502
|
healedSteps: script.healedSteps,
|
|
@@ -87589,7 +88517,12 @@ The recorded flow may have expected a DIFFERENT starting screen than the one sho
|
|
|
87589
88517
|
1. Compare the recorded first step's expected screen to the LIVE tree.
|
|
87590
88518
|
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().
|
|
87591
88519
|
3. If a locator is simply wrong, swap it for a stronger identity locator present in the LIVE tree.
|
|
87592
|
-
3b. If the failing step ASSERTS
|
|
88520
|
+
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.
|
|
88521
|
+
## Strong oracle protection (non-negotiable)
|
|
88522
|
+
- NEVER remove or downgrade assertStateDelta, captureState, or assertText outcome proof to go green.
|
|
88523
|
+
- NEVER replace a durable outcome assert with mere assertVisible of a tab/header.
|
|
88524
|
+
- You MAY fix locators, waits, and navigation around those oracles.
|
|
88525
|
+
- If the app truly violates the oracle on a healthy UI, return verdict "app_bug" instead of stripping the assert.
|
|
87593
88526
|
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.
|
|
87594
88527
|
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.
|
|
87595
88528
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
@@ -87598,7 +88531,7 @@ The recorded flow may have expected a DIFFERENT starting screen than the one sho
|
|
|
87598
88531
|
{"verdict":"observe","action":{"type":"back"}}
|
|
87599
88532
|
{"verdict":"observe","action":{"type":"relaunch"}}
|
|
87600
88533
|
{"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)
|
|
87601
|
-
{"verdict":"app_bug","reason":"
|
|
88534
|
+
{"verdict":"app_bug","reason":"product defect with durable oracle evidence (wrong entity, validation bypass, wrong count, missing confirmation) \u2014 NOT ANR/System UI"}
|
|
87602
88535
|
{"verdict":"config_issue","reason":"429/rate limit/lockout/quota"}
|
|
87603
88536
|
{"verdict":"needs_device","reason":"cannot resolve even with live access"}
|
|
87604
88537
|
{"verdict":"prereq_unmet","reason":"required entity/state absent for the account"}
|
|
@@ -87849,6 +88782,7 @@ var require_browser_config = __commonJS({
|
|
|
87849
88782
|
var path_1 = __require("path");
|
|
87850
88783
|
var os_1 = __require("os");
|
|
87851
88784
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
88785
|
+
var google_signin_js_1 = require_google_signin();
|
|
87852
88786
|
exports.IS_CLOUD = (0, shared_1.parseBoolFlag)(process.env.RUNNER_CLOUD);
|
|
87853
88787
|
exports.CLOUD_ARGS = [
|
|
87854
88788
|
"--no-sandbox",
|
|
@@ -87954,6 +88888,10 @@ var require_browser_config = __commonJS({
|
|
|
87954
88888
|
(document.head || document.documentElement).appendChild(style);
|
|
87955
88889
|
|
|
87956
88890
|
// Layer 2: Web Animations API - make instant (preserves completion callbacks)
|
|
88891
|
+
try {
|
|
88892
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
88893
|
+
} catch {}
|
|
88894
|
+
|
|
87957
88895
|
const origAnimate = Element.prototype.animate;
|
|
87958
88896
|
Element.prototype.animate = function(keyframes, options) {
|
|
87959
88897
|
if (typeof options === 'object' && options !== null) {
|
|
@@ -88060,6 +88998,20 @@ var require_browser_config = __commonJS({
|
|
|
88060
88998
|
return null;
|
|
88061
88999
|
}
|
|
88062
89000
|
}
|
|
89001
|
+
function googleSsoCloudLaunch(env = process.env) {
|
|
89002
|
+
if (!(0, shared_1.parseBoolFlag)(env.RUNNER_CLOUD))
|
|
89003
|
+
return null;
|
|
89004
|
+
if (!(0, google_signin_js_1.loadGoogleSignInConfig)(env))
|
|
89005
|
+
return null;
|
|
89006
|
+
const executablePath = getFullChromiumPath();
|
|
89007
|
+
if (!executablePath)
|
|
89008
|
+
return null;
|
|
89009
|
+
return {
|
|
89010
|
+
executablePath,
|
|
89011
|
+
extraArgs: ["--headless=new"],
|
|
89012
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
89013
|
+
};
|
|
89014
|
+
}
|
|
88063
89015
|
function getMCPBrowserConfig2(opts) {
|
|
88064
89016
|
const userDataDir = opts?.forceIsolated ? null : getTestUserDataDir2();
|
|
88065
89017
|
const headless = isHeadless2();
|
|
@@ -88083,15 +89035,21 @@ var require_browser_config = __commonJS({
|
|
|
88083
89035
|
};
|
|
88084
89036
|
}
|
|
88085
89037
|
const headlessShellPath = getHeadlessShellPath();
|
|
88086
|
-
const
|
|
89038
|
+
const googleLaunch = googleSsoCloudLaunch();
|
|
89039
|
+
const useHeadlessShell = headless && headlessShellPath && !opts?.harPath && !googleLaunch;
|
|
89040
|
+
const cloud = (0, shared_1.parseBoolFlag)(process.env.RUNNER_CLOUD);
|
|
89041
|
+
const args = [
|
|
89042
|
+
...cloud ? exports.CLOUD_ARGS : ["--disable-blink-features=AutomationControlled"],
|
|
89043
|
+
...googleLaunch?.extraArgs ?? []
|
|
89044
|
+
];
|
|
88087
89045
|
return {
|
|
88088
89046
|
browserName: "chromium",
|
|
88089
89047
|
isolated: true,
|
|
88090
89048
|
launchOptions: {
|
|
88091
|
-
headless,
|
|
88092
|
-
...useHeadlessShell ? { executablePath: headlessShellPath } : {},
|
|
89049
|
+
headless: googleLaunch ? false : headless,
|
|
89050
|
+
...googleLaunch ? { executablePath: googleLaunch.executablePath, ignoreDefaultArgs: googleLaunch.ignoreDefaultArgs } : useHeadlessShell ? { executablePath: headlessShellPath } : {},
|
|
88093
89051
|
channel: "",
|
|
88094
|
-
args
|
|
89052
|
+
args,
|
|
88095
89053
|
...slowMo > 0 ? { slowMo } : {}
|
|
88096
89054
|
},
|
|
88097
89055
|
contextOptions: {
|
|
@@ -88104,15 +89062,21 @@ var require_browser_config = __commonJS({
|
|
|
88104
89062
|
function getPlaywrightLaunchOptions2(withProfile = false, extraArgs) {
|
|
88105
89063
|
const headless = isHeadless2();
|
|
88106
89064
|
const slowMo = getSlowMoMs();
|
|
89065
|
+
const googleLaunch = googleSsoCloudLaunch();
|
|
88107
89066
|
const baseArgs = exports.IS_CLOUD ? exports.CLOUD_ARGS : [
|
|
88108
89067
|
"--disable-blink-features=AutomationControlled"
|
|
88109
89068
|
];
|
|
88110
|
-
const mergedArgs =
|
|
89069
|
+
const mergedArgs = [
|
|
89070
|
+
...baseArgs,
|
|
89071
|
+
...googleLaunch?.extraArgs ?? [],
|
|
89072
|
+
...extraArgs && extraArgs.length > 0 ? extraArgs : []
|
|
89073
|
+
];
|
|
88111
89074
|
return {
|
|
88112
|
-
headless,
|
|
89075
|
+
headless: googleLaunch ? false : headless,
|
|
88113
89076
|
// Use Chrome channel only when a real Chrome user profile is being loaded.
|
|
88114
89077
|
// In isolated mode, bundled Chromium avoids hard dependency on Chrome installation.
|
|
88115
89078
|
...exports.IS_CLOUD || !withProfile ? {} : { channel: "chrome" },
|
|
89079
|
+
...googleLaunch ? { executablePath: googleLaunch.executablePath, ignoreDefaultArgs: googleLaunch.ignoreDefaultArgs } : {},
|
|
88116
89080
|
args: mergedArgs,
|
|
88117
89081
|
...slowMo > 0 ? { slowMo } : {}
|
|
88118
89082
|
};
|
|
@@ -91665,7 +92629,7 @@ var require_kill_tree = __commonJS({
|
|
|
91665
92629
|
var node_child_process_1 = __require("child_process");
|
|
91666
92630
|
var node_util_1 = __require("util");
|
|
91667
92631
|
var node_fs_1 = __require("fs");
|
|
91668
|
-
var
|
|
92632
|
+
var execFileAsync5 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
91669
92633
|
var PS_TIMEOUT_MS = 2e3;
|
|
91670
92634
|
var PS_BUFFER = 1024 * 1024;
|
|
91671
92635
|
async function gracefulCloseWithTreeKill(options) {
|
|
@@ -91719,7 +92683,7 @@ var require_kill_tree = __commonJS({
|
|
|
91719
92683
|
timer.unref?.();
|
|
91720
92684
|
let stdout;
|
|
91721
92685
|
try {
|
|
91722
|
-
const result = await
|
|
92686
|
+
const result = await execFileAsync5("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
91723
92687
|
maxBuffer: PS_BUFFER,
|
|
91724
92688
|
signal: controller.signal
|
|
91725
92689
|
});
|
|
@@ -91757,7 +92721,7 @@ var require_kill_tree = __commonJS({
|
|
|
91757
92721
|
timer.unref?.();
|
|
91758
92722
|
let stdout;
|
|
91759
92723
|
try {
|
|
91760
|
-
const result = await
|
|
92724
|
+
const result = await execFileAsync5("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
91761
92725
|
maxBuffer: PS_BUFFER,
|
|
91762
92726
|
signal: controller.signal
|
|
91763
92727
|
});
|
|
@@ -91834,7 +92798,7 @@ var require_kill_tree = __commonJS({
|
|
|
91834
92798
|
timer.unref?.();
|
|
91835
92799
|
let stdout;
|
|
91836
92800
|
try {
|
|
91837
|
-
const result = await
|
|
92801
|
+
const result = await execFileAsync5("ps", ["-A", "-o", "pid=,ppid="], {
|
|
91838
92802
|
maxBuffer: PS_BUFFER,
|
|
91839
92803
|
signal: controller.signal
|
|
91840
92804
|
});
|
|
@@ -101410,13 +102374,12 @@ Every assertion in the generated test must be proven live.
|
|
|
101410
102374
|
**Frame**
|
|
101411
102375
|
- \`import { test, expect } from '@playwright/test'\` with tags [${tagLiteral}].
|
|
101412
102376
|
- 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\`.`}
|
|
101413
|
-
- **Credential rotation (
|
|
101414
|
-
(a)
|
|
101415
|
-
(b)
|
|
101416
|
-
(
|
|
101417
|
-
|
|
101418
|
-
- **
|
|
101419
|
-
- **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.
|
|
102377
|
+
- **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:
|
|
102378
|
+
(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
|
|
102379
|
+
(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.
|
|
102380
|
+
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).
|
|
102381
|
+
- **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.
|
|
102382
|
+
- **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.
|
|
101420
102383
|
- **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.
|
|
101421
102384
|
- ${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."}
|
|
101422
102385
|
- Always \`await\` on \`expect()\`. Never use \`test.tags()\` as a method \u2014 tags go in the options object.
|
|
@@ -102030,7 +102993,7 @@ Fix EVERY flagged locator, keep every other line exactly as-is, then submit the
|
|
|
102030
102993
|
}
|
|
102031
102994
|
const verifyCallsSinceLastFinish = verifyCallCount - verifyCallsAtLastFinishGenerate;
|
|
102032
102995
|
verifyCallsAtLastFinishGenerate = verifyCallCount;
|
|
102033
|
-
const bugWitnessEnabled = (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_BUG_WITNESS_ENABLED);
|
|
102996
|
+
const bugWitnessEnabled = (0, shared_1.parseBoolFlag)(process.env.DISCOVERY_BUG_WITNESS_ENABLED, true);
|
|
102034
102997
|
const bugWitnessVerdict = bugWitnessEnabled ? resolveSuspectedBugVerdict(suspectedBugDeclared, bugDescription, verifyCallsSinceLastFinish, strengthVerdict?.signals) : null;
|
|
102035
102998
|
if (bugWitnessVerdict) {
|
|
102036
102999
|
strengthVerdict = bugWitnessVerdict;
|
|
@@ -319455,6 +320418,7 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319455
320418
|
var promises_1 = __require("fs/promises");
|
|
319456
320419
|
var node_path_1 = __require("path");
|
|
319457
320420
|
var browser_config_js_1 = require_browser_config();
|
|
320421
|
+
var google_signin_js_1 = require_google_signin();
|
|
319458
320422
|
var PLAYWRIGHT_MCP_VERSION = "0.0.68";
|
|
319459
320423
|
var INBOX_SECRET_ENV_KEYS = [
|
|
319460
320424
|
"VERIFICATION_INBOX_HOST",
|
|
@@ -319464,7 +320428,10 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319464
320428
|
"VERIFICATION_INBOX_PASSWORD",
|
|
319465
320429
|
"VERIFICATION_INBOX_ADDRESS",
|
|
319466
320430
|
"VERIFICATION_INBOX_MAILBOX",
|
|
319467
|
-
"VERIFICATION_INBOX_EXTRA_MAILBOXES"
|
|
320431
|
+
"VERIFICATION_INBOX_EXTRA_MAILBOXES",
|
|
320432
|
+
"GOOGLE_SIGNIN_PASSWORD",
|
|
320433
|
+
"GOOGLE_SIGNIN_EMAIL",
|
|
320434
|
+
"GOOGLE_SIGNIN_TOTP_SECRET"
|
|
319468
320435
|
];
|
|
319469
320436
|
function collectInboxSecretEnv() {
|
|
319470
320437
|
const out = {};
|
|
@@ -319505,12 +320472,13 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319505
320472
|
const body = cfg.format === "json" ? buildJsonConfig(cfg) : buildTomlConfig(cfg);
|
|
319506
320473
|
await (0, promises_1.writeFile)(cfg.configPath, body, { encoding: "utf8", mode: 384 });
|
|
319507
320474
|
if (!cfg.cdpEndpoint && browser_config_js_1.IS_CLOUD) {
|
|
320475
|
+
const launchOptions = (0, browser_config_js_1.getMCPBrowserConfig)({ forceIsolated: true }).launchOptions ?? {
|
|
320476
|
+
headless: true,
|
|
320477
|
+
args: [...browser_config_js_1.CLOUD_ARGS]
|
|
320478
|
+
};
|
|
319508
320479
|
const browserConfig = {
|
|
319509
320480
|
browser: {
|
|
319510
|
-
launchOptions
|
|
319511
|
-
headless: true,
|
|
319512
|
-
args: [...browser_config_js_1.CLOUD_ARGS]
|
|
319513
|
-
}
|
|
320481
|
+
launchOptions
|
|
319514
320482
|
}
|
|
319515
320483
|
};
|
|
319516
320484
|
await (0, promises_1.writeFile)(mcpBrowserConfigPath(cfg.configPath), JSON.stringify(browserConfig, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
@@ -319534,7 +320502,11 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319534
320502
|
];
|
|
319535
320503
|
if (cfg.includeCredentialServer && cfg.cdpEndpoint) {
|
|
319536
320504
|
const serverJs = (0, node_path_1.join)(__dirname, "credential-mcp-server.js");
|
|
319537
|
-
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)}"
|
|
320505
|
+
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)}"`);
|
|
320506
|
+
for (const [k, v] of Object.entries((0, google_signin_js_1.collectGoogleSignInEnv)())) {
|
|
320507
|
+
blocks.push(`${k} = "${tomlEscape(v)}"`);
|
|
320508
|
+
}
|
|
320509
|
+
blocks.push("");
|
|
319538
320510
|
}
|
|
319539
320511
|
if (cfg.includeDiscoveryServer) {
|
|
319540
320512
|
if (!cfg.runDir) {
|
|
@@ -319582,7 +320554,7 @@ var require_playwright_mcp_config = __commonJS({
|
|
|
319582
320554
|
type: "stdio",
|
|
319583
320555
|
command: "node",
|
|
319584
320556
|
args: [serverJs],
|
|
319585
|
-
env: { CDP_ENDPOINT: cfg.cdpEndpoint }
|
|
320557
|
+
env: { CDP_ENDPOINT: cfg.cdpEndpoint, ...(0, google_signin_js_1.collectGoogleSignInEnv)() }
|
|
319586
320558
|
};
|
|
319587
320559
|
}
|
|
319588
320560
|
if (cfg.includeDiscoveryServer) {
|
|
@@ -327789,6 +328761,7 @@ var require_discover_explorer = __commonJS({
|
|
|
327789
328761
|
var credential_tools_js_1 = require_credential_tools();
|
|
327790
328762
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
327791
328763
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
328764
|
+
var google_signin_js_1 = require_google_signin();
|
|
327792
328765
|
var discover_planner_js_1 = require_discover_planner();
|
|
327793
328766
|
var exploration_prompt_builder_js_1 = require_exploration_prompt_builder();
|
|
327794
328767
|
var ai_queue_js_1 = require_ai_queue();
|
|
@@ -328193,7 +329166,8 @@ NEVER assert: user-specific text (names, emails), counts/numbers, timestamps, ma
|
|
|
328193
329166
|
};
|
|
328194
329167
|
function buildSystemPrompt(baseUrl, variableContext, existingSiteMap, existingTestNames, options) {
|
|
328195
329168
|
const browserCredentials = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
328196
|
-
const { secrets:
|
|
329169
|
+
const { secrets: explorerSecretVarsRaw, publics: explorerPublicVarsRaw } = (0, credential_tools_js_1.partitionTestVariables)(browserCredentials);
|
|
329170
|
+
const explorerSecretVars = options?.inboxPrompt ? (0, google_signin_js_1.mergeGoogleSignInSecrets)(explorerSecretVarsRaw) : explorerSecretVarsRaw;
|
|
328197
329171
|
const hasCredentials = Object.keys(explorerSecretVars).length > 0;
|
|
328198
329172
|
const { DISCOVERY_SHOULD_REGISTER: _omitRegisterFlag, ...explorerPublicVars } = explorerPublicVarsRaw;
|
|
328199
329173
|
const explorerPublicLines = (0, credential_tools_js_1.renderPublicVariablesBlock)(explorerPublicVars);
|
|
@@ -328810,10 +329784,12 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
328810
329784
|
if (emailVerificationUnavailable) {
|
|
328811
329785
|
logs2.push("Email verification unavailable \u2014 the agent will ATTEMPT registration once; any flow requiring an emailed code/link will be reported BLOCKED.");
|
|
328812
329786
|
}
|
|
329787
|
+
const googleGuidance = (0, google_signin_js_1.googleSignInGuidanceOpts)();
|
|
328813
329788
|
const inboxPromptBlock = inboxState ? (0, inbox_tools_js_1.buildInboxPromptBlock)({
|
|
328814
329789
|
email: inboxState.email,
|
|
328815
329790
|
passwordKey: inboxState.passwordKey ?? "TEST_USER_PASSWORD",
|
|
328816
|
-
context: options?.mode
|
|
329791
|
+
context: options?.mode,
|
|
329792
|
+
...googleGuidance
|
|
328817
329793
|
}) : "";
|
|
328818
329794
|
try {
|
|
328819
329795
|
const { tools: mcpTools } = await mcpClient.listTools();
|
|
@@ -328834,7 +329810,8 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
328834
329810
|
}
|
|
328835
329811
|
const browserCredsForTools = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
328836
329812
|
const { secrets: explorerToolSecrets } = (0, credential_tools_js_1.partitionTestVariables)(browserCredsForTools);
|
|
328837
|
-
|
|
329813
|
+
const explorerToolSecretsWithGoogle = inboxState ? (0, google_signin_js_1.mergeGoogleSignInSecrets)(explorerToolSecrets) : explorerToolSecrets;
|
|
329814
|
+
if (Object.keys(explorerToolSecretsWithGoogle).length > 0) {
|
|
328838
329815
|
openaiTools.push(credential_tools_js_1.FILL_CREDENTIAL_TOOL, credential_tools_js_1.FILL_CREDENTIAL_FORM_TOOL);
|
|
328839
329816
|
if (options?.onCredentialUpdate) {
|
|
328840
329817
|
openaiTools.push(credential_tools_js_1.SET_CREDENTIAL_TOOL);
|
|
@@ -329474,8 +330451,12 @@ ${currentSummary}`;
|
|
|
329474
330451
|
let finishBouncedThisBatch = false;
|
|
329475
330452
|
let finishStreakCountedThisBatch = false;
|
|
329476
330453
|
const fullVarMapForGuard = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
329477
|
-
const { secrets:
|
|
329478
|
-
const
|
|
330454
|
+
const { secrets: partitionedSecretsForGuard } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMapForGuard);
|
|
330455
|
+
const secretsOnlyForGuard = inboxState ? (0, google_signin_js_1.mergeGoogleSignInSecrets)(partitionedSecretsForGuard) : partitionedSecretsForGuard;
|
|
330456
|
+
const allowedCredentialValues = [...new Set([
|
|
330457
|
+
...inboxState ? [inboxState.email, inboxState.baseEmail] : [],
|
|
330458
|
+
...inboxState ? (0, google_signin_js_1.googleSignInAllowedEmails)() : []
|
|
330459
|
+
].filter((value) => !!value))];
|
|
329479
330460
|
const secretsForLogging = Object.fromEntries(Object.entries(secretsOnlyForGuard).filter(([, value]) => !allowedCredentialValues.includes(value)));
|
|
329480
330461
|
for (const toolCall of assistantMessage.tool_calls) {
|
|
329481
330462
|
const toolName = toolCall.function.name;
|
|
@@ -330326,8 +331307,9 @@ Exploration complete: ${summary}`);
|
|
|
330326
331307
|
}
|
|
330327
331308
|
const fullVarMap = variableContext.allTestVariables ?? variableContext.publicTestVariables;
|
|
330328
331309
|
const { secrets: dispatchSecrets } = (0, credential_tools_js_1.partitionTestVariables)(fullVarMap);
|
|
331310
|
+
const fillSecrets = inboxState ? (0, google_signin_js_1.mergeGoogleSignInSecrets)(dispatchSecrets) : dispatchSecrets;
|
|
330329
331311
|
try {
|
|
330330
|
-
const result = toolName === "fill_credential" ? await (0, credential_tools_js_1.handleFillCredential)(toolArgs, { mcpClient, credentials:
|
|
331312
|
+
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 });
|
|
330331
331313
|
if (result.didMutate) {
|
|
330332
331314
|
lastSnapshotText = null;
|
|
330333
331315
|
const codeLines = (0, exploration_parser_js_1.extractCodeLines)(result.content);
|
|
@@ -332367,7 +333349,7 @@ ${keyLines || "- (none provided)"}
|
|
|
332367
333349
|
## EXACTLY what to do
|
|
332368
333350
|
1. Navigate to ${baseUrl} and take a \`browser_snapshot\`.
|
|
332369
333351
|
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.
|
|
332370
|
-
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.
|
|
333352
|
+
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).
|
|
332371
333353
|
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.
|
|
332372
333354
|
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\`.
|
|
332373
333355
|
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.
|
|
@@ -332921,6 +333903,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
332921
333903
|
var grok_cli_js_1 = require_grok_cli();
|
|
332922
333904
|
var playwright_mcp_config_js_1 = require_playwright_mcp_config();
|
|
332923
333905
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
333906
|
+
var google_signin_js_1 = require_google_signin();
|
|
332924
333907
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
332925
333908
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
332926
333909
|
var agentic_survey_prompts_js_1 = require_agentic_survey_prompts();
|
|
@@ -333445,7 +334428,7 @@ var require_grok_survey_runner = __commonJS({
|
|
|
333445
334428
|
};
|
|
333446
334429
|
const surveyAgentsMd = inboxReg ? `${(0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)()}
|
|
333447
334430
|
|
|
333448
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey)}` : (0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)();
|
|
334431
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : (0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)();
|
|
333449
334432
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "AGENTS.md"), surveyAgentsMd, "utf8");
|
|
333450
334433
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_survey_prompts_js_1.buildSurveyBriefMd)(briefInput), "utf8");
|
|
333451
334434
|
log2("Wrote AGENTS.md + BRIEF.md");
|
|
@@ -334852,6 +335835,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
334852
335835
|
var playwright_native_js_1 = require_playwright_native();
|
|
334853
335836
|
var playwright_mcp_config_js_1 = require_playwright_mcp_config();
|
|
334854
335837
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
335838
|
+
var google_signin_js_1 = require_google_signin();
|
|
334855
335839
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
334856
335840
|
var grok_survey_runner_js_1 = require_grok_survey_runner();
|
|
334857
335841
|
var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
|
|
@@ -335145,7 +336129,7 @@ var require_grok_discovery_runner = __commonJS({
|
|
|
335145
336129
|
};
|
|
335146
336130
|
const discoveryAgentsMd = inboxReg ? `${(0, agentic_discovery_prompts_js_1.buildDiscoveryAgentsMd)()}
|
|
335147
336131
|
|
|
335148
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey)}` : (0, agentic_discovery_prompts_js_1.buildDiscoveryAgentsMd)();
|
|
336132
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : (0, agentic_discovery_prompts_js_1.buildDiscoveryAgentsMd)();
|
|
335149
336133
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "AGENTS.md"), discoveryAgentsMd, "utf8");
|
|
335150
336134
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_discovery_prompts_js_1.buildDiscoveryBriefMd)(briefInput), "utf8");
|
|
335151
336135
|
await (0, promises_1.mkdir)(testsDirPath, { recursive: true });
|
|
@@ -336098,7 +337082,7 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
|
|
|
336098
337082
|
|
|
336099
337083
|
---
|
|
336100
337084
|
|
|
336101
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxRegV2.email, inboxRegV2.passwordKey)}` : "");
|
|
337085
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxRegV2.email, inboxRegV2.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : "");
|
|
336102
337086
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "AGENTS.md"), agentsMd, "utf8");
|
|
336103
337087
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_discovery_prompts_js_1.buildDiscoveryBriefMd)(briefInput), "utf8");
|
|
336104
337088
|
await (0, promises_1.mkdir)(testsDirPath, { recursive: true });
|
|
@@ -337070,6 +338054,7 @@ var require_claude_survey_runner = __commonJS({
|
|
|
337070
338054
|
var claude_stream_parser_js_1 = require_claude_stream_parser();
|
|
337071
338055
|
var playwright_mcp_config_js_1 = require_playwright_mcp_config();
|
|
337072
338056
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
338057
|
+
var google_signin_js_1 = require_google_signin();
|
|
337073
338058
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
337074
338059
|
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
337075
338060
|
var grok_survey_runner_js_1 = require_grok_survey_runner();
|
|
@@ -337170,7 +338155,7 @@ ${preview}`);
|
|
|
337170
338155
|
};
|
|
337171
338156
|
const surveyAgentsMd = inboxReg ? `${(0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)({ canvasMode })}
|
|
337172
338157
|
|
|
337173
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey)}` : (0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)({ canvasMode });
|
|
338158
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : (0, agentic_survey_prompts_js_1.buildSurveyAgentsMd)({ canvasMode });
|
|
337174
338159
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "AGENTS.md"), surveyAgentsMd, "utf8");
|
|
337175
338160
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_survey_prompts_js_1.buildSurveyBriefMd)(briefInput), "utf8");
|
|
337176
338161
|
log2("Wrote AGENTS.md + BRIEF.md");
|
|
@@ -337880,6 +338865,7 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
337880
338865
|
var claude_cli_js_1 = require_claude_cli();
|
|
337881
338866
|
var playwright_mcp_config_js_1 = require_playwright_mcp_config();
|
|
337882
338867
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
338868
|
+
var google_signin_js_1 = require_google_signin();
|
|
337883
338869
|
var coverage_examples_content_js_1 = require_coverage_examples_content();
|
|
337884
338870
|
var scrub_credentials_js_1 = require_scrub_credentials();
|
|
337885
338871
|
var agentic_cli_constants_js_1 = require_agentic_cli_constants();
|
|
@@ -338048,7 +339034,7 @@ var require_claude_discovery_runner = __commonJS({
|
|
|
338048
339034
|
};
|
|
338049
339035
|
const explorerContractMd = inboxReg ? `${(0, agentic_discovery_prompts_js_1.buildExplorerContractMd)(contractInput)}
|
|
338050
339036
|
|
|
338051
|
-
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey)}` : (0, agentic_discovery_prompts_js_1.buildExplorerContractMd)(contractInput);
|
|
339037
|
+
${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwordKey, (0, google_signin_js_1.googleSignInGuidanceOpts)())}` : (0, agentic_discovery_prompts_js_1.buildExplorerContractMd)(contractInput);
|
|
338052
339038
|
await (0, promises_1.writeFile)(claudeMdPath, explorerContractMd, "utf8");
|
|
338053
339039
|
await (0, promises_1.writeFile)((0, node_path_1.join)(runDir, "BRIEF.md"), (0, agentic_discovery_prompts_js_1.buildDiscoveryBriefMd)(briefInput), "utf8");
|
|
338054
339040
|
await (0, promises_1.mkdir)(testsDirPath, { recursive: true });
|
|
@@ -338920,6 +339906,10 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
|
|
|
338920
339906
|
appId,
|
|
338921
339907
|
platform: platform3,
|
|
338922
339908
|
appBrief: ctx.appBrief,
|
|
339909
|
+
// Optional project docs / knowledge facts — empty is valid; planner
|
|
339910
|
+
// relies on live explorer evidence when absent (bug capture does not
|
|
339911
|
+
// require documents).
|
|
339912
|
+
knowledgeContext: ctx.knowledgeContext,
|
|
338923
339913
|
featureName: ctx.featureName,
|
|
338924
339914
|
existingTestNames: ctx.existingTestNames,
|
|
338925
339915
|
existingFeatureNames: ctx.existingFeatureNames,
|
|
@@ -349080,7 +350070,7 @@ var require_update = __commonJS({
|
|
|
349080
350070
|
var require_opts_arg = __commonJS({
|
|
349081
350071
|
"../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports, module) {
|
|
349082
350072
|
"use strict";
|
|
349083
|
-
var { promisify:
|
|
350073
|
+
var { promisify: promisify5 } = __require("util");
|
|
349084
350074
|
var fs = __require("fs");
|
|
349085
350075
|
var optsArg = (opts) => {
|
|
349086
350076
|
if (!opts)
|
|
@@ -349094,9 +350084,9 @@ var require_opts_arg = __commonJS({
|
|
|
349094
350084
|
else
|
|
349095
350085
|
throw new TypeError("invalid options argument");
|
|
349096
350086
|
opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
|
|
349097
|
-
opts.mkdirAsync =
|
|
350087
|
+
opts.mkdirAsync = promisify5(opts.mkdir);
|
|
349098
350088
|
opts.stat = opts.stat || opts.fs.stat || fs.stat;
|
|
349099
|
-
opts.statAsync =
|
|
350089
|
+
opts.statAsync = promisify5(opts.stat);
|
|
349100
350090
|
opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
|
|
349101
350091
|
opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
|
|
349102
350092
|
return opts;
|
|
@@ -353945,7 +354935,7 @@ var require_process_inventory = __commonJS({
|
|
|
353945
354935
|
var node_child_process_1 = __require("child_process");
|
|
353946
354936
|
var node_os_1 = __importDefault(__require("os"));
|
|
353947
354937
|
var node_util_1 = __require("util");
|
|
353948
|
-
var
|
|
354938
|
+
var execFileAsync5 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
353949
354939
|
var DEFAULT_LIMIT = 60;
|
|
353950
354940
|
var DEFAULT_TIMEOUT_MS2 = 800;
|
|
353951
354941
|
var MAX_COMMAND_LENGTH = 320;
|
|
@@ -354007,7 +354997,7 @@ var require_process_inventory = __commonJS({
|
|
|
354007
354997
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
354008
354998
|
timer.unref?.();
|
|
354009
354999
|
try {
|
|
354010
|
-
const { stdout } = await
|
|
355000
|
+
const { stdout } = await execFileAsync5(command, args, {
|
|
354011
355001
|
maxBuffer: 1024 * 1024,
|
|
354012
355002
|
signal: controller.signal
|
|
354013
355003
|
});
|
|
@@ -355301,7 +356291,7 @@ var require_package4 = __commonJS({
|
|
|
355301
356291
|
"package.json"(exports, module) {
|
|
355302
356292
|
module.exports = {
|
|
355303
356293
|
name: "@validate.qa/runner",
|
|
355304
|
-
version: "1.0.
|
|
356294
|
+
version: "1.0.93",
|
|
355305
356295
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
355306
356296
|
bin: {
|
|
355307
356297
|
"validate-runner": "dist/cli.js"
|
|
@@ -356166,11 +357156,7 @@ var SYSTEM_UI_DISMISS_LABELS = [
|
|
|
356166
357156
|
"OK",
|
|
356167
357157
|
"Got it",
|
|
356168
357158
|
"GOT IT",
|
|
356169
|
-
"Continue",
|
|
356170
|
-
"Close",
|
|
356171
357159
|
"Dismiss",
|
|
356172
|
-
"I understand",
|
|
356173
|
-
"Accept",
|
|
356174
357160
|
// Android 15+ / 16 KB page-size compatibility dialog primary action.
|
|
356175
357161
|
"OK, got it"
|
|
356176
357162
|
];
|
|
@@ -360100,7 +361086,11 @@ async function executeAssertVisible(sessionId, step, platform3, isCrossPlatform
|
|
|
360100
361086
|
tolerateAmbiguity: true
|
|
360101
361087
|
});
|
|
360102
361088
|
const result2 = await wdRequest(`${sessionPath}/element/${elementId2}/displayed`);
|
|
360103
|
-
if (!result2?.value)
|
|
361089
|
+
if (!result2?.value) {
|
|
361090
|
+
throw new Error(
|
|
361091
|
+
`Assertion failed: expected "${css}" to be visible, but the WEBVIEW element resolved and is not displayed`
|
|
361092
|
+
);
|
|
361093
|
+
}
|
|
360104
361094
|
});
|
|
360105
361095
|
return;
|
|
360106
361096
|
}
|
|
@@ -360112,7 +361102,9 @@ async function executeAssertVisible(sessionId, step, platform3, isCrossPlatform
|
|
|
360112
361102
|
});
|
|
360113
361103
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/displayed`);
|
|
360114
361104
|
if (!result?.value) {
|
|
360115
|
-
throw new Error(
|
|
361105
|
+
throw new Error(
|
|
361106
|
+
`Assertion failed: expected "${step.target?.primary ?? "element"}" to be visible, but it resolved and is not displayed`
|
|
361107
|
+
);
|
|
360116
361108
|
}
|
|
360117
361109
|
}
|
|
360118
361110
|
async function executeAssertText(sessionId, step, platform3, isCrossPlatform = false) {
|
|
@@ -362641,6 +363633,26 @@ function getMirrorSessionId(deviceId) {
|
|
|
362641
363633
|
// src/services/mobile/mobile-bridge-driver.ts
|
|
362642
363634
|
var import_runner_core9 = __toESM(require_dist2());
|
|
362643
363635
|
init_dist();
|
|
363636
|
+
import { execFile as execFile4 } from "child_process";
|
|
363637
|
+
import { promisify as promisify4 } from "util";
|
|
363638
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
363639
|
+
async function clearAndroidAppDataViaAdb(deviceId, appId) {
|
|
363640
|
+
const serial = deviceId?.trim();
|
|
363641
|
+
const pkg2 = appId?.trim();
|
|
363642
|
+
if (!serial || !pkg2 || !serial.startsWith("emulator-")) return false;
|
|
363643
|
+
try {
|
|
363644
|
+
const { stdout, stderr } = await execFileAsync4(
|
|
363645
|
+
"adb",
|
|
363646
|
+
["-s", serial, "shell", "pm", "clear", pkg2],
|
|
363647
|
+
{ timeout: 2e4, maxBuffer: 64 * 1024 }
|
|
363648
|
+
);
|
|
363649
|
+
const out = `${stdout ?? ""}
|
|
363650
|
+
${stderr ?? ""}`.toLowerCase();
|
|
363651
|
+
return out.includes("success");
|
|
363652
|
+
} catch {
|
|
363653
|
+
return false;
|
|
363654
|
+
}
|
|
363655
|
+
}
|
|
362644
363656
|
var APPIUM_SESSION_ID_PATTERN2 = /^[a-f0-9-]{1,64}$/i;
|
|
362645
363657
|
function isRecoverableAppiumSessionFailure(err) {
|
|
362646
363658
|
if (isAppiumDriverDeadError(err)) return true;
|
|
@@ -363039,7 +364051,17 @@ var MobileBridgeDriver = class {
|
|
|
363039
364051
|
throw new Error(`iOS simulator app-data reset failed: ${wiped.detail}`);
|
|
363040
364052
|
}
|
|
363041
364053
|
} else {
|
|
363042
|
-
await this.
|
|
364054
|
+
const adbFirst = await clearAndroidAppDataViaAdb(this.deviceId, this.appId);
|
|
364055
|
+
if (!adbFirst) {
|
|
364056
|
+
try {
|
|
364057
|
+
await this.executeScript("mobile: clearApp", [{ appId: this.appId }]);
|
|
364058
|
+
} catch (clearErr) {
|
|
364059
|
+
const adbCleared = await clearAndroidAppDataViaAdb(this.deviceId, this.appId);
|
|
364060
|
+
if (!adbCleared) {
|
|
364061
|
+
throw clearErr;
|
|
364062
|
+
}
|
|
364063
|
+
}
|
|
364064
|
+
}
|
|
363043
364065
|
}
|
|
363044
364066
|
await this.executeScript("mobile: activateApp", [this.appLifecycleArgs()]);
|
|
363045
364067
|
try {
|
|
@@ -364271,10 +365293,28 @@ function truncateAuditRequestBody(body) {
|
|
|
364271
365293
|
function isTerminalAuditStatus(status) {
|
|
364272
365294
|
return status === 400 || status === 404 || status === 409 || status === 413;
|
|
364273
365295
|
}
|
|
365296
|
+
var AuditDrainError = class extends Error {
|
|
365297
|
+
constructor(message, dropped) {
|
|
365298
|
+
super(message);
|
|
365299
|
+
this.dropped = dropped;
|
|
365300
|
+
this.name = "AuditDrainError";
|
|
365301
|
+
}
|
|
365302
|
+
};
|
|
365303
|
+
function auditDroppedFromDrainFailure(reason) {
|
|
365304
|
+
if (reason instanceof AuditDrainError) return reason.dropped;
|
|
365305
|
+
if (reason && typeof reason === "object" && "dropped" in reason) {
|
|
365306
|
+
const dropped = reason.dropped;
|
|
365307
|
+
if (typeof dropped === "number" && Number.isFinite(dropped)) {
|
|
365308
|
+
return Math.max(0, Math.floor(dropped));
|
|
365309
|
+
}
|
|
365310
|
+
}
|
|
365311
|
+
return 0;
|
|
365312
|
+
}
|
|
364274
365313
|
function createAuditFlusher(url, headers) {
|
|
364275
365314
|
const entries = [];
|
|
364276
365315
|
let byteEstimate = 0;
|
|
364277
365316
|
let flushInFlight = null;
|
|
365317
|
+
let droppedTotal = 0;
|
|
364278
365318
|
const doFlush = () => {
|
|
364279
365319
|
if (entries.length === 0) return Promise.resolve();
|
|
364280
365320
|
if (flushInFlight) return flushInFlight;
|
|
@@ -364297,9 +365337,16 @@ function createAuditFlusher(url, headers) {
|
|
|
364297
365337
|
body: JSON.stringify({ entries: toSend.map((e) => e.entry) }),
|
|
364298
365338
|
signal: AbortSignal.timeout(3e4)
|
|
364299
365339
|
});
|
|
364300
|
-
if (res.ok)
|
|
365340
|
+
if (res.ok) {
|
|
365341
|
+
const ack = await res.json().catch(() => null);
|
|
365342
|
+
if (typeof ack?.dropped === "number" && Number.isFinite(ack.dropped)) {
|
|
365343
|
+
droppedTotal += Math.max(0, Math.floor(ack.dropped));
|
|
365344
|
+
}
|
|
365345
|
+
continue;
|
|
365346
|
+
}
|
|
364301
365347
|
if (isTerminalAuditStatus(res.status)) {
|
|
364302
365348
|
console.warn(`[audit-flush] HTTP ${res.status} (terminal) - dropping ${toSend.length} audit entries`);
|
|
365349
|
+
droppedTotal += toSend.length;
|
|
364303
365350
|
continue;
|
|
364304
365351
|
}
|
|
364305
365352
|
entries.unshift(...toSend);
|
|
@@ -364339,6 +365386,7 @@ function createAuditFlusher(url, headers) {
|
|
|
364339
365386
|
droppedCount++;
|
|
364340
365387
|
}
|
|
364341
365388
|
console.warn(`[audit-flush] queue over ${AUDIT_QUEUE_MAX_BYTES} bytes - dropped ${droppedCount} oldest audit entries`);
|
|
365389
|
+
droppedTotal += droppedCount;
|
|
364342
365390
|
}
|
|
364343
365391
|
if (entries.length >= AUDIT_FLUSH_COUNT || byteEstimate >= AUDIT_FLUSH_BYTES) {
|
|
364344
365392
|
void doFlush();
|
|
@@ -364347,16 +365395,20 @@ function createAuditFlusher(url, headers) {
|
|
|
364347
365395
|
const drain = async () => {
|
|
364348
365396
|
for (let attempt = 1; attempt <= AUDIT_DRAIN_ATTEMPTS; attempt++) {
|
|
364349
365397
|
if (flushInFlight) await flushInFlight;
|
|
364350
|
-
if (entries.length === 0) return;
|
|
365398
|
+
if (entries.length === 0) return { dropped: droppedTotal };
|
|
364351
365399
|
await doFlush();
|
|
364352
|
-
if (entries.length === 0) return;
|
|
365400
|
+
if (entries.length === 0) return { dropped: droppedTotal };
|
|
364353
365401
|
if (attempt < AUDIT_DRAIN_ATTEMPTS) {
|
|
364354
365402
|
await new Promise((resolve2) => setTimeout(resolve2, AUDIT_DRAIN_DELAY_MS * attempt));
|
|
364355
365403
|
}
|
|
364356
365404
|
}
|
|
364357
365405
|
if (entries.length > 0) {
|
|
364358
|
-
throw new
|
|
365406
|
+
throw new AuditDrainError(
|
|
365407
|
+
`audit drain failed after ${AUDIT_DRAIN_ATTEMPTS} attempts - ${entries.length} entries could not be flushed to ${url}`,
|
|
365408
|
+
droppedTotal + entries.length
|
|
365409
|
+
);
|
|
364359
365410
|
}
|
|
365411
|
+
return { dropped: droppedTotal };
|
|
364360
365412
|
};
|
|
364361
365413
|
return { push, drain };
|
|
364362
365414
|
}
|
|
@@ -365605,7 +366657,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
365605
366657
|
log.warn("[stream] discovery-plan failed after 3 attempts - plan checkpoint skipped");
|
|
365606
366658
|
};
|
|
365607
366659
|
let discoveryResult;
|
|
365608
|
-
const buildFinalBody = (r, overrideError, auditWarning) => {
|
|
366660
|
+
const buildFinalBody = (r, overrideError, auditWarning, auditDroppedCount = 0) => {
|
|
365609
366661
|
const isNativeDiscovery = ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE";
|
|
365610
366662
|
const { exploreFinalMessages: _exploreFinalMessages, ...rest } = r;
|
|
365611
366663
|
void _exploreFinalMessages;
|
|
@@ -365618,6 +366670,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
365618
366670
|
summary: rest.summary,
|
|
365619
366671
|
mode: rest.mode,
|
|
365620
366672
|
error: overrideError ?? rest.error,
|
|
366673
|
+
auditDroppedCount,
|
|
365621
366674
|
// Resumable-pause checkpoint: native deep generation tripped the AI-overload breaker.
|
|
365622
366675
|
// Without forwarding these, the server's isDiscoveryPaused is false → the run finalizes
|
|
365623
366676
|
// COMPLETED/FAILED instead of PAUSED → the Resume affordance never appears for
|
|
@@ -366003,12 +367056,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366003
367056
|
discoveryAuditWarning = `[audit] ${drainErr instanceof Error ? drainErr.message : String(drainErr)}`;
|
|
366004
367057
|
console.warn(`[runner] ${discoveryAuditWarning}`);
|
|
366005
367058
|
}
|
|
367059
|
+
const auditDroppedCount = drainOutcome.status === "fulfilled" ? drainOutcome.value.dropped : auditDroppedFromDrainFailure(drainOutcome.reason);
|
|
366006
367060
|
const harStatus = harOutcome.status === "fulfilled" ? harOutcome.value : null;
|
|
366007
367061
|
harPosted = decideHarPosted(harStatus !== null ? { status: harStatus } : null);
|
|
366008
367062
|
if (!harPosted) {
|
|
366009
367063
|
log.warn("[runner] discovery-har POST failed - final /discovery-result will carry HAR fallback");
|
|
366010
367064
|
}
|
|
366011
|
-
const finalBody = buildFinalBody(discoveryResult, void 0, discoveryAuditWarning || void 0);
|
|
367065
|
+
const finalBody = buildFinalBody(discoveryResult, void 0, discoveryAuditWarning || void 0, auditDroppedCount);
|
|
366012
367066
|
let postOk = false;
|
|
366013
367067
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
366014
367068
|
const timeoutMs = attempt === 0 ? 12e4 : 3e4;
|
|
@@ -366042,14 +367096,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366042
367096
|
const msg = err instanceof Error ? err.message : String(err);
|
|
366043
367097
|
log.fail(`Discovery FAILED: ${msg}`);
|
|
366044
367098
|
let drainMsg = "";
|
|
367099
|
+
let auditDroppedCount = 0;
|
|
366045
367100
|
try {
|
|
366046
|
-
await drainAudit();
|
|
367101
|
+
auditDroppedCount = (await drainAudit()).dropped;
|
|
366047
367102
|
} catch (drainErr) {
|
|
367103
|
+
auditDroppedCount = auditDroppedFromDrainFailure(drainErr);
|
|
366048
367104
|
drainMsg = `
|
|
366049
367105
|
[audit] ${drainErr instanceof Error ? drainErr.message : String(drainErr)}`;
|
|
366050
367106
|
console.warn(`[runner]${drainMsg}`);
|
|
366051
367107
|
}
|
|
366052
|
-
const errorBody = discoveryResult ? buildFinalBody(discoveryResult, msg, drainMsg || void 0) : prepareDiscoveryResultForPost({
|
|
367108
|
+
const errorBody = discoveryResult ? buildFinalBody(discoveryResult, msg, drainMsg || void 0, auditDroppedCount) : prepareDiscoveryResultForPost({
|
|
366053
367109
|
...explorePosted ? {} : { collectedPages: [], collectedTransitions: [] },
|
|
366054
367110
|
tests: [],
|
|
366055
367111
|
logs: `Fatal error: ${msg}${drainMsg}`,
|
|
@@ -366057,6 +367113,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366057
367113
|
summary: `Fatal error: ${msg}`,
|
|
366058
367114
|
exploreStats: { pagesDiscovered: 0, pagesVisited: 0, elementsFound: 0, transitionsFound: 0 },
|
|
366059
367115
|
duration: (Date.now() - startTime) / 1e3,
|
|
367116
|
+
auditDroppedCount,
|
|
366060
367117
|
mode: ctx.mode,
|
|
366061
367118
|
error: msg,
|
|
366062
367119
|
streamedTestCount: streamedTestKeys.size,
|
|
@@ -366422,6 +367479,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366422
367479
|
status: typeof c.status === "number" ? c.status : void 0
|
|
366423
367480
|
})) : void 0;
|
|
366424
367481
|
const uiPathSucceeded = Array.isArray(reproduce.stepResults) && reproduce.stepResults.filter((s) => s.status === "passed").length >= Math.max(1, reproduce.stepResults.length - 1);
|
|
367482
|
+
const failedStepOrder = Array.isArray(reproduce.stepResults) ? reproduce.stepResults.find((s) => s.status === "failed")?.stepOrder : void 0;
|
|
366425
367483
|
const heal = await (0, import_runner_core14.executeMobileTestWithHealing)(
|
|
366426
367484
|
mobileSteps,
|
|
366427
367485
|
healTarget,
|
|
@@ -366433,6 +367491,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
366433
367491
|
testName: run2.testName,
|
|
366434
367492
|
tags: tagList,
|
|
366435
367493
|
failureCount,
|
|
367494
|
+
failedStepOrder,
|
|
366436
367495
|
apiCalls: reproduceApiCalls,
|
|
366437
367496
|
uiPathSucceeded
|
|
366438
367497
|
},
|