@validate.qa/runner 1.0.88 → 1.0.90
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 +1575 -34
- package/dist/cli.mjs +1575 -34
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1793,7 +1793,11 @@ var init_constants = __esm({
|
|
|
1793
1793
|
"home",
|
|
1794
1794
|
"waitForElement",
|
|
1795
1795
|
"assertVisible",
|
|
1796
|
-
"assertText"
|
|
1796
|
+
"assertText",
|
|
1797
|
+
/** Capture list labels / badge counts into run-local state bag (Phase 1 oracles). */
|
|
1798
|
+
"captureState",
|
|
1799
|
+
/** Assert a prior captureState delta (removed id, count change, etc.). */
|
|
1800
|
+
"assertStateDelta"
|
|
1797
1801
|
];
|
|
1798
1802
|
BLOCKED_ENV_VARS = /* @__PURE__ */ new Set([
|
|
1799
1803
|
// Execution / code-injection vectors
|
|
@@ -2228,7 +2232,10 @@ function readMobileMetadata(value) {
|
|
|
2228
2232
|
webCss: typeof value.webCss === "string" && value.webCss.trim() ? value.webCss.trim() : void 0,
|
|
2229
2233
|
webviewContext: typeof value.webviewContext === "string" && /^WEBVIEW/i.test(value.webviewContext.trim()) ? value.webviewContext.trim() : void 0,
|
|
2230
2234
|
matchIndex: hasValidMatchPair ? rawMatchIndex : void 0,
|
|
2231
|
-
expectedMatchCount: hasValidMatchPair ? rawExpectedMatchCount : void 0
|
|
2235
|
+
expectedMatchCount: hasValidMatchPair ? rawExpectedMatchCount : void 0,
|
|
2236
|
+
stateKey: typeof value.stateKey === "string" && value.stateKey.trim() ? value.stateKey.trim() : void 0,
|
|
2237
|
+
stateKind: typeof value.stateKind === "string" && value.stateKind.trim() ? value.stateKind.trim() : void 0,
|
|
2238
|
+
expect: typeof value.expect === "string" && value.expect.trim() ? value.expect.trim() : void 0
|
|
2232
2239
|
};
|
|
2233
2240
|
}
|
|
2234
2241
|
function nonEmpty(value) {
|
|
@@ -2311,6 +2318,10 @@ function descriptionForStep(action, stepAction, metadata) {
|
|
|
2311
2318
|
return `Assert ${label} is visible`;
|
|
2312
2319
|
case "assertText":
|
|
2313
2320
|
return `Assert ${label} shows "${action.value ?? action.innerText ?? ""}"`;
|
|
2321
|
+
case "captureState":
|
|
2322
|
+
return `Capture UI state "${metadata.stateKey ?? label}" (${metadata.stateKind ?? "list_ids"})`;
|
|
2323
|
+
case "assertStateDelta":
|
|
2324
|
+
return `Assert state delta "${metadata.stateKey ?? label}" expect=${metadata.expect ?? action.value ?? "changed"}`;
|
|
2314
2325
|
default:
|
|
2315
2326
|
return `Execute ${stepAction}`;
|
|
2316
2327
|
}
|
|
@@ -2356,6 +2367,8 @@ function buildMobileStep(action, order) {
|
|
|
2356
2367
|
step.assertion = `${labelForAction(action, metadata)} should be visible`;
|
|
2357
2368
|
} else if (stepAction === "assertText") {
|
|
2358
2369
|
step.assertion = action.value ?? action.innerText ?? void 0;
|
|
2370
|
+
} else if (stepAction === "assertStateDelta") {
|
|
2371
|
+
step.assertion = metadata.expect ?? action.value ?? "changed";
|
|
2359
2372
|
}
|
|
2360
2373
|
if ((stepAction === "swipe" || stepAction === "scroll") && (metadata.direction || metadata.distance)) {
|
|
2361
2374
|
const stepMetadata = ensureMetadata(step);
|
|
@@ -2364,6 +2377,24 @@ function buildMobileStep(action, order) {
|
|
|
2364
2377
|
if (metadata.distance)
|
|
2365
2378
|
stepMetadata.distance = metadata.distance;
|
|
2366
2379
|
}
|
|
2380
|
+
if (stepAction === "captureState" || stepAction === "assertStateDelta") {
|
|
2381
|
+
const stepMetadata = ensureMetadata(step);
|
|
2382
|
+
const stateKey = nonEmpty(metadata.stateKey) ?? nonEmpty(action.selector) ?? nonEmpty(metadata.targetLabel) ?? `state-${order}`;
|
|
2383
|
+
stepMetadata.stateKey = stateKey;
|
|
2384
|
+
if (stepAction === "captureState") {
|
|
2385
|
+
stepMetadata.stateKind = nonEmpty(metadata.stateKind) ?? "list_ids";
|
|
2386
|
+
}
|
|
2387
|
+
if (stepAction === "assertStateDelta") {
|
|
2388
|
+
stepMetadata.expect = nonEmpty(metadata.expect) ?? nonEmpty(action.value) ?? "changed";
|
|
2389
|
+
}
|
|
2390
|
+
if (!step.target) {
|
|
2391
|
+
step.target = {
|
|
2392
|
+
primary: stateKey,
|
|
2393
|
+
fallbacks: [],
|
|
2394
|
+
screenName: metadata.screenName || "UnknownScreen"
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2367
2398
|
return step;
|
|
2368
2399
|
}
|
|
2369
2400
|
function buildLaunchStep(target, medium, appResetOnLaunch = false) {
|
|
@@ -2575,6 +2606,9 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
2575
2606
|
used.find = true;
|
|
2576
2607
|
}
|
|
2577
2608
|
break;
|
|
2609
|
+
case "captureState":
|
|
2610
|
+
case "assertStateDelta":
|
|
2611
|
+
break;
|
|
2578
2612
|
default:
|
|
2579
2613
|
break;
|
|
2580
2614
|
}
|
|
@@ -2693,6 +2727,8 @@ function buildAppiumCode(testName, target, steps) {
|
|
|
2693
2727
|
const expectedLiteral = toJsStringLiteral(step.assertion ?? "");
|
|
2694
2728
|
const replayExpected = step.assertion?.includes("{{") ? `resolveValue(${expectedLiteral})` : expectedLiteral;
|
|
2695
2729
|
pushAction(`await expectText(driver, ${sel}, ${replayExpected});`);
|
|
2730
|
+
} else if (step.action === "captureState" || step.action === "assertStateDelta") {
|
|
2731
|
+
lines.push(` // ${step.action}: ${step.description.replace(/\n/g, " ").slice(0, 120)}`);
|
|
2696
2732
|
} else if (step.action === "back") {
|
|
2697
2733
|
lines.push(` await driver.back();`);
|
|
2698
2734
|
} else if (step.action === "home") {
|
|
@@ -3035,7 +3071,9 @@ var init_mobile_test_generation = __esm({
|
|
|
3035
3071
|
HOME: "home",
|
|
3036
3072
|
WAIT_FOR_ELEMENT: "waitForElement",
|
|
3037
3073
|
ASSERT_VISIBLE: "assertVisible",
|
|
3038
|
-
ASSERT_TEXT: "assertText"
|
|
3074
|
+
ASSERT_TEXT: "assertText",
|
|
3075
|
+
CAPTURE_STATE: "captureState",
|
|
3076
|
+
ASSERT_STATE_DELTA: "assertStateDelta"
|
|
3039
3077
|
};
|
|
3040
3078
|
MOBILE_VALUE_PLACEHOLDER_RE = /\{\{([A-Za-z0-9_]+)\}\}/g;
|
|
3041
3079
|
}
|
|
@@ -3266,7 +3304,9 @@ var init_mobile_step_healer = __esm({
|
|
|
3266
3304
|
"home",
|
|
3267
3305
|
"waitForElement",
|
|
3268
3306
|
"assertVisible",
|
|
3269
|
-
"assertText"
|
|
3307
|
+
"assertText",
|
|
3308
|
+
"captureState",
|
|
3309
|
+
"assertStateDelta"
|
|
3270
3310
|
]);
|
|
3271
3311
|
FOREIGN_PACKAGE_RE = /\b[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*){2,}\b/gi;
|
|
3272
3312
|
ANDROID_RESOURCE_OWNER_RE = /([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+):id\//gi;
|
|
@@ -80305,6 +80345,306 @@ ${JSON.stringify(gatePlan)}`;
|
|
|
80305
80345
|
}
|
|
80306
80346
|
});
|
|
80307
80347
|
|
|
80348
|
+
// ../runner-core/dist/services/mobile/scenario-taxonomy.js
|
|
80349
|
+
var require_scenario_taxonomy = __commonJS({
|
|
80350
|
+
"../runner-core/dist/services/mobile/scenario-taxonomy.js"(exports2) {
|
|
80351
|
+
"use strict";
|
|
80352
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
80353
|
+
exports2.ALL_SCENARIO_KINDS = void 0;
|
|
80354
|
+
exports2.extractUiSignals = extractUiSignals;
|
|
80355
|
+
exports2.requiredScenarioKinds = requiredScenarioKinds;
|
|
80356
|
+
exports2.coverageGaps = coverageGaps;
|
|
80357
|
+
exports2.inferScenarioKind = inferScenarioKind;
|
|
80358
|
+
exports2.inferScenarioKinds = inferScenarioKinds;
|
|
80359
|
+
exports2.isStrongOracleKind = isStrongOracleKind;
|
|
80360
|
+
exports2.formatRequiredScenarioKindsPrompt = formatRequiredScenarioKindsPrompt;
|
|
80361
|
+
exports2.formatCoverageGapLog = formatCoverageGapLog;
|
|
80362
|
+
exports2.ALL_SCENARIO_KINDS = [
|
|
80363
|
+
"smoke_nav",
|
|
80364
|
+
"happy_submit",
|
|
80365
|
+
"negative_validation",
|
|
80366
|
+
"crud_identity",
|
|
80367
|
+
"count_delta",
|
|
80368
|
+
"persistence_roundtrip",
|
|
80369
|
+
"auth_session",
|
|
80370
|
+
"navigation_integrity",
|
|
80371
|
+
"permission_system",
|
|
80372
|
+
"double_submit"
|
|
80373
|
+
];
|
|
80374
|
+
var DELETE_RE = /\b(delete|remove|archive|trash)\b/i;
|
|
80375
|
+
var EMAIL_RE = /\b(e-?mail|email address)\b/i;
|
|
80376
|
+
var PASSWORD_RE = /\b(password|passcode|pin)\b/i;
|
|
80377
|
+
var SIGN_OUT_RE = /\b(sign\s*out|log\s*out|logout)\b/i;
|
|
80378
|
+
var TOGGLE_RE = /\b(toggle|switch|enable|disable|on\/off)\b/i;
|
|
80379
|
+
var SUBMIT_RE = /\b(submit|save|send|create|confirm|apply|done|update|post)\b/i;
|
|
80380
|
+
var BADGE_OR_COUNT_RE = /\b(\d+)\s*(item|items|open|unread|invoice|message|result|pending|new)\b/i;
|
|
80381
|
+
var BADGE_LABEL_RE = /\b(badge|count|open invoices|unread|notifications?)\b/i;
|
|
80382
|
+
var PERMISSION_RE = /\b(camera|microphone|location|photos?|permission|allow access|not now)\b/i;
|
|
80383
|
+
var SETTINGS_RE = /\b(settings|preferences|profile|account)\b/i;
|
|
80384
|
+
var TAB_RE = /\b(tab|home|catalog|forms|edges|inbox|feed|dashboard)\b/i;
|
|
80385
|
+
function elementText(el) {
|
|
80386
|
+
return [
|
|
80387
|
+
el.label ?? "",
|
|
80388
|
+
el.text ?? "",
|
|
80389
|
+
el.accessibilityId ?? "",
|
|
80390
|
+
el.resourceId ?? "",
|
|
80391
|
+
el.role ?? ""
|
|
80392
|
+
].join(" ");
|
|
80393
|
+
}
|
|
80394
|
+
function isEditable(el) {
|
|
80395
|
+
const role = el.role ?? "";
|
|
80396
|
+
return /EditText|TextField|SecureTextField|SearchField|AutoComplete|textbox|searchbox/i.test(role) || el.role === "textbox" || el.role === "searchbox";
|
|
80397
|
+
}
|
|
80398
|
+
function isToggle(el) {
|
|
80399
|
+
const role = el.role ?? "";
|
|
80400
|
+
return /Switch|CheckBox|Toggle|RadioButton/i.test(role) || TOGGLE_RE.test(elementText(el));
|
|
80401
|
+
}
|
|
80402
|
+
function isRowAction(el) {
|
|
80403
|
+
const t = elementText(el);
|
|
80404
|
+
return DELETE_RE.test(t) && el.actionable !== false;
|
|
80405
|
+
}
|
|
80406
|
+
function extractUiSignals(graph, appId) {
|
|
80407
|
+
const screens = [...graph.screens.values()].filter((s) => {
|
|
80408
|
+
if (!appId)
|
|
80409
|
+
return true;
|
|
80410
|
+
return s.screenId === appId || s.screenId.startsWith(`${appId}#`) || s.screenId.startsWith(`${appId}.`);
|
|
80411
|
+
});
|
|
80412
|
+
let hasListWithRowActions = false;
|
|
80413
|
+
let hasFormFields = false;
|
|
80414
|
+
let hasEmailField = false;
|
|
80415
|
+
let hasNumericBadge = false;
|
|
80416
|
+
let hasSettingsToggles = false;
|
|
80417
|
+
let hasSignOut = false;
|
|
80418
|
+
let hasDestructiveConfirm = false;
|
|
80419
|
+
let hasPrimarySubmit = false;
|
|
80420
|
+
let hasTabOrDrawerNav = false;
|
|
80421
|
+
let hasPermissionCopy = false;
|
|
80422
|
+
for (const state2 of screens) {
|
|
80423
|
+
const labels = state2.elements.map(elementText);
|
|
80424
|
+
const blob = [state2.screenName ?? "", state2.screenType ?? "", ...state2.observations ?? [], ...labels].join(" ");
|
|
80425
|
+
if (PASSWORD_RE.test(blob) || state2.elements.some(isEditable))
|
|
80426
|
+
hasFormFields = true;
|
|
80427
|
+
if (labels.some((t) => EMAIL_RE.test(t)) || state2.elements.some((el) => isEditable(el) && EMAIL_RE.test(elementText(el)))) {
|
|
80428
|
+
hasEmailField = true;
|
|
80429
|
+
hasFormFields = true;
|
|
80430
|
+
}
|
|
80431
|
+
if (state2.elements.some(isRowAction))
|
|
80432
|
+
hasListWithRowActions = true;
|
|
80433
|
+
if (!hasListWithRowActions && state2.elements.filter((el) => el.actionable).length >= 6 && DELETE_RE.test(blob)) {
|
|
80434
|
+
hasListWithRowActions = true;
|
|
80435
|
+
}
|
|
80436
|
+
if (BADGE_OR_COUNT_RE.test(blob) || BADGE_LABEL_RE.test(blob))
|
|
80437
|
+
hasNumericBadge = true;
|
|
80438
|
+
if (state2.elements.some(isToggle) || SETTINGS_RE.test(blob) && TOGGLE_RE.test(blob)) {
|
|
80439
|
+
hasSettingsToggles = true;
|
|
80440
|
+
}
|
|
80441
|
+
if (SIGN_OUT_RE.test(blob))
|
|
80442
|
+
hasSignOut = true;
|
|
80443
|
+
if (DELETE_RE.test(blob) && /\b(confirm|are you sure|permanently)\b/i.test(blob)) {
|
|
80444
|
+
hasDestructiveConfirm = true;
|
|
80445
|
+
}
|
|
80446
|
+
if (labels.some((t) => SUBMIT_RE.test(t)))
|
|
80447
|
+
hasPrimarySubmit = true;
|
|
80448
|
+
if (TAB_RE.test(blob) || state2.screenType === "TAB" || /\btab\b/i.test(state2.screenName ?? "")) {
|
|
80449
|
+
hasTabOrDrawerNav = true;
|
|
80450
|
+
}
|
|
80451
|
+
if (PERMISSION_RE.test(blob) || state2.screenType === "PERMISSION")
|
|
80452
|
+
hasPermissionCopy = true;
|
|
80453
|
+
}
|
|
80454
|
+
return {
|
|
80455
|
+
hasListWithRowActions,
|
|
80456
|
+
hasFormFields,
|
|
80457
|
+
hasEmailField,
|
|
80458
|
+
hasNumericBadge,
|
|
80459
|
+
hasSettingsToggles,
|
|
80460
|
+
hasSignOut,
|
|
80461
|
+
hasDestructiveConfirm,
|
|
80462
|
+
hasPrimarySubmit,
|
|
80463
|
+
hasTabOrDrawerNav,
|
|
80464
|
+
hasPermissionCopy
|
|
80465
|
+
};
|
|
80466
|
+
}
|
|
80467
|
+
function requiredScenarioKinds(signals) {
|
|
80468
|
+
const kinds = /* @__PURE__ */ new Set(["smoke_nav"]);
|
|
80469
|
+
if (signals.hasFormFields || signals.hasPrimarySubmit) {
|
|
80470
|
+
kinds.add("happy_submit");
|
|
80471
|
+
}
|
|
80472
|
+
if (signals.hasFormFields || signals.hasEmailField) {
|
|
80473
|
+
kinds.add("negative_validation");
|
|
80474
|
+
}
|
|
80475
|
+
if (signals.hasListWithRowActions) {
|
|
80476
|
+
kinds.add("crud_identity");
|
|
80477
|
+
}
|
|
80478
|
+
if (signals.hasNumericBadge) {
|
|
80479
|
+
kinds.add("count_delta");
|
|
80480
|
+
}
|
|
80481
|
+
if (signals.hasSettingsToggles) {
|
|
80482
|
+
kinds.add("persistence_roundtrip");
|
|
80483
|
+
}
|
|
80484
|
+
if (signals.hasSignOut) {
|
|
80485
|
+
kinds.add("auth_session");
|
|
80486
|
+
}
|
|
80487
|
+
if (signals.hasTabOrDrawerNav) {
|
|
80488
|
+
kinds.add("navigation_integrity");
|
|
80489
|
+
}
|
|
80490
|
+
if (signals.hasPermissionCopy) {
|
|
80491
|
+
kinds.add("permission_system");
|
|
80492
|
+
}
|
|
80493
|
+
if (signals.hasPrimarySubmit && signals.hasFormFields) {
|
|
80494
|
+
kinds.add("double_submit");
|
|
80495
|
+
}
|
|
80496
|
+
return exports2.ALL_SCENARIO_KINDS.filter((k) => kinds.has(k));
|
|
80497
|
+
}
|
|
80498
|
+
function coverageGaps(signals, plannedKinds) {
|
|
80499
|
+
const planned = new Set(plannedKinds);
|
|
80500
|
+
return requiredScenarioKinds(signals).filter((k) => !planned.has(k));
|
|
80501
|
+
}
|
|
80502
|
+
function inferScenarioKind(scenario) {
|
|
80503
|
+
const text = `${scenario.name} ${scenario.goal} ${scenario.variant ?? ""} ${scenario.type}`.toLowerCase();
|
|
80504
|
+
if (/\b(invalid|empty|required|validation|too short|malformed|reject|error message)\b/.test(text) || scenario.variant === "validation" || scenario.type === "EDGE_CASE" && /\b(empty|invalid|required)\b/.test(text)) {
|
|
80505
|
+
return "negative_validation";
|
|
80506
|
+
}
|
|
80507
|
+
if (/\b(delete|remove|wrong row|entity|archive)\b/.test(text) && /\b(list|row|item|record)\b/.test(text)) {
|
|
80508
|
+
return "crud_identity";
|
|
80509
|
+
}
|
|
80510
|
+
if (/\b(badge|count|unread|open invoices|\+\d|increment|decrement)\b/.test(text)) {
|
|
80511
|
+
return "count_delta";
|
|
80512
|
+
}
|
|
80513
|
+
if (/\b(persist|revisit|return|leave|settings|toggle|favorite|remember)\b/.test(text)) {
|
|
80514
|
+
return "persistence_roundtrip";
|
|
80515
|
+
}
|
|
80516
|
+
if (/\b(sign\s*out|log\s*out|session|sign\s*in|remember me|auth)\b/.test(text) || scenario.type === "HAPPY_PATH" && /\b(login|sign in)\b/.test(text)) {
|
|
80517
|
+
return "auth_session";
|
|
80518
|
+
}
|
|
80519
|
+
if (/\b(tab|navigate|navigation|back|deep link|empty.?state)\b/.test(text)) {
|
|
80520
|
+
return "navigation_integrity";
|
|
80521
|
+
}
|
|
80522
|
+
if (/\b(permission|camera|microphone|location|allow)\b/.test(text)) {
|
|
80523
|
+
return "permission_system";
|
|
80524
|
+
}
|
|
80525
|
+
if (/\b(double|twice|duplicate submit|double.?tap)\b/.test(text)) {
|
|
80526
|
+
return "double_submit";
|
|
80527
|
+
}
|
|
80528
|
+
if (scenario.type === "HAPPY_PATH" || scenario.variant === "happy") {
|
|
80529
|
+
return "happy_submit";
|
|
80530
|
+
}
|
|
80531
|
+
if (scenario.type === "USER_JOURNEY" || scenario.type === "REGRESSION") {
|
|
80532
|
+
return "navigation_integrity";
|
|
80533
|
+
}
|
|
80534
|
+
return "smoke_nav";
|
|
80535
|
+
}
|
|
80536
|
+
function inferScenarioKinds(scenarios) {
|
|
80537
|
+
return [...new Set(scenarios.map(inferScenarioKind))];
|
|
80538
|
+
}
|
|
80539
|
+
function isStrongOracleKind(kind) {
|
|
80540
|
+
return kind !== "smoke_nav" && kind !== "navigation_integrity";
|
|
80541
|
+
}
|
|
80542
|
+
function formatRequiredScenarioKindsPrompt(signals) {
|
|
80543
|
+
const required = requiredScenarioKinds(signals);
|
|
80544
|
+
if (required.length === 0)
|
|
80545
|
+
return "";
|
|
80546
|
+
const lines = required.map((kind) => {
|
|
80547
|
+
switch (kind) {
|
|
80548
|
+
case "smoke_nav":
|
|
80549
|
+
return '- smoke_nav: reach a primary destination and prove it with a durable product signal (not only "tab visible")';
|
|
80550
|
+
case "happy_submit":
|
|
80551
|
+
return "- happy_submit: complete a form/primary action and assert the durable result (new row, saved state, success that outlives a toast)";
|
|
80552
|
+
case "negative_validation":
|
|
80553
|
+
return "- negative_validation: submit empty/invalid input (e.g. empty required, malformed email-like field) and assert error OR blocked submit \u2014 never invent success copy";
|
|
80554
|
+
case "crud_identity":
|
|
80555
|
+
return '- crud_identity: create or note an entity id/label, perform delete/edit on THAT entity, assert identity set changed correctly (not "list still visible")';
|
|
80556
|
+
case "count_delta":
|
|
80557
|
+
return "- count_delta: capture a numeric badge/count, perform an action, assert the number changed as expected";
|
|
80558
|
+
case "persistence_roundtrip":
|
|
80559
|
+
return "- persistence_roundtrip: change a setting/toggle, leave the screen (or relaunch if policy allows), return, assert value persisted";
|
|
80560
|
+
case "auth_session":
|
|
80561
|
+
return "- auth_session: prove sign-in session (profile/sign-out) and/or sign-out leaves only logged-out chrome";
|
|
80562
|
+
case "navigation_integrity":
|
|
80563
|
+
return "- navigation_integrity: tab/drawer/back reaches the intended screen family";
|
|
80564
|
+
case "permission_system":
|
|
80565
|
+
return "- permission_system: permission prompt or settings path behaves honestly (allow/deny/settings)";
|
|
80566
|
+
case "double_submit":
|
|
80567
|
+
return "- double_submit: single user intent must not double-create (assert count +1 not +2, or single confirmation)";
|
|
80568
|
+
default:
|
|
80569
|
+
return `- ${kind}`;
|
|
80570
|
+
}
|
|
80571
|
+
});
|
|
80572
|
+
return "## Required scenario kinds (from observed UI \u2014 fill each or explain impossibility in reasoning)\nThese kinds are derived ONLY from controls/screens the explorer saw. Plan at least one scenario per kind when evidence supports it.\n" + lines.join("\n");
|
|
80573
|
+
}
|
|
80574
|
+
function formatCoverageGapLog(gaps) {
|
|
80575
|
+
if (gaps.length === 0)
|
|
80576
|
+
return "Scenario taxonomy: all required kinds covered by plan";
|
|
80577
|
+
return `Scenario taxonomy coverage gaps (required but not planned): ${gaps.join(", ")}`;
|
|
80578
|
+
}
|
|
80579
|
+
}
|
|
80580
|
+
});
|
|
80581
|
+
|
|
80582
|
+
// ../runner-core/dist/services/mobile/coverage-map.js
|
|
80583
|
+
var require_coverage_map = __commonJS({
|
|
80584
|
+
"../runner-core/dist/services/mobile/coverage-map.js"(exports2) {
|
|
80585
|
+
"use strict";
|
|
80586
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
80587
|
+
exports2.buildMobileCoverageMap = buildMobileCoverageMap;
|
|
80588
|
+
var scenario_taxonomy_js_1 = require_scenario_taxonomy();
|
|
80589
|
+
function strongLookup(name, map2) {
|
|
80590
|
+
if (!map2)
|
|
80591
|
+
return false;
|
|
80592
|
+
if (map2 instanceof Map)
|
|
80593
|
+
return map2.get(name) === true;
|
|
80594
|
+
return Object.prototype.hasOwnProperty.call(map2, name) && map2[name] === true;
|
|
80595
|
+
}
|
|
80596
|
+
function buildMobileCoverageMap(input) {
|
|
80597
|
+
const signals = (0, scenario_taxonomy_js_1.extractUiSignals)(input.graph, input.appId);
|
|
80598
|
+
const required = (0, scenario_taxonomy_js_1.requiredScenarioKinds)(signals);
|
|
80599
|
+
const planned = input.plannedScenarios ?? [];
|
|
80600
|
+
const plannedKinds = planned.map(scenario_taxonomy_js_1.inferScenarioKind);
|
|
80601
|
+
const plannedSet = new Set(plannedKinds);
|
|
80602
|
+
const gaps = (0, scenario_taxonomy_js_1.coverageGaps)(signals, plannedKinds);
|
|
80603
|
+
const kindsWithStrong = /* @__PURE__ */ new Set();
|
|
80604
|
+
for (const s of planned) {
|
|
80605
|
+
const kind = (0, scenario_taxonomy_js_1.inferScenarioKind)(s);
|
|
80606
|
+
const isStrong = (0, scenario_taxonomy_js_1.isStrongOracleKind)(kind) || strongLookup(s.name, input.strongByScenarioName);
|
|
80607
|
+
if (isStrong)
|
|
80608
|
+
kindsWithStrong.add(kind);
|
|
80609
|
+
}
|
|
80610
|
+
const warnings = [];
|
|
80611
|
+
const entries = required.map((kind) => {
|
|
80612
|
+
const isPlanned = plannedSet.has(kind);
|
|
80613
|
+
const strongPlanned = kindsWithStrong.has(kind);
|
|
80614
|
+
let warning;
|
|
80615
|
+
if (!isPlanned) {
|
|
80616
|
+
warning = `Required kind "${kind}" has no planned scenario`;
|
|
80617
|
+
warnings.push(warning);
|
|
80618
|
+
} else if (!strongPlanned && (0, scenario_taxonomy_js_1.isStrongOracleKind)(kind)) {
|
|
80619
|
+
warning = `Kind "${kind}" is planned but has no strong-oracle scenario (smoke-only coverage)`;
|
|
80620
|
+
warnings.push(warning);
|
|
80621
|
+
}
|
|
80622
|
+
return {
|
|
80623
|
+
kind,
|
|
80624
|
+
required: true,
|
|
80625
|
+
planned: isPlanned,
|
|
80626
|
+
strongPlanned,
|
|
80627
|
+
warning
|
|
80628
|
+
};
|
|
80629
|
+
});
|
|
80630
|
+
if (planned.length > 0 && plannedKinds.every((k) => k === "smoke_nav" || k === "navigation_integrity")) {
|
|
80631
|
+
const w = "All planned scenarios are smoke/nav \u2014 no strong product oracles";
|
|
80632
|
+
if (!warnings.includes(w))
|
|
80633
|
+
warnings.push(w);
|
|
80634
|
+
}
|
|
80635
|
+
const summary = warnings.length === 0 ? `Coverage map: ${required.length} required kinds, all covered with strong scenarios where needed` : `Coverage map: ${warnings.length} warning(s); ${(0, scenario_taxonomy_js_1.formatCoverageGapLog)(gaps)}`;
|
|
80636
|
+
return {
|
|
80637
|
+
signals,
|
|
80638
|
+
requiredKinds: required,
|
|
80639
|
+
gaps,
|
|
80640
|
+
entries,
|
|
80641
|
+
warnings,
|
|
80642
|
+
summary
|
|
80643
|
+
};
|
|
80644
|
+
}
|
|
80645
|
+
}
|
|
80646
|
+
});
|
|
80647
|
+
|
|
80308
80648
|
// ../runner-core/dist/services/mobile/mobile-discovery-prompts.js
|
|
80309
80649
|
var require_mobile_discovery_prompts = __commonJS({
|
|
80310
80650
|
"../runner-core/dist/services/mobile/mobile-discovery-prompts.js"(exports2) {
|
|
@@ -80322,6 +80662,8 @@ var require_mobile_discovery_prompts = __commonJS({
|
|
|
80322
80662
|
var ai_provider_js_1 = require_ai_provider();
|
|
80323
80663
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
80324
80664
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
80665
|
+
var scenario_taxonomy_js_1 = require_scenario_taxonomy();
|
|
80666
|
+
var coverage_map_js_1 = require_coverage_map();
|
|
80325
80667
|
function isTargetAppScreenId(screenId, appId) {
|
|
80326
80668
|
if (!appId)
|
|
80327
80669
|
return true;
|
|
@@ -80396,14 +80738,14 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
80396
80738
|
2. **One purpose per scenario** - each scenario tests ONE thing (happy path, validation, journey, or bug)
|
|
80397
80739
|
3. **Happy paths first** - then pair edge cases to the same form/flow
|
|
80398
80740
|
4. **Multi-screen journeys** - only when screen transitions were actually observed. When an observed target element sits below the fold, include an explicit swipe step to scroll it into view before the tap (mobile elements off-screen are not hittable until revealed)
|
|
80399
|
-
5. **Bug reports** - for observed
|
|
80741
|
+
5. **Bug reports** - for observed *product* defects (wrong row deleted, validation bypassed, count wrong). Do NOT plan scenarios whose expected outcome is system ANR / "isn't responding" freezes \u2014 those are environment noise
|
|
80400
80742
|
6. **Steps are human-readable** - "Type into the email field", not "driver.findElement(AppiumBy.accessibilityId('email')).sendKeys()"
|
|
80401
80743
|
7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, tabs, sheets, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, home screen reached). Name the target screen or exact copy only when it is the real product outcome; do NOT list "heading visible", "button visible", or similar element inventory unless that exact control is the behavior under test.
|
|
80402
80744
|
8. **Auth-aware** - mark scenarios that need a signed-in session. Credentials are filled ON-DEVICE from the stored TEST_USER_EMAIL / TEST_USER_PASSWORD keys (key-based injection at run time) \u2014 say "type the test account email", never a literal value and never a code idiom like process.env
|
|
80403
80745
|
9. **No duplicates** - check existing test names before adding
|
|
80404
80746
|
10. **All tests start from the app launch entry** - every generated test starts by launching the target app. The runner clears app data when the device/reset policy safely supports it; otherwise it performs a terminate + activate relaunch, which can preserve app session data. Therefore scenarios MUST navigate through the app's own UI from whatever launch state is valid, and MUST NOT rely on mutable data from a prior scenario disappearing. Steps must reflect this: say "Launch the app and tap Sign In" not "deep-link to the login screen". Do NOT navigate by deep-link; the app's launch + tab/drawer navigation is the only entry path. \`targetPages\` lists the screen-ids involved in the flow.
|
|
80405
80747
|
11. **targetPages must be copied CHARACTER-FOR-CHARACTER from the screen-ids in the evidence** (they look like \`com.example.app.MainActivity#ab12cd34\`). NEVER invent, prettify, or abbreviate them \u2014 no "Landing", "AUTH", "Home Screen", or any name that is not literally in the provided screens. A scenario whose targetPages contains even ONE id not present in the evidence is REJECTED WHOLESALE by the validator, so a single made-up route silently destroys the entire scenario. If you are unsure which screen-id a step touches, use the screen-id of the screen where the flow STARTS.
|
|
80406
|
-
12. **Happy paths must verify the DURABLE OUTCOME, not just the trigger.** A HAPPY_PATH is NOT complete until it verifies the feature's real output. "Button tapped" / "Sheet opened" / "Spinner appeared" / "Settings screen opened" is NOT a happy path - that is a UI interaction test. Form-mutation happy paths (change password, create/edit/save a record, send a message, checkout, etc.) MUST include typing into the relevant fields AND committing the form (submit/save/send or keyboard Enter/Done) \u2014 navigating to the form screen alone is insufficient. Apply this to any feature where an action produces a result the user then sees or uses: CRUD creates, photo/file uploads, searches that return results, imports, exports, payments, messages sent, AI/ML outputs, generated documents, sync runs, analysis runs, conversion, data processing. The scenario MUST include:
|
|
80748
|
+
12. **Happy paths must verify the DURABLE OUTCOME, not just the trigger.** A HAPPY_PATH is NOT complete until it verifies the feature's real output. "Button tapped" / "Sheet opened" / "Spinner appeared" / "Settings screen opened" is NOT a happy path - that is a UI interaction test. When the explorer observed list rows with delete/remove, plan at least one scenario that captures entity identity before the action and asserts that identity is gone or changed afterward (not merely that the list screen is still visible). When numeric badges/counts were observed, plan a before/after count check. Form-mutation happy paths (change password, create/edit/save a record, send a message, checkout, etc.) MUST include typing into the relevant fields AND committing the form (submit/save/send or keyboard Enter/Done) \u2014 navigating to the form screen alone is insufficient. Apply this to any feature where an action produces a result the user then sees or uses: CRUD creates, photo/file uploads, searches that return results, imports, exports, payments, messages sent, AI/ML outputs, generated documents, sync runs, analysis runs, conversion, data processing. The scenario MUST include:
|
|
80407
80749
|
- The trigger step (Tap Submit / Generate / Send / Upload / Run / Analyze / Save / Pay / etc.)
|
|
80408
80750
|
- A wait step for the operation to complete. Phrase it by SIGNAL, not wall-clock ("Wait for the result to appear - up to 60 seconds"). Prefer signals the app actually uses: spinner disappears, status badge flips to a success state, a new row appears, an item count increments, a success toast/snackbar shows, the screen transitions to a result screen.
|
|
80409
80751
|
- A verification step on the DURABLE RESULT - the thing that outlives the transient UI: a new list row, a persisted record, a received message, a committed state change, visible output text. Do NOT assert on the spinner disappearing alone; assert on what replaced it.
|
|
@@ -80803,6 +81145,14 @@ SCOPE: this is a feature-focused deep run. Plan scenarios ONLY for the "${contex
|
|
|
80803
81145
|
sections.push(`## Operator notes
|
|
80804
81146
|
${context.userNotes.trim()}`);
|
|
80805
81147
|
}
|
|
81148
|
+
if (context.mode === "deep" || context.mode === "full") {
|
|
81149
|
+
const signals = (0, scenario_taxonomy_js_1.extractUiSignals)(graph, context.appId);
|
|
81150
|
+
const requiredBlock = (0, scenario_taxonomy_js_1.formatRequiredScenarioKindsPrompt)(signals);
|
|
81151
|
+
if (requiredBlock) {
|
|
81152
|
+
sections.push(requiredBlock);
|
|
81153
|
+
sections.push('## Scenario kind coverage rules\n1. Prefer scenarios whose expectedOutcomes prove durable product results (state deltas, validation errors, persistence), not only "control X is visible".\n2. For negative_validation: empty required field AND/OR malformed email-like input; expected outcome must be blocked submit or visible validation error \u2014 never success Home after invalid submit.\n3. For crud_identity: capture entity identity before delete/edit, then assert that identity is gone/changed (state delta) \u2014 never only that the list screen is still visible.\n4. For count_delta: capture before count, mutate, assert count changed as expected.\n5. For double_submit: assert count +1 not +2 (or single confirmation), not merely that a button remains visible.\n6. Never plan expectedOutcomes that are ANR / System UI freezes.\n7. If a required kind is impossible given evidence, say so in "reasoning" \u2014 do not invent unobserved screens.');
|
|
81154
|
+
}
|
|
81155
|
+
}
|
|
80806
81156
|
const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration || a.screenId.localeCompare(b.screenId));
|
|
80807
81157
|
const includedScreenIds = new Set(orderedScreens.map((state2) => state2.screenId));
|
|
80808
81158
|
const maxElementsPerScreen = context.mode === "survey" ? SURVEY_MAX_ELEMENTS_PER_SCREEN : MAX_ELEMENTS_PER_SCREEN;
|
|
@@ -81526,6 +81876,43 @@ ${constraints.join("\n")}`);
|
|
|
81526
81876
|
mobileAdaptationLogs.push(`Dropped ${guestWallFilter.dropped} guest-wall scenario(s) \u2014 explore session was authenticated; re-exec would not see sign-in gates`);
|
|
81527
81877
|
}
|
|
81528
81878
|
const plannedScenarios = guestWallFilter.scenarios;
|
|
81879
|
+
let taxonomyGapLogs = [];
|
|
81880
|
+
let taxonomyCoverageGaps = result.coverageGaps ?? [];
|
|
81881
|
+
if (input.mode !== "survey" && plannedScenarios.length > 0) {
|
|
81882
|
+
const signals = (0, scenario_taxonomy_js_1.extractUiSignals)(input.graph, input.appId);
|
|
81883
|
+
const plannedKinds = (0, scenario_taxonomy_js_1.inferScenarioKinds)(plannedScenarios);
|
|
81884
|
+
const gaps = (0, scenario_taxonomy_js_1.coverageGaps)(signals, plannedKinds);
|
|
81885
|
+
const coverageMap = (0, coverage_map_js_1.buildMobileCoverageMap)({
|
|
81886
|
+
graph: input.graph,
|
|
81887
|
+
appId: input.appId,
|
|
81888
|
+
plannedScenarios
|
|
81889
|
+
});
|
|
81890
|
+
taxonomyGapLogs = [(0, scenario_taxonomy_js_1.formatCoverageGapLog)(gaps), coverageMap.summary];
|
|
81891
|
+
if (coverageMap.warnings.length > 0) {
|
|
81892
|
+
taxonomyGapLogs.push(...coverageMap.warnings.slice(0, 12));
|
|
81893
|
+
}
|
|
81894
|
+
if (gaps.length > 0) {
|
|
81895
|
+
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}`);
|
|
81896
|
+
const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
81897
|
+
taxonomyCoverageGaps = [
|
|
81898
|
+
...taxonomyCoverageGaps,
|
|
81899
|
+
...gaps.map((kind) => ({
|
|
81900
|
+
scenarioName: `[taxonomy] ${kind}`,
|
|
81901
|
+
reason: `Required scenario kind "${kind}" not represented in the planned set`,
|
|
81902
|
+
suggestion: `Add a ${kind} scenario backed by observed controls, or document impossibility`,
|
|
81903
|
+
unmetPrerequisites: [],
|
|
81904
|
+
detectedAt
|
|
81905
|
+
})),
|
|
81906
|
+
...coverageMap.warnings.filter((w) => /smoke-only|no strong-oracle/i.test(w)).map((w) => ({
|
|
81907
|
+
scenarioName: "[coverage-map] strong-oracle gap",
|
|
81908
|
+
reason: w,
|
|
81909
|
+
suggestion: "Author durable assertStateDelta/assertText outcomes for this kind",
|
|
81910
|
+
unmetPrerequisites: [],
|
|
81911
|
+
detectedAt
|
|
81912
|
+
}))
|
|
81913
|
+
];
|
|
81914
|
+
}
|
|
81915
|
+
}
|
|
81529
81916
|
if (input.mode !== "survey" && plannedScenarios.length === 0 && input.graph.screens.size > 0) {
|
|
81530
81917
|
const fallbackScenarios = buildMobileFallbackScenarios(input);
|
|
81531
81918
|
if (fallbackScenarios.length > 0) {
|
|
@@ -81584,7 +81971,8 @@ ${constraints.join("\n")}`);
|
|
|
81584
81971
|
...result,
|
|
81585
81972
|
scenarios: plannedScenarios,
|
|
81586
81973
|
featureGroups: mobileFeatureGroups,
|
|
81587
|
-
|
|
81974
|
+
coverageGaps: taxonomyCoverageGaps.length > 0 ? taxonomyCoverageGaps : result.coverageGaps,
|
|
81975
|
+
logs: [...result.logs, ...mobileAdaptationLogs, ...taxonomyGapLogs]
|
|
81588
81976
|
};
|
|
81589
81977
|
}
|
|
81590
81978
|
}
|
|
@@ -81638,6 +82026,123 @@ var require_discovery_lint_flags = __commonJS({
|
|
|
81638
82026
|
}
|
|
81639
82027
|
});
|
|
81640
82028
|
|
|
82029
|
+
// ../runner-core/dist/services/mobile/anr-env.js
|
|
82030
|
+
var require_anr_env = __commonJS({
|
|
82031
|
+
"../runner-core/dist/services/mobile/anr-env.js"(exports2) {
|
|
82032
|
+
"use strict";
|
|
82033
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
82034
|
+
exports2.textLooksLikeAnrOrSystemUi = textLooksLikeAnrOrSystemUi;
|
|
82035
|
+
exports2.isAnrOrSystemUiEnvironmentFailure = isAnrOrSystemUiEnvironmentFailure;
|
|
82036
|
+
exports2.stepLooksLikeAnrChromeAssert = stepLooksLikeAnrChromeAssert;
|
|
82037
|
+
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;
|
|
82038
|
+
function textLooksLikeAnrOrSystemUi(text) {
|
|
82039
|
+
if (!text || typeof text !== "string")
|
|
82040
|
+
return false;
|
|
82041
|
+
return ANR_TEXT_RE.test(text);
|
|
82042
|
+
}
|
|
82043
|
+
function isAnrOrSystemUiEnvironmentFailure(error2, logs2) {
|
|
82044
|
+
const blob = `${error2 ?? ""}
|
|
82045
|
+
${logs2 ?? ""}`;
|
|
82046
|
+
if (textLooksLikeAnrOrSystemUi(blob))
|
|
82047
|
+
return true;
|
|
82048
|
+
if (/\bcold-boot\b/i.test(blob) && /\b(froze|hang|anr|unresponsive)\b/i.test(blob))
|
|
82049
|
+
return true;
|
|
82050
|
+
if (/\bno actionable elements\b/i.test(blob) && /\b(system|anr|responding)\b/i.test(blob)) {
|
|
82051
|
+
return true;
|
|
82052
|
+
}
|
|
82053
|
+
return false;
|
|
82054
|
+
}
|
|
82055
|
+
function stepLooksLikeAnrChromeAssert(step) {
|
|
82056
|
+
if (step.action !== "assertVisible" && step.action !== "assertText")
|
|
82057
|
+
return false;
|
|
82058
|
+
return textLooksLikeAnrOrSystemUi([step.description, step.assertion, step.value].filter(Boolean).join(" "));
|
|
82059
|
+
}
|
|
82060
|
+
}
|
|
82061
|
+
});
|
|
82062
|
+
|
|
82063
|
+
// ../runner-core/dist/services/mobile/oracle-quality.js
|
|
82064
|
+
var require_oracle_quality = __commonJS({
|
|
82065
|
+
"../runner-core/dist/services/mobile/oracle-quality.js"(exports2) {
|
|
82066
|
+
"use strict";
|
|
82067
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
82068
|
+
exports2.assessStrongOracleQuality = assessStrongOracleQuality;
|
|
82069
|
+
var anr_env_js_1 = require_anr_env();
|
|
82070
|
+
var CHROME_ONLY_RE = /\b(tab|home tab|catalog tab|forms tab|account tab|back|menu|navigation|nav bar|bottom nav|status bar|query|search field)\b/i;
|
|
82071
|
+
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;
|
|
82072
|
+
function stepBlob(step) {
|
|
82073
|
+
return [step.description, step.assertion, step.value, step.target?.primary].filter(Boolean).join(" ");
|
|
82074
|
+
}
|
|
82075
|
+
function isChromeOnlyVisible(step) {
|
|
82076
|
+
if (step.action !== "assertVisible")
|
|
82077
|
+
return false;
|
|
82078
|
+
const blob = stepBlob(step);
|
|
82079
|
+
if (PRODUCT_OUTCOME_TEXT_RE.test(blob))
|
|
82080
|
+
return false;
|
|
82081
|
+
return CHROME_ONLY_RE.test(blob) || blob.length < 40;
|
|
82082
|
+
}
|
|
82083
|
+
function assessStrongOracleQuality(steps) {
|
|
82084
|
+
const productSteps = steps.filter((s) => s.action !== "launchApp");
|
|
82085
|
+
const anrAsserts = productSteps.filter(anr_env_js_1.stepLooksLikeAnrChromeAssert);
|
|
82086
|
+
const nonAnrAsserts = productSteps.filter((s) => (s.action === "assertVisible" || s.action === "assertText" || s.action === "assertStateDelta") && !(0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s));
|
|
82087
|
+
if (productSteps.length > 0 && anrAsserts.length > 0 && nonAnrAsserts.length === 0) {
|
|
82088
|
+
return {
|
|
82089
|
+
strong: false,
|
|
82090
|
+
unpairedCaptures: 0,
|
|
82091
|
+
hasStateDelta: false,
|
|
82092
|
+
hasStrongAssertText: false,
|
|
82093
|
+
hasStrongValidationVisible: false,
|
|
82094
|
+
anrOnly: true,
|
|
82095
|
+
reason: "only asserts system ANR / freeze chrome \u2014 not a product test"
|
|
82096
|
+
};
|
|
82097
|
+
}
|
|
82098
|
+
const captures = steps.filter((s) => s.action === "captureState").length;
|
|
82099
|
+
const deltas = steps.filter((s) => s.action === "assertStateDelta").length;
|
|
82100
|
+
const unpairedCaptures = Math.max(0, captures - deltas);
|
|
82101
|
+
const hasStateDelta = deltas > 0;
|
|
82102
|
+
const hasStrongAssertText = steps.some((s) => {
|
|
82103
|
+
if (s.action !== "assertText")
|
|
82104
|
+
return false;
|
|
82105
|
+
if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s))
|
|
82106
|
+
return false;
|
|
82107
|
+
const blob = stepBlob(s);
|
|
82108
|
+
if (/^(email|password|username|name|query|search)$/i.test((s.assertion ?? "").trim())) {
|
|
82109
|
+
return false;
|
|
82110
|
+
}
|
|
82111
|
+
return blob.length >= 3;
|
|
82112
|
+
});
|
|
82113
|
+
const hasStrongValidationVisible = steps.some((s) => {
|
|
82114
|
+
if (s.action !== "assertVisible")
|
|
82115
|
+
return false;
|
|
82116
|
+
if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s) || isChromeOnlyVisible(s))
|
|
82117
|
+
return false;
|
|
82118
|
+
return PRODUCT_OUTCOME_TEXT_RE.test(stepBlob(s));
|
|
82119
|
+
});
|
|
82120
|
+
const strong = hasStateDelta || hasStrongAssertText || hasStrongValidationVisible;
|
|
82121
|
+
let reason;
|
|
82122
|
+
if (strong && hasStateDelta) {
|
|
82123
|
+
reason = unpairedCaptures > 0 ? `has assertStateDelta but ${unpairedCaptures} unpaired captureState (prefer 1:1 pairing)` : "has assertStateDelta durable state proof";
|
|
82124
|
+
} else if (strong && hasStrongAssertText) {
|
|
82125
|
+
reason = "has product assertText outcome";
|
|
82126
|
+
} else if (strong) {
|
|
82127
|
+
reason = "has validation/error assertVisible outcome";
|
|
82128
|
+
} else if (unpairedCaptures > 0) {
|
|
82129
|
+
reason = `captureState without assertStateDelta (${unpairedCaptures}) \u2014 not strong`;
|
|
82130
|
+
} else {
|
|
82131
|
+
reason = "no durable state/text/validation oracle \u2014 smoke/nav only";
|
|
82132
|
+
}
|
|
82133
|
+
return {
|
|
82134
|
+
strong,
|
|
82135
|
+
unpairedCaptures,
|
|
82136
|
+
hasStateDelta,
|
|
82137
|
+
hasStrongAssertText,
|
|
82138
|
+
hasStrongValidationVisible,
|
|
82139
|
+
anrOnly: false,
|
|
82140
|
+
reason
|
|
82141
|
+
};
|
|
82142
|
+
}
|
|
82143
|
+
}
|
|
82144
|
+
});
|
|
82145
|
+
|
|
81641
82146
|
// ../runner-core/dist/services/mobile/mobile-generator.js
|
|
81642
82147
|
var require_mobile_generator = __commonJS({
|
|
81643
82148
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
|
|
@@ -81663,6 +82168,9 @@ var require_mobile_generator = __commonJS({
|
|
|
81663
82168
|
var ai_retry_js_1 = require_ai_retry();
|
|
81664
82169
|
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
81665
82170
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
82171
|
+
var scenario_taxonomy_js_1 = require_scenario_taxonomy();
|
|
82172
|
+
var anr_env_js_1 = require_anr_env();
|
|
82173
|
+
var oracle_quality_js_1 = require_oracle_quality();
|
|
81666
82174
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
81667
82175
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
81668
82176
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -81804,6 +82312,50 @@ var require_mobile_generator = __commonJS({
|
|
|
81804
82312
|
}
|
|
81805
82313
|
}
|
|
81806
82314
|
};
|
|
82315
|
+
var MOBILE_CAPTURE_STATE_TOOL = {
|
|
82316
|
+
type: "function",
|
|
82317
|
+
function: {
|
|
82318
|
+
name: "mobile_capture_state",
|
|
82319
|
+
description: "Capture list labels or numeric badge counts into a named state key BEFORE a mutating action (delete/create/toggle). MUST pair with mobile_assert_state_delta afterward \u2014 capture alone is not a strong oracle and will not tag the test @strong-oracle.",
|
|
82320
|
+
parameters: {
|
|
82321
|
+
type: "object",
|
|
82322
|
+
properties: {
|
|
82323
|
+
stateKey: {
|
|
82324
|
+
type: "string",
|
|
82325
|
+
description: 'Short key for this bag entry (e.g. "invoice_list", "open_count"). Reuse the same key in mobile_assert_state_delta.'
|
|
82326
|
+
},
|
|
82327
|
+
kind: {
|
|
82328
|
+
type: "string",
|
|
82329
|
+
enum: ["list_ids", "badge_count"],
|
|
82330
|
+
description: "list_ids = unique visible labels; badge_count = numeric count tokens. Default list_ids."
|
|
82331
|
+
}
|
|
82332
|
+
},
|
|
82333
|
+
required: ["stateKey"]
|
|
82334
|
+
}
|
|
82335
|
+
}
|
|
82336
|
+
};
|
|
82337
|
+
var MOBILE_ASSERT_STATE_DELTA_TOOL = {
|
|
82338
|
+
type: "function",
|
|
82339
|
+
function: {
|
|
82340
|
+
name: "mobile_assert_state_delta",
|
|
82341
|
+
description: "Compare the current screen to a prior mobile_capture_state. Verifies LIVE then records assertStateDelta. Use after delete/create/count-changing actions. expect: changed | entity_removed | count_decreased | count_increased | unchanged | persisted.",
|
|
82342
|
+
parameters: {
|
|
82343
|
+
type: "object",
|
|
82344
|
+
properties: {
|
|
82345
|
+
stateKey: {
|
|
82346
|
+
type: "string",
|
|
82347
|
+
description: "Must match a prior mobile_capture_state stateKey in this scenario."
|
|
82348
|
+
},
|
|
82349
|
+
expect: {
|
|
82350
|
+
type: "string",
|
|
82351
|
+
enum: ["changed", "entity_removed", "count_decreased", "count_increased", "unchanged", "persisted"],
|
|
82352
|
+
description: "What must differ (or persist) vs the prior capture. Default: changed."
|
|
82353
|
+
}
|
|
82354
|
+
},
|
|
82355
|
+
required: ["stateKey"]
|
|
82356
|
+
}
|
|
82357
|
+
}
|
|
82358
|
+
};
|
|
81807
82359
|
var MOBILE_FILL_CREDENTIAL_TOOL = {
|
|
81808
82360
|
type: "function",
|
|
81809
82361
|
function: {
|
|
@@ -81826,7 +82378,7 @@ var require_mobile_generator = __commonJS({
|
|
|
81826
82378
|
type: "function",
|
|
81827
82379
|
function: {
|
|
81828
82380
|
name: "finish_generate",
|
|
81829
|
-
description: "Call when the scenario is fully exercised and its outcome is asserted. verified:true requires you to have called at least one assert tool (mobile_assert_visible / mobile_assert_text) \u2014 claiming verified without any assert is auto-downgraded.",
|
|
82381
|
+
description: "Call when the scenario is fully exercised and its outcome is asserted. verified:true requires you to have called at least one assert tool (mobile_assert_visible / mobile_assert_text / mobile_assert_state_delta) \u2014 claiming verified without any assert is auto-downgraded.",
|
|
81830
82382
|
parameters: {
|
|
81831
82383
|
type: "object",
|
|
81832
82384
|
properties: {
|
|
@@ -81858,6 +82410,8 @@ var require_mobile_generator = __commonJS({
|
|
|
81858
82410
|
MOBILE_RELAUNCH_TOOL,
|
|
81859
82411
|
MOBILE_ASSERT_VISIBLE_TOOL,
|
|
81860
82412
|
MOBILE_ASSERT_TEXT_TOOL,
|
|
82413
|
+
MOBILE_CAPTURE_STATE_TOOL,
|
|
82414
|
+
MOBILE_ASSERT_STATE_DELTA_TOOL,
|
|
81861
82415
|
FINISH_GENERATE_TOOL
|
|
81862
82416
|
];
|
|
81863
82417
|
var STRUCTURAL_KEY_MAX_ELEMENTS = 16;
|
|
@@ -82124,8 +82678,11 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
|
|
|
82124
82678
|
- Navigation scenarios: you MUST land on the destination and assert something unique to it.
|
|
82125
82679
|
4. PROVE the outcome with an assert tool that matches the scenario \u2014 not ambient chrome:
|
|
82126
82680
|
- mobile_assert_visible({ref}) / mobile_assert_text({ref, expected})
|
|
82681
|
+
- For list delete/create or count changes: mobile_capture_state({stateKey, kind}) BEFORE the mutation, then mobile_assert_state_delta({stateKey, expect}) AFTER \u2014 BOTH are required. captureState alone does NOT prove the outcome.
|
|
82682
|
+
- 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".
|
|
82127
82683
|
- 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.
|
|
82128
82684
|
- Do NOT assert bottom-nav tab labels alone, notification badge counts ("N unread"), or one-time empty-state CTAs as the scenario outcome.
|
|
82685
|
+
- 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.
|
|
82129
82686
|
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.
|
|
82130
82687
|
|
|
82131
82688
|
## Rules
|
|
@@ -82134,7 +82691,7 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
|
|
|
82134
82691
|
- Assert only DURABLE elements \u2014 screen chrome that exists on every visit (tab labels, headers, nav bars, the entity you just created). NEVER assert one-time / data-dependent UI: empty-state CTAs, first-run or onboarding prompts, or counters \u2014 they vanish as soon as the account has data, and the test then fails forever on a healthy app.
|
|
82135
82692
|
- 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.
|
|
82136
82693
|
- 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.
|
|
82137
|
-
- If the APP itself misbehaves (
|
|
82694
|
+
- 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.
|
|
82138
82695
|
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
82139
82696
|
- NEVER tap the phone's home screen / launcher (app icons, "Predicted app" chips, search bars, widgets). Those are NOT part of the app under test \u2014 tapping them is junk that poisons the test. If the app ever backgrounds or you find yourself on the launcher, use mobile_relaunch to cold-start the app; do NOT tap a launcher icon or app-prediction to get back in.
|
|
82140
82697
|
- Do NOT re-do a sequence you already completed. If you filled a form then navigated away, do NOT come back and fill it again \u2014 the scenario needs ONE clean pass. Filling the login form, going back, and re-filling it is exactly the kind of wandering that produces a bloated, broken test.
|
|
@@ -82237,6 +82794,7 @@ Start by calling mobile_snapshot, then execute ONLY the steps needed for this go
|
|
|
82237
82794
|
const { scenario, driver, platform: platform3, maxIterations, agentModel, provider, aiClient, auditGroupId, onAICall, onScreenshot, targetAppId, credentials, redactionCredentials, log: emitLog, allowRelaunch } = args;
|
|
82238
82795
|
const log2 = (line) => emitLog(redactCredentialValues(line, redactionCredentials));
|
|
82239
82796
|
const actions = [];
|
|
82797
|
+
const stateBag = /* @__PURE__ */ new Map();
|
|
82240
82798
|
let assertCount = 0;
|
|
82241
82799
|
let finish = null;
|
|
82242
82800
|
let finishIntentRejections = 0;
|
|
@@ -82706,7 +83264,9 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82706
83264
|
const screenDependentTools = /* @__PURE__ */ new Set([
|
|
82707
83265
|
...screenMutatingTools,
|
|
82708
83266
|
"mobile_assert_visible",
|
|
82709
|
-
"mobile_assert_text"
|
|
83267
|
+
"mobile_assert_text",
|
|
83268
|
+
"mobile_capture_state",
|
|
83269
|
+
"mobile_assert_state_delta"
|
|
82710
83270
|
]);
|
|
82711
83271
|
for (const toolCall of assistantMessage.tool_calls) {
|
|
82712
83272
|
const toolName = toolCall.function.name;
|
|
@@ -82750,7 +83310,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82750
83310
|
if (intentReason)
|
|
82751
83311
|
reasons.push(intentReason);
|
|
82752
83312
|
if (verifiedWithoutAssert) {
|
|
82753
|
-
reasons.push("claimed verified:true but never called mobile_assert_visible / mobile_assert_text");
|
|
83313
|
+
reasons.push("claimed verified:true but never called mobile_assert_visible / mobile_assert_text / mobile_assert_state_delta");
|
|
82754
83314
|
}
|
|
82755
83315
|
if (strengthRejectReason !== null) {
|
|
82756
83316
|
reasons.push(`assertion strength: ${strengthRejectReason} \u2014 your only live proof re-checks a control you already used. Assert the scenario OUTCOME instead: an element on the resulting screen, the created/updated row, or the success/error message.`);
|
|
@@ -82821,10 +83381,12 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82821
83381
|
}
|
|
82822
83382
|
} : action;
|
|
82823
83383
|
actions.push((0, shared_1.placeholderizeRecordedMobileAction)(finalizeAction(stamped), credentials));
|
|
82824
|
-
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT")
|
|
83384
|
+
if (action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT" || action.type === "ASSERT_STATE_DELTA") {
|
|
82825
83385
|
assertCount++;
|
|
83386
|
+
}
|
|
82826
83387
|
},
|
|
82827
83388
|
recordedActions: actions,
|
|
83389
|
+
stateBag,
|
|
82828
83390
|
log: log2
|
|
82829
83391
|
});
|
|
82830
83392
|
if (screenMutatingTools.has(toolName))
|
|
@@ -82843,7 +83405,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82843
83405
|
return { actions, assertCount, finish, lastSnapshot: lastTargetSnapshot, lastScreenName: lastTargetScreenName };
|
|
82844
83406
|
}
|
|
82845
83407
|
async function dispatchGenerateTool(args) {
|
|
82846
|
-
const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, allowRelaunch, recordAction, recordedActions, log: log2 } = args;
|
|
83408
|
+
const { toolName, toolArgs, driver, platform: platform3, targetAppId, latest, absorbObservation, resolveRef, resolveHybridRef, maybeEnrichHybridWeb, setLatest, credentials, screenName, allowRelaunch, recordAction, recordedActions, stateBag, log: log2 } = args;
|
|
82847
83409
|
const unknownRef = (ref) => ({
|
|
82848
83410
|
ok: false,
|
|
82849
83411
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
@@ -83388,10 +83950,170 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83388
83950
|
target: metadata.targetLabel
|
|
83389
83951
|
};
|
|
83390
83952
|
}
|
|
83953
|
+
case "mobile_capture_state": {
|
|
83954
|
+
const stateKey = nonEmpty3(typeof toolArgs.stateKey === "string" ? toolArgs.stateKey : "");
|
|
83955
|
+
if (!stateKey) {
|
|
83956
|
+
return { ok: false, error: "mobile_capture_state requires a non-empty stateKey." };
|
|
83957
|
+
}
|
|
83958
|
+
const kindRaw = typeof toolArgs.kind === "string" ? toolArgs.kind.trim() : "list_ids";
|
|
83959
|
+
const kind = kindRaw === "badge_count" ? "badge_count" : "list_ids";
|
|
83960
|
+
const snap = latest();
|
|
83961
|
+
if (!snap) {
|
|
83962
|
+
return { ok: false, error: "No snapshot yet \u2014 call mobile_snapshot before mobile_capture_state." };
|
|
83963
|
+
}
|
|
83964
|
+
const values = captureValuesFromElements(snap.screen.elements, kind);
|
|
83965
|
+
stateBag.set(stateKey, { kind, values });
|
|
83966
|
+
recordAction({
|
|
83967
|
+
type: "CAPTURE_STATE",
|
|
83968
|
+
selector: stateKey,
|
|
83969
|
+
metadata: {
|
|
83970
|
+
screenName: screenName(),
|
|
83971
|
+
targetLabel: stateKey,
|
|
83972
|
+
stateKey,
|
|
83973
|
+
stateKind: kind
|
|
83974
|
+
}
|
|
83975
|
+
});
|
|
83976
|
+
log2(` capture_state: key=${stateKey} kind=${kind} n=${values.length}`);
|
|
83977
|
+
return {
|
|
83978
|
+
ok: true,
|
|
83979
|
+
captured: true,
|
|
83980
|
+
stateKey,
|
|
83981
|
+
kind,
|
|
83982
|
+
sample: values.slice(0, 8),
|
|
83983
|
+
count: values.length
|
|
83984
|
+
};
|
|
83985
|
+
}
|
|
83986
|
+
case "mobile_assert_state_delta": {
|
|
83987
|
+
const stateKey = nonEmpty3(typeof toolArgs.stateKey === "string" ? toolArgs.stateKey : "");
|
|
83988
|
+
if (!stateKey) {
|
|
83989
|
+
return { ok: false, error: "mobile_assert_state_delta requires a non-empty stateKey." };
|
|
83990
|
+
}
|
|
83991
|
+
const prior = stateBag.get(stateKey);
|
|
83992
|
+
if (!prior) {
|
|
83993
|
+
return {
|
|
83994
|
+
ok: false,
|
|
83995
|
+
error: `No prior mobile_capture_state for key "${stateKey}". Capture before the mutating action, then assert.`
|
|
83996
|
+
};
|
|
83997
|
+
}
|
|
83998
|
+
const expectRaw = typeof toolArgs.expect === "string" ? toolArgs.expect.trim() : "changed";
|
|
83999
|
+
const expect = expectRaw || "changed";
|
|
84000
|
+
const snap = latest();
|
|
84001
|
+
if (!snap) {
|
|
84002
|
+
return { ok: false, error: "No snapshot yet \u2014 call mobile_snapshot after the action, then assert_state_delta." };
|
|
84003
|
+
}
|
|
84004
|
+
const current = captureValuesFromElements(snap.screen.elements, prior.kind);
|
|
84005
|
+
const liveError = liveAssertStateDelta(prior.values, current, expect, stateKey);
|
|
84006
|
+
if (liveError) {
|
|
84007
|
+
return {
|
|
84008
|
+
ok: false,
|
|
84009
|
+
error: liveError,
|
|
84010
|
+
beforeSample: prior.values.slice(0, 8),
|
|
84011
|
+
afterSample: current.slice(0, 8)
|
|
84012
|
+
};
|
|
84013
|
+
}
|
|
84014
|
+
recordAction({
|
|
84015
|
+
type: "ASSERT_STATE_DELTA",
|
|
84016
|
+
value: expect,
|
|
84017
|
+
selector: stateKey,
|
|
84018
|
+
metadata: {
|
|
84019
|
+
screenName: screenName(),
|
|
84020
|
+
targetLabel: stateKey,
|
|
84021
|
+
stateKey,
|
|
84022
|
+
stateKind: prior.kind,
|
|
84023
|
+
expect
|
|
84024
|
+
}
|
|
84025
|
+
});
|
|
84026
|
+
log2(` assert_state_delta: key=${stateKey} expect=${expect}`);
|
|
84027
|
+
return {
|
|
84028
|
+
ok: true,
|
|
84029
|
+
asserted: "state_delta",
|
|
84030
|
+
stateKey,
|
|
84031
|
+
expect,
|
|
84032
|
+
beforeCount: prior.values.length,
|
|
84033
|
+
afterCount: current.length
|
|
84034
|
+
};
|
|
84035
|
+
}
|
|
83391
84036
|
default:
|
|
83392
84037
|
return { ok: false, error: `Unknown tool: ${toolName}` };
|
|
83393
84038
|
}
|
|
83394
84039
|
}
|
|
84040
|
+
function captureValuesFromElements(elements, kind) {
|
|
84041
|
+
if (kind === "badge_count") {
|
|
84042
|
+
const nums = /* @__PURE__ */ new Set();
|
|
84043
|
+
const preferred = /* @__PURE__ */ new Set();
|
|
84044
|
+
for (const el of elements) {
|
|
84045
|
+
const t = [el.label, el.text, el.accessibilityId].filter(Boolean).join(" ");
|
|
84046
|
+
for (const m of t.matchAll(/\b(\d{1,6})\b/g))
|
|
84047
|
+
nums.add(m[1]);
|
|
84048
|
+
if (/(?:count|badge|open|unread|item|invoice|result|pending)/i.test(t)) {
|
|
84049
|
+
for (const m of t.matchAll(/\b(\d{1,6})\b/g))
|
|
84050
|
+
preferred.add(m[1]);
|
|
84051
|
+
}
|
|
84052
|
+
}
|
|
84053
|
+
return preferred.size > 0 ? [...preferred] : [...nums].slice(0, 20);
|
|
84054
|
+
}
|
|
84055
|
+
const labels = /* @__PURE__ */ new Set();
|
|
84056
|
+
for (const el of elements) {
|
|
84057
|
+
for (const raw of [el.label, el.text, el.accessibilityId]) {
|
|
84058
|
+
const t = raw?.trim() ?? "";
|
|
84059
|
+
if (t.length < 2 || t.length > 80)
|
|
84060
|
+
continue;
|
|
84061
|
+
if (/^(true|false|null|OK|Cancel)$/i.test(t))
|
|
84062
|
+
continue;
|
|
84063
|
+
labels.add(t);
|
|
84064
|
+
}
|
|
84065
|
+
}
|
|
84066
|
+
return [...labels].slice(0, 80);
|
|
84067
|
+
}
|
|
84068
|
+
function liveAssertStateDelta(before, after, expect, key) {
|
|
84069
|
+
const beforeSet = new Set(before);
|
|
84070
|
+
const afterSet = new Set(after);
|
|
84071
|
+
const removed = before.filter((v) => !afterSet.has(v));
|
|
84072
|
+
const added = after.filter((v) => !beforeSet.has(v));
|
|
84073
|
+
const exp = expect.toLowerCase().trim();
|
|
84074
|
+
if (exp === "changed" || exp === "any_change") {
|
|
84075
|
+
if (removed.length === 0 && added.length === 0) {
|
|
84076
|
+
return `assertStateDelta(${key}): expected change, captures identical (${before.length} values)`;
|
|
84077
|
+
}
|
|
84078
|
+
return null;
|
|
84079
|
+
}
|
|
84080
|
+
if (exp === "removed" || exp === "entity_removed" || exp.startsWith("removed:")) {
|
|
84081
|
+
if (removed.length === 0) {
|
|
84082
|
+
return `assertStateDelta(${key}): expected at least one entity removed`;
|
|
84083
|
+
}
|
|
84084
|
+
return null;
|
|
84085
|
+
}
|
|
84086
|
+
if (exp === "count_decreased" || exp === "count-1") {
|
|
84087
|
+
const b = before.map(Number).filter((n) => Number.isFinite(n));
|
|
84088
|
+
const a = after.map(Number).filter((n) => Number.isFinite(n));
|
|
84089
|
+
const bMax = b.length ? Math.max(...b) : NaN;
|
|
84090
|
+
const aMax = a.length ? Math.max(...a) : NaN;
|
|
84091
|
+
if (!Number.isFinite(bMax) || !Number.isFinite(aMax) || aMax >= bMax) {
|
|
84092
|
+
return `assertStateDelta(${key}): expected count decrease (before max=${bMax}, after max=${aMax})`;
|
|
84093
|
+
}
|
|
84094
|
+
return null;
|
|
84095
|
+
}
|
|
84096
|
+
if (exp === "count_increased" || exp === "count+1") {
|
|
84097
|
+
const b = before.map(Number).filter((n) => Number.isFinite(n));
|
|
84098
|
+
const a = after.map(Number).filter((n) => Number.isFinite(n));
|
|
84099
|
+
const bMax = b.length ? Math.max(...b) : NaN;
|
|
84100
|
+
const aMax = a.length ? Math.max(...a) : NaN;
|
|
84101
|
+
if (!Number.isFinite(bMax) || !Number.isFinite(aMax) || aMax <= bMax) {
|
|
84102
|
+
return `assertStateDelta(${key}): expected count increase (before max=${bMax}, after max=${aMax})`;
|
|
84103
|
+
}
|
|
84104
|
+
return null;
|
|
84105
|
+
}
|
|
84106
|
+
if (exp === "unchanged" || exp === "persisted") {
|
|
84107
|
+
if (removed.length > 0 || added.length > 0) {
|
|
84108
|
+
return `assertStateDelta(${key}): expected persistence/unchanged but set changed`;
|
|
84109
|
+
}
|
|
84110
|
+
return null;
|
|
84111
|
+
}
|
|
84112
|
+
if (removed.length === 0 && added.length === 0) {
|
|
84113
|
+
return `assertStateDelta(${key}): expected change (expect=${expect}), state identical`;
|
|
84114
|
+
}
|
|
84115
|
+
return null;
|
|
84116
|
+
}
|
|
83395
84117
|
function recordAssertVisible(toolArgs, ctx) {
|
|
83396
84118
|
const { resolveRef, latest, screenName, platform: platform3, recordAction } = ctx;
|
|
83397
84119
|
const element = resolveRef(toolArgs.ref);
|
|
@@ -83561,6 +84283,9 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83561
84283
|
function weakAssertionTargetReason(element) {
|
|
83562
84284
|
if (isAlwaysPresentContainer(element))
|
|
83563
84285
|
return "always-present app/system container";
|
|
84286
|
+
if ((0, anr_env_js_1.textLooksLikeAnrOrSystemUi)([element.text, element.label, element.accessibilityId, element.resourceId].filter(Boolean).join(" "))) {
|
|
84287
|
+
return "system ANR / freeze dialog chrome (environment, not product)";
|
|
84288
|
+
}
|
|
83564
84289
|
const hasIdentitySignal = Boolean(nonEmpty3(element.accessibilityId) || nonEmpty3(element.resourceId) || nonEmpty3(element.text) || nonEmpty3(element.label));
|
|
83565
84290
|
if (!hasIdentitySignal)
|
|
83566
84291
|
return "no identity locator (accessibility id / resource id / text) for the assert gate";
|
|
@@ -84371,6 +85096,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
84371
85096
|
lines.push(` await expect(page.getByText('${expected}')).toBeVisible();`);
|
|
84372
85097
|
break;
|
|
84373
85098
|
}
|
|
85099
|
+
case "CAPTURE_STATE":
|
|
85100
|
+
lines.push(` // captureState ${escape((action.metadata.stateKey ?? label).slice(0, 40))}`);
|
|
85101
|
+
break;
|
|
85102
|
+
case "ASSERT_STATE_DELTA": {
|
|
85103
|
+
const expect = escape((action.metadata.expect ?? action.value ?? "changed").slice(0, 40));
|
|
85104
|
+
lines.push(` await expect(page.getByText('state:${expect}')).toBeVisible();`);
|
|
85105
|
+
break;
|
|
85106
|
+
}
|
|
84374
85107
|
case "CLEAR":
|
|
84375
85108
|
lines.push(` await page.getByLabel('${label}').fill('');`);
|
|
84376
85109
|
break;
|
|
@@ -84414,6 +85147,46 @@ ${statePacket}`, redactionCredentials) },
|
|
|
84414
85147
|
return [action.selector, action.value, action.innerText, action.metadata?.targetLabel].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
|
|
84415
85148
|
}
|
|
84416
85149
|
function evaluateMobileAssertionStrength(actions) {
|
|
85150
|
+
const anrAsserts = actions.filter((a) => {
|
|
85151
|
+
if (a.type !== "ASSERT_VISIBLE" && a.type !== "ASSERT_TEXT")
|
|
85152
|
+
return false;
|
|
85153
|
+
return (0, anr_env_js_1.textLooksLikeAnrOrSystemUi)([a.value, a.innerText, a.metadata?.targetLabel, a.selector].filter(Boolean).join(" "));
|
|
85154
|
+
});
|
|
85155
|
+
if (anrAsserts.length > 0) {
|
|
85156
|
+
const otherAsserts = actions.filter((a) => (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT" || a.type === "ASSERT_STATE_DELTA") && !anrAsserts.includes(a));
|
|
85157
|
+
if (otherAsserts.length === 0) {
|
|
85158
|
+
return {
|
|
85159
|
+
ok: false,
|
|
85160
|
+
severity: "block",
|
|
85161
|
+
reason: `only asserts system ANR / "isn't responding" chrome \u2014 not a product test (discard or re-record on a healthy session)`,
|
|
85162
|
+
signals: {
|
|
85163
|
+
apiTest: false,
|
|
85164
|
+
interacted: actions.some((a) => a.type === "TAP" || a.type === "TYPE"),
|
|
85165
|
+
mutated: false,
|
|
85166
|
+
navOnly: false,
|
|
85167
|
+
hasOutcomeProof: false,
|
|
85168
|
+
errorBanner: false,
|
|
85169
|
+
terminal: null
|
|
85170
|
+
}
|
|
85171
|
+
};
|
|
85172
|
+
}
|
|
85173
|
+
}
|
|
85174
|
+
if (actions.some((a) => a.type === "ASSERT_STATE_DELTA")) {
|
|
85175
|
+
return {
|
|
85176
|
+
ok: true,
|
|
85177
|
+
severity: "none",
|
|
85178
|
+
reason: "state-delta oracle proves a durable product change (identity/count/persistence)",
|
|
85179
|
+
signals: {
|
|
85180
|
+
apiTest: false,
|
|
85181
|
+
interacted: actions.some((a) => a.type === "TAP" || a.type === "TYPE" || a.type === "CLEAR"),
|
|
85182
|
+
mutated: actions.some((a) => a.type === "TYPE" || a.type === "CLEAR" || a.type === "TAP"),
|
|
85183
|
+
navOnly: false,
|
|
85184
|
+
hasOutcomeProof: true,
|
|
85185
|
+
errorBanner: false,
|
|
85186
|
+
terminal: null
|
|
85187
|
+
}
|
|
85188
|
+
};
|
|
85189
|
+
}
|
|
84417
85190
|
const modelAsserts = actions.filter((a) => a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT");
|
|
84418
85191
|
if (modelAsserts.length === 0) {
|
|
84419
85192
|
return {
|
|
@@ -84544,9 +85317,10 @@ ${statePacket}`, redactionCredentials) },
|
|
|
84544
85317
|
}
|
|
84545
85318
|
const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
|
|
84546
85319
|
const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
|
|
84547
|
-
const
|
|
85320
|
+
const bugLooksLikeAnr = (0, anr_env_js_1.textLooksLikeAnrOrSystemUi)(bugDescription);
|
|
85321
|
+
const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0 && !bugLooksLikeAnr;
|
|
84548
85322
|
if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
|
|
84549
|
-
log2(" suspectedBug declared but NOT honored: " + (process.env.DISCOVERY_BUG_WITNESS_ENABLED !== "true" ? "bug-witness channel disabled (DISCOVERY_BUG_WITNESS_ENABLED)." : bugDescription.length < 12 ? "bugDescription too short." : "no honored assert backs the observation."));
|
|
85323
|
+
log2(" suspectedBug declared but NOT honored: " + (bugLooksLikeAnr ? "ANR/System UI freeze is environment noise, not a product bug-witness." : process.env.DISCOVERY_BUG_WITNESS_ENABLED !== "true" ? "bug-witness channel disabled (DISCOVERY_BUG_WITNESS_ENABLED)." : bugDescription.length < 12 ? "bugDescription too short." : "no honored assert backs the observation."));
|
|
84550
85324
|
}
|
|
84551
85325
|
const strengthVerdict = bugWitnessHonored ? {
|
|
84552
85326
|
...baseStrengthVerdict,
|
|
@@ -84583,6 +85357,26 @@ ${statePacket}`, redactionCredentials) },
|
|
|
84583
85357
|
if (coldResetUnavailable)
|
|
84584
85358
|
tags.push("@warm-state");
|
|
84585
85359
|
const strengthBlocked = !strengthVerdict.ok || strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug" || !verified || coldResetUnavailable;
|
|
85360
|
+
{
|
|
85361
|
+
const kind = (0, scenario_taxonomy_js_1.inferScenarioKind)(scenario);
|
|
85362
|
+
const oracleQuality = (0, oracle_quality_js_1.assessStrongOracleQuality)(compiled.steps);
|
|
85363
|
+
if (oracleQuality.anrOnly) {
|
|
85364
|
+
log2(` discarding ANR-chrome-only recording: ${oracleQuality.reason}`);
|
|
85365
|
+
return null;
|
|
85366
|
+
}
|
|
85367
|
+
if (oracleQuality.unpairedCaptures > 0) {
|
|
85368
|
+
log2(` oracle quality: ${oracleQuality.reason}`);
|
|
85369
|
+
}
|
|
85370
|
+
const strong = verified && !strengthBlocked && strengthVerdict.severity === "none" && oracleQuality.strong && ((0, scenario_taxonomy_js_1.isStrongOracleKind)(kind) || oracleQuality.hasStateDelta || oracleQuality.hasStrongAssertText);
|
|
85371
|
+
if (strong)
|
|
85372
|
+
tags.push("@strong-oracle");
|
|
85373
|
+
else if (!tags.includes("@smoke")) {
|
|
85374
|
+
tags.push("@smoke");
|
|
85375
|
+
if (!oracleQuality.strong) {
|
|
85376
|
+
log2(` tagged @smoke (not strong-oracle): ${oracleQuality.reason}`);
|
|
85377
|
+
}
|
|
85378
|
+
}
|
|
85379
|
+
}
|
|
84586
85380
|
const strengthReason = coldResetUnavailable ? `Cold app-data reset unavailable${coldResetReason ? ` (${coldResetReason})` : ""} \u2014 generation may not match cold-boot replay. ${strengthVerdict.reason}` : !verified && recording.assertCount === 0 ? "Mobile scenario finished unverified \u2014 the agent never proved the scenario outcome live, so the recording may not demonstrate the goal (e.g. a happy path that never completed). Review before promotion." : strengthVerdict.reason;
|
|
84587
85381
|
const test = {
|
|
84588
85382
|
name: compiled.name,
|
|
@@ -84830,6 +85624,354 @@ Mobile generate phase complete: ${tests.length}/${scenarios.length} test(s) gene
|
|
|
84830
85624
|
}
|
|
84831
85625
|
});
|
|
84832
85626
|
|
|
85627
|
+
// ../runner-core/dist/services/mobile/failure-triage.js
|
|
85628
|
+
var require_failure_triage = __commonJS({
|
|
85629
|
+
"../runner-core/dist/services/mobile/failure-triage.js"(exports2) {
|
|
85630
|
+
"use strict";
|
|
85631
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
85632
|
+
exports2.hasStrongMobileOracle = hasStrongMobileOracle;
|
|
85633
|
+
exports2.isStrongOracleStep = isStrongOracleStep;
|
|
85634
|
+
exports2.listStrongOracleKinds = listStrongOracleKinds;
|
|
85635
|
+
exports2.triageMobileFailure = triageMobileFailure;
|
|
85636
|
+
exports2.triageToHealTerminal = triageToHealTerminal;
|
|
85637
|
+
var anr_env_js_1 = require_anr_env();
|
|
85638
|
+
var STRONG_ACTIONS = /* @__PURE__ */ new Set(["assertStateDelta", "assertText", "captureState"]);
|
|
85639
|
+
var CHROME_VISIBLE_RE = /\b(tab|home|settings|back|menu|navigation|nav bar|bottom nav|status bar)\b/i;
|
|
85640
|
+
function hasStrongMobileOracle(steps) {
|
|
85641
|
+
return steps.some(isStrongOracleStep);
|
|
85642
|
+
}
|
|
85643
|
+
function isStrongOracleStep(step) {
|
|
85644
|
+
if (STRONG_ACTIONS.has(step.action))
|
|
85645
|
+
return true;
|
|
85646
|
+
if (step.action !== "assertVisible")
|
|
85647
|
+
return false;
|
|
85648
|
+
const blob = [
|
|
85649
|
+
step.assertion ?? "",
|
|
85650
|
+
step.description ?? "",
|
|
85651
|
+
step.target?.primary ?? "",
|
|
85652
|
+
...step.target?.fallbacks ?? []
|
|
85653
|
+
].join(" ");
|
|
85654
|
+
if (/\b(error|invalid|required|must|cannot|failed|denied|blocked|empty|missing|deleted|removed|saved|success|created|updated)\b/i.test(blob)) {
|
|
85655
|
+
return true;
|
|
85656
|
+
}
|
|
85657
|
+
if (CHROME_VISIBLE_RE.test(blob) && blob.length < 80)
|
|
85658
|
+
return false;
|
|
85659
|
+
return false;
|
|
85660
|
+
}
|
|
85661
|
+
function listStrongOracleKinds(steps) {
|
|
85662
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
85663
|
+
for (const step of steps) {
|
|
85664
|
+
if (step.action === "assertStateDelta")
|
|
85665
|
+
kinds.add("assertStateDelta");
|
|
85666
|
+
else if (step.action === "assertText")
|
|
85667
|
+
kinds.add("assertText");
|
|
85668
|
+
else if (step.action === "captureState")
|
|
85669
|
+
kinds.add("captureState");
|
|
85670
|
+
else if (isStrongOracleStep(step))
|
|
85671
|
+
kinds.add(step.action);
|
|
85672
|
+
}
|
|
85673
|
+
return [...kinds];
|
|
85674
|
+
}
|
|
85675
|
+
function errorText(error2, logs2) {
|
|
85676
|
+
return `${error2 ?? ""}
|
|
85677
|
+
${logs2 ?? ""}`.trim();
|
|
85678
|
+
}
|
|
85679
|
+
function leadingPrefix(error2) {
|
|
85680
|
+
if (!error2)
|
|
85681
|
+
return null;
|
|
85682
|
+
const m = error2.match(/^(CONFIG_ISSUE|APP_BUG|RATE_LIMIT_BLOCKED|PREREQ_UNMET|HEAL_SECRET_LEAK)\s*:/i);
|
|
85683
|
+
return m ? m[1].toUpperCase() : null;
|
|
85684
|
+
}
|
|
85685
|
+
function looksLikeConfig(text) {
|
|
85686
|
+
if (/CONFIG_ISSUE\s*:/i.test(text))
|
|
85687
|
+
return true;
|
|
85688
|
+
if (/RATE_LIMIT_BLOCKED\s*:/i.test(text))
|
|
85689
|
+
return true;
|
|
85690
|
+
if (/Missing mobile test variable/i.test(text))
|
|
85691
|
+
return true;
|
|
85692
|
+
if (/\b(quota|rate.?limit|429|lockout)\b/i.test(text))
|
|
85693
|
+
return true;
|
|
85694
|
+
if (/\b(device (?:offline|disconnected|not found)|session (?:not started|terminated|deleted))\b/i.test(text)) {
|
|
85695
|
+
return true;
|
|
85696
|
+
}
|
|
85697
|
+
if (/\b(Appium|adbExec|UiAutomator2|XCUITest)\b/i.test(text) && /\b(ECONNREFUSED|timeout|not available|could not start|driver)\b/i.test(text)) {
|
|
85698
|
+
return true;
|
|
85699
|
+
}
|
|
85700
|
+
return false;
|
|
85701
|
+
}
|
|
85702
|
+
function looksLikePrereq(text) {
|
|
85703
|
+
return /PREREQ_UNMET\s*:/i.test(text) || /\b(prerequisite|fixture missing|no test data|account (?:missing|not provisioned)|required entity)\b/i.test(text);
|
|
85704
|
+
}
|
|
85705
|
+
function looksLikeFlake(text) {
|
|
85706
|
+
return /\b(stale element|animation|transition|intermittent|flaky|race condition)\b/i.test(text) || /\b(element (?:is )?not interactable|obscured|covered by)\b/i.test(text);
|
|
85707
|
+
}
|
|
85708
|
+
function looksLikeLocatorMiss(text) {
|
|
85709
|
+
return /not found|no such element|No identity locator|unable to locate/i.test(text);
|
|
85710
|
+
}
|
|
85711
|
+
function looksLikeOracleFailure(text) {
|
|
85712
|
+
return /assertStateDelta/i.test(text) || /expected state/i.test(text) || /expected (?:["'].+["'] )?(?:to be |text|visible|removed|count)/i.test(text) || /assertion failed|Assert .+ (?:visible|shows|contains)/i.test(text);
|
|
85713
|
+
}
|
|
85714
|
+
function triageMobileFailure(input) {
|
|
85715
|
+
const failureCount = Math.max(1, input.failureCount ?? 1);
|
|
85716
|
+
const tags = input.tags ?? [];
|
|
85717
|
+
const text = errorText(input.error, input.logs);
|
|
85718
|
+
const prefix = leadingPrefix(input.error);
|
|
85719
|
+
const strongKinds = listStrongOracleKinds(input.steps);
|
|
85720
|
+
const hasStrong = strongKinds.length > 0 || tags.some((t) => t === "@strong-oracle" || t === "strong-oracle");
|
|
85721
|
+
const evidence = {
|
|
85722
|
+
hasStrongOracle: hasStrong,
|
|
85723
|
+
strongOracleKinds: strongKinds,
|
|
85724
|
+
failureCount,
|
|
85725
|
+
errorPrefix: prefix
|
|
85726
|
+
};
|
|
85727
|
+
if ((0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(input.error, input.logs)) {
|
|
85728
|
+
return {
|
|
85729
|
+
verdict: "config_issue",
|
|
85730
|
+
reason: "ANR / System UI freeze (device or app hung) \u2014 treat as environment, not product bug",
|
|
85731
|
+
shouldHeal: false,
|
|
85732
|
+
healVerdict: "config_issue",
|
|
85733
|
+
evidence
|
|
85734
|
+
};
|
|
85735
|
+
}
|
|
85736
|
+
if (prefix === "CONFIG_ISSUE" || prefix === "RATE_LIMIT_BLOCKED") {
|
|
85737
|
+
return {
|
|
85738
|
+
verdict: "config_issue",
|
|
85739
|
+
reason: `Environment/config prefix on error (${prefix})`,
|
|
85740
|
+
shouldHeal: false,
|
|
85741
|
+
healVerdict: "config_issue",
|
|
85742
|
+
evidence
|
|
85743
|
+
};
|
|
85744
|
+
}
|
|
85745
|
+
if (prefix === "APP_BUG") {
|
|
85746
|
+
return {
|
|
85747
|
+
verdict: "app_bug_candidate",
|
|
85748
|
+
reason: "Error already classified APP_BUG",
|
|
85749
|
+
shouldHeal: false,
|
|
85750
|
+
healVerdict: "app_bug",
|
|
85751
|
+
evidence
|
|
85752
|
+
};
|
|
85753
|
+
}
|
|
85754
|
+
if (prefix === "PREREQ_UNMET") {
|
|
85755
|
+
return {
|
|
85756
|
+
verdict: "prereq_unmet",
|
|
85757
|
+
reason: "Error already classified PREREQ_UNMET",
|
|
85758
|
+
shouldHeal: false,
|
|
85759
|
+
healVerdict: "prereq_unmet",
|
|
85760
|
+
evidence
|
|
85761
|
+
};
|
|
85762
|
+
}
|
|
85763
|
+
if (looksLikeConfig(text)) {
|
|
85764
|
+
return {
|
|
85765
|
+
verdict: "config_issue",
|
|
85766
|
+
reason: "Failure matches environment/config/device patterns",
|
|
85767
|
+
shouldHeal: false,
|
|
85768
|
+
healVerdict: "config_issue",
|
|
85769
|
+
evidence
|
|
85770
|
+
};
|
|
85771
|
+
}
|
|
85772
|
+
if (looksLikePrereq(text)) {
|
|
85773
|
+
return {
|
|
85774
|
+
verdict: "prereq_unmet",
|
|
85775
|
+
reason: "Failure matches missing prerequisite / fixture patterns",
|
|
85776
|
+
shouldHeal: false,
|
|
85777
|
+
healVerdict: "prereq_unmet",
|
|
85778
|
+
evidence
|
|
85779
|
+
};
|
|
85780
|
+
}
|
|
85781
|
+
if (input.uiSuccessWithFailedWriteApi && hasStrong) {
|
|
85782
|
+
return {
|
|
85783
|
+
verdict: "app_bug_candidate",
|
|
85784
|
+
reason: "UI path completed but a write API failed (network secondary oracle)",
|
|
85785
|
+
shouldHeal: false,
|
|
85786
|
+
healVerdict: "app_bug",
|
|
85787
|
+
evidence
|
|
85788
|
+
};
|
|
85789
|
+
}
|
|
85790
|
+
if (hasStrong && failureCount >= 2 && (looksLikeOracleFailure(text) || !looksLikeLocatorMiss(text))) {
|
|
85791
|
+
if (looksLikeLocatorMiss(text) && failureCount < 3) {
|
|
85792
|
+
return {
|
|
85793
|
+
verdict: "needs_heal",
|
|
85794
|
+
reason: "Strong oracle present but error looks like a locator miss \u2014 attempt heal",
|
|
85795
|
+
shouldHeal: true,
|
|
85796
|
+
evidence
|
|
85797
|
+
};
|
|
85798
|
+
}
|
|
85799
|
+
return {
|
|
85800
|
+
verdict: "app_bug_candidate",
|
|
85801
|
+
reason: `Strong oracle (${strongKinds.join(", ") || "tagged"}) failed ${failureCount}\xD7 with product-shaped error`,
|
|
85802
|
+
shouldHeal: false,
|
|
85803
|
+
healVerdict: "app_bug",
|
|
85804
|
+
evidence
|
|
85805
|
+
};
|
|
85806
|
+
}
|
|
85807
|
+
if (looksLikeFlake(text) && failureCount === 1 && !hasStrong) {
|
|
85808
|
+
return {
|
|
85809
|
+
verdict: "flake_candidate",
|
|
85810
|
+
reason: "Transient interactability / race signals on a weak-oracle test",
|
|
85811
|
+
shouldHeal: true,
|
|
85812
|
+
// still allow heal, but tag flaky path
|
|
85813
|
+
extraTags: ["@flaky"],
|
|
85814
|
+
evidence
|
|
85815
|
+
};
|
|
85816
|
+
}
|
|
85817
|
+
if (tags.includes("@smoke") && !hasStrong && looksLikeLocatorMiss(text)) {
|
|
85818
|
+
return {
|
|
85819
|
+
verdict: "needs_heal",
|
|
85820
|
+
reason: "Smoke/nav locator failure \u2014 heal allowed",
|
|
85821
|
+
shouldHeal: true,
|
|
85822
|
+
evidence
|
|
85823
|
+
};
|
|
85824
|
+
}
|
|
85825
|
+
return {
|
|
85826
|
+
verdict: "needs_heal",
|
|
85827
|
+
reason: hasStrong ? "Strong oracle first failure \u2014 allow heal once before APP_BUG escalation" : "Default: attempt script/live heal",
|
|
85828
|
+
shouldHeal: true,
|
|
85829
|
+
evidence
|
|
85830
|
+
};
|
|
85831
|
+
}
|
|
85832
|
+
function triageToHealTerminal(triage) {
|
|
85833
|
+
if (triage.shouldHeal || !triage.healVerdict)
|
|
85834
|
+
return null;
|
|
85835
|
+
return {
|
|
85836
|
+
verdict: triage.healVerdict,
|
|
85837
|
+
healReason: triage.reason,
|
|
85838
|
+
extraTags: triage.extraTags
|
|
85839
|
+
};
|
|
85840
|
+
}
|
|
85841
|
+
}
|
|
85842
|
+
});
|
|
85843
|
+
|
|
85844
|
+
// ../runner-core/dist/services/mobile/oracle-protection.js
|
|
85845
|
+
var require_oracle_protection = __commonJS({
|
|
85846
|
+
"../runner-core/dist/services/mobile/oracle-protection.js"(exports2) {
|
|
85847
|
+
"use strict";
|
|
85848
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
85849
|
+
exports2.fingerprintOracles = fingerprintOracles;
|
|
85850
|
+
exports2.rejectOracleWeakeningPatch = rejectOracleWeakeningPatch;
|
|
85851
|
+
exports2.oraclesPreserved = oraclesPreserved;
|
|
85852
|
+
var failure_triage_js_1 = require_failure_triage();
|
|
85853
|
+
var EXPECT_STRENGTH = {
|
|
85854
|
+
entity_removed: 5,
|
|
85855
|
+
removed: 5,
|
|
85856
|
+
count_decreased: 4,
|
|
85857
|
+
"count-1": 4,
|
|
85858
|
+
count_increased: 4,
|
|
85859
|
+
"count+1": 4,
|
|
85860
|
+
persisted: 3,
|
|
85861
|
+
unchanged: 3,
|
|
85862
|
+
changed: 2,
|
|
85863
|
+
any_change: 2
|
|
85864
|
+
};
|
|
85865
|
+
function normalizeText(value) {
|
|
85866
|
+
return (value ?? "").trim().toLowerCase().replace(/\s+/g, " ");
|
|
85867
|
+
}
|
|
85868
|
+
function stateKeyOf(step) {
|
|
85869
|
+
const meta = step.metadata;
|
|
85870
|
+
const key = typeof meta?.stateKey === "string" ? meta.stateKey.trim() : "";
|
|
85871
|
+
return key || normalizeText(step.target?.primary) || normalizeText(step.description) || `order-${step.order}`;
|
|
85872
|
+
}
|
|
85873
|
+
function expectOf(step) {
|
|
85874
|
+
const meta = step.metadata;
|
|
85875
|
+
const fromMeta = typeof meta?.expect === "string" ? meta.expect : "";
|
|
85876
|
+
return normalizeText(fromMeta || step.assertion || step.value || "changed");
|
|
85877
|
+
}
|
|
85878
|
+
function fingerprintOracles(steps) {
|
|
85879
|
+
const assertTexts = [];
|
|
85880
|
+
const stateExpects = {};
|
|
85881
|
+
let stateDeltas = 0;
|
|
85882
|
+
let captures = 0;
|
|
85883
|
+
let strongVisible = 0;
|
|
85884
|
+
for (const step of steps) {
|
|
85885
|
+
if (step.action === "assertStateDelta") {
|
|
85886
|
+
stateDeltas += 1;
|
|
85887
|
+
stateExpects[stateKeyOf(step)] = expectOf(step);
|
|
85888
|
+
} else if (step.action === "captureState")
|
|
85889
|
+
captures += 1;
|
|
85890
|
+
else if (step.action === "assertText") {
|
|
85891
|
+
const t = normalizeText(step.assertion ?? step.value ?? step.description);
|
|
85892
|
+
if (t)
|
|
85893
|
+
assertTexts.push(t);
|
|
85894
|
+
} else if (step.action === "assertVisible" && (0, failure_triage_js_1.isStrongOracleStep)(step)) {
|
|
85895
|
+
strongVisible += 1;
|
|
85896
|
+
}
|
|
85897
|
+
}
|
|
85898
|
+
return {
|
|
85899
|
+
stateDeltas,
|
|
85900
|
+
captures,
|
|
85901
|
+
assertTexts,
|
|
85902
|
+
stateExpects,
|
|
85903
|
+
strongVisible,
|
|
85904
|
+
strongTotal: stateDeltas + captures + assertTexts.length + strongVisible
|
|
85905
|
+
};
|
|
85906
|
+
}
|
|
85907
|
+
function expectStrength(expect) {
|
|
85908
|
+
const e = expect.toLowerCase().trim();
|
|
85909
|
+
if (e.startsWith("removed:"))
|
|
85910
|
+
return 5;
|
|
85911
|
+
return EXPECT_STRENGTH[e] ?? 1;
|
|
85912
|
+
}
|
|
85913
|
+
function rejectOracleWeakeningPatch(original, patched) {
|
|
85914
|
+
if (!Array.isArray(patched) || patched.length === 0) {
|
|
85915
|
+
return "healed steps missing \u2014 refusing empty patch that would drop oracles";
|
|
85916
|
+
}
|
|
85917
|
+
const before = fingerprintOracles(original);
|
|
85918
|
+
const after = fingerprintOracles(patched);
|
|
85919
|
+
if (before.stateDeltas > 0 && after.stateDeltas < before.stateDeltas) {
|
|
85920
|
+
return `heal patch removed assertStateDelta oracle(s) (${before.stateDeltas} \u2192 ${after.stateDeltas}) \u2014 refusing to go green by dropping state proof`;
|
|
85921
|
+
}
|
|
85922
|
+
for (const [key, beforeExpect] of Object.entries(before.stateExpects)) {
|
|
85923
|
+
const afterExpect = after.stateExpects[key];
|
|
85924
|
+
if (!afterExpect) {
|
|
85925
|
+
return `heal patch removed assertStateDelta for key "${key}" (expect was ${beforeExpect})`;
|
|
85926
|
+
}
|
|
85927
|
+
if (expectStrength(afterExpect) < expectStrength(beforeExpect)) {
|
|
85928
|
+
return `heal patch softened assertStateDelta expect for "${key}" (${beforeExpect} \u2192 ${afterExpect})`;
|
|
85929
|
+
}
|
|
85930
|
+
}
|
|
85931
|
+
if (before.captures > 0 && after.captures < before.captures && after.stateDeltas === 0) {
|
|
85932
|
+
return `heal patch removed captureState without keeping assertStateDelta (${before.captures} \u2192 ${after.captures})`;
|
|
85933
|
+
}
|
|
85934
|
+
if (before.assertTexts.length > 0) {
|
|
85935
|
+
const afterSet = new Set(after.assertTexts);
|
|
85936
|
+
const lost = before.assertTexts.filter((t) => !afterSet.has(t));
|
|
85937
|
+
if (after.assertTexts.length < before.assertTexts.length) {
|
|
85938
|
+
return `heal patch dropped assertText outcome proof (${before.assertTexts.length} \u2192 ${after.assertTexts.length})`;
|
|
85939
|
+
}
|
|
85940
|
+
if (lost.length > 0) {
|
|
85941
|
+
const softKept = lost.every((t) => after.assertTexts.some((a) => a.includes(t) || t.includes(a)));
|
|
85942
|
+
if (!softKept) {
|
|
85943
|
+
return `heal patch replaced assertText expectations (${lost.slice(0, 2).join("; ")}) \u2014 outcome must stay`;
|
|
85944
|
+
}
|
|
85945
|
+
}
|
|
85946
|
+
}
|
|
85947
|
+
if (before.strongVisible > 0 && after.strongVisible + after.assertTexts.length + after.stateDeltas < before.strongVisible) {
|
|
85948
|
+
return `heal patch weakened strong assertVisible validation oracles (${before.strongVisible} \u2192 ${after.strongVisible})`;
|
|
85949
|
+
}
|
|
85950
|
+
if (before.strongTotal > 0 && after.strongTotal === 0) {
|
|
85951
|
+
return "heal patch removed all strong oracles \u2014 refusing visibility-only green path";
|
|
85952
|
+
}
|
|
85953
|
+
for (const orig of original) {
|
|
85954
|
+
if (orig.action !== "assertStateDelta" && orig.action !== "assertText")
|
|
85955
|
+
continue;
|
|
85956
|
+
const key = normalizeText(orig.description ?? orig.assertion ?? "");
|
|
85957
|
+
if (!key)
|
|
85958
|
+
continue;
|
|
85959
|
+
const stillStrong = patched.some((p) => (p.action === "assertStateDelta" || p.action === "assertText") && normalizeText(p.description ?? p.assertion ?? "") === key);
|
|
85960
|
+
if (stillStrong)
|
|
85961
|
+
continue;
|
|
85962
|
+
const onlyVisible = patched.some((p) => p.action === "assertVisible" && normalizeText(p.description ?? p.assertion ?? "") === key);
|
|
85963
|
+
if (onlyVisible) {
|
|
85964
|
+
return `heal patch downgraded strong oracle "${key.slice(0, 60)}" to assertVisible`;
|
|
85965
|
+
}
|
|
85966
|
+
}
|
|
85967
|
+
return null;
|
|
85968
|
+
}
|
|
85969
|
+
function oraclesPreserved(original, patched) {
|
|
85970
|
+
return rejectOracleWeakeningPatch(original, patched) === null;
|
|
85971
|
+
}
|
|
85972
|
+
}
|
|
85973
|
+
});
|
|
85974
|
+
|
|
84833
85975
|
// ../runner-core/dist/services/mobile/mobile-heal-postprocess.js
|
|
84834
85976
|
var require_mobile_heal_postprocess = __commonJS({
|
|
84835
85977
|
"../runner-core/dist/services/mobile/mobile-heal-postprocess.js"(exports2) {
|
|
@@ -84838,6 +85980,8 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
84838
85980
|
exports2.redactMobileSecretsFromText = redactMobileSecretsFromText;
|
|
84839
85981
|
exports2.detectMobileStepSecretLeak = detectMobileStepSecretLeak;
|
|
84840
85982
|
exports2.computeMobileHealOutcome = computeMobileHealOutcome2;
|
|
85983
|
+
var oracle_protection_js_1 = require_oracle_protection();
|
|
85984
|
+
var anr_env_js_1 = require_anr_env();
|
|
84841
85985
|
var REDACT_KEY_RE = /email|username|user|phone|password|secret|token|api[_-]?key|access[_-]?key|\bpass\b|credential|bearer|\bpat\b|session|cookie|jwt|otp|pin/i;
|
|
84842
85986
|
var DETECT_KEY_RE = /password|secret|token|api[_]?key|\bpass\b|credential|bearer|\bpat\b/i;
|
|
84843
85987
|
var MIN_REDACT_LEN = 4;
|
|
@@ -84903,18 +86047,32 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
84903
86047
|
function computeMobileHealOutcome2(args) {
|
|
84904
86048
|
const { heal } = args;
|
|
84905
86049
|
const hasFix = Array.isArray(heal.healedSteps) && heal.healedSteps.length > 0;
|
|
84906
|
-
|
|
86050
|
+
let isVerifiedFix = heal.passed === true && heal.verdict === "fix" && hasFix;
|
|
84907
86051
|
const secretLeak = isVerifiedFix ? detectMobileStepSecretLeak(heal.healedSteps, args.testVariables) : null;
|
|
84908
|
-
const
|
|
84909
|
-
|
|
86052
|
+
const oracleWeakening = isVerifiedFix && args.originalSteps ? (0, oracle_protection_js_1.rejectOracleWeakeningPatch)(args.originalSteps, heal.healedSteps ?? []) : null;
|
|
86053
|
+
if (oracleWeakening)
|
|
86054
|
+
isVerifiedFix = false;
|
|
86055
|
+
const anrEnv = (0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(heal.healReason ?? args.finalError ?? args.reproduceError, typeof args.finalError === "string" ? args.finalError : void 0);
|
|
86056
|
+
const isConfigIssue = heal.verdict === "config_issue" || heal.verdict === "app_bug" && anrEnv;
|
|
86057
|
+
const isAppBug = !anrEnv && (heal.verdict === "app_bug" || Boolean(oracleWeakening && heal.passed));
|
|
84910
86058
|
const extraTags = new Set((heal.extraTags ?? []).filter((t) => typeof t === "string" && t.length > 0));
|
|
84911
86059
|
if (secretLeak)
|
|
84912
86060
|
extraTags.add("@needs-review");
|
|
86061
|
+
if (oracleWeakening && !anrEnv) {
|
|
86062
|
+
extraTags.add("@needs-review");
|
|
86063
|
+
extraTags.add("@bug-found");
|
|
86064
|
+
}
|
|
86065
|
+
if (anrEnv) {
|
|
86066
|
+
extraTags.delete("@bug-found");
|
|
86067
|
+
extraTags.add("@needs-review");
|
|
86068
|
+
}
|
|
84913
86069
|
if (!heal.passed && NEEDS_REVIEW_VERDICTS.has(heal.verdict))
|
|
84914
86070
|
extraTags.add("@needs-review");
|
|
84915
86071
|
let status;
|
|
84916
86072
|
if (secretLeak)
|
|
84917
86073
|
status = "FAILED";
|
|
86074
|
+
else if (oracleWeakening && !anrEnv)
|
|
86075
|
+
status = "FAILED";
|
|
84918
86076
|
else if (isVerifiedFix)
|
|
84919
86077
|
status = "PASSED";
|
|
84920
86078
|
else if (isConfigIssue)
|
|
@@ -84926,8 +86084,11 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
84926
86084
|
error2 = void 0;
|
|
84927
86085
|
} else if (secretLeak) {
|
|
84928
86086
|
error2 = `HEAL_SECRET_LEAK: healed steps contained a literal secret (${secretLeak}); patch dropped, tagged @needs-review`;
|
|
86087
|
+
} else if (oracleWeakening && !anrEnv) {
|
|
86088
|
+
error2 = `APP_BUG: heal attempted to drop strong oracle (${oracleWeakening}); patch refused`;
|
|
84929
86089
|
} else if (isConfigIssue) {
|
|
84930
|
-
|
|
86090
|
+
const reason = heal.healReason ?? args.finalError ?? args.reproduceError ?? "environment/rate-limit blocker";
|
|
86091
|
+
error2 = anrEnv ? `CONFIG_ISSUE: ANR / System UI freeze (not a product assertion failure) \u2014 ${reason}` : `CONFIG_ISSUE: ${reason}`;
|
|
84931
86092
|
} else if (isAppBug) {
|
|
84932
86093
|
error2 = `APP_BUG: ${heal.healReason ?? args.finalError ?? args.reproduceError ?? "application defect found during heal"}`;
|
|
84933
86094
|
} else if (heal.passed && heal.verdict === "fix" && !hasFix) {
|
|
@@ -84942,13 +86103,64 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
84942
86103
|
error: error2,
|
|
84943
86104
|
extraTags: extraTags.size > 0 ? [...extraTags] : void 0,
|
|
84944
86105
|
removeTags: heal.removeTags,
|
|
84945
|
-
shouldPatch: isVerifiedFix && !secretLeak,
|
|
86106
|
+
shouldPatch: isVerifiedFix && !secretLeak && !oracleWeakening,
|
|
84946
86107
|
secretLeak
|
|
84947
86108
|
};
|
|
84948
86109
|
}
|
|
84949
86110
|
}
|
|
84950
86111
|
});
|
|
84951
86112
|
|
|
86113
|
+
// ../runner-core/dist/services/mobile/network-oracle.js
|
|
86114
|
+
var require_network_oracle = __commonJS({
|
|
86115
|
+
"../runner-core/dist/services/mobile/network-oracle.js"(exports2) {
|
|
86116
|
+
"use strict";
|
|
86117
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
86118
|
+
exports2.evaluateNetworkSecondaryOracle = evaluateNetworkSecondaryOracle;
|
|
86119
|
+
exports2.uiSuccessWithFailedWriteApi = uiSuccessWithFailedWriteApi;
|
|
86120
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
86121
|
+
function isWrite(method) {
|
|
86122
|
+
return WRITE_METHODS.has(method.toUpperCase());
|
|
86123
|
+
}
|
|
86124
|
+
function isServerOrClientError(status) {
|
|
86125
|
+
return status >= 400 && status < 600;
|
|
86126
|
+
}
|
|
86127
|
+
function isNoiseUrl(url) {
|
|
86128
|
+
return /\b(analytics|telemetry|sentry|bugsnag|crashlytics|facebook\.com\/tr|google-analytics|doubleclick|hotjar|segment\.io|amplitude|mixpanel)\b/i.test(url);
|
|
86129
|
+
}
|
|
86130
|
+
function evaluateNetworkSecondaryOracle(input) {
|
|
86131
|
+
const calls = input.apiCalls;
|
|
86132
|
+
if (!calls || calls.length === 0)
|
|
86133
|
+
return null;
|
|
86134
|
+
if (input.uiPathSucceeded === false)
|
|
86135
|
+
return null;
|
|
86136
|
+
for (const call of calls) {
|
|
86137
|
+
const method = String(call.method ?? "GET");
|
|
86138
|
+
const url = String(call.url ?? "");
|
|
86139
|
+
const status = typeof call.status === "number" ? call.status : 0;
|
|
86140
|
+
if (!isWrite(method))
|
|
86141
|
+
continue;
|
|
86142
|
+
if (!isServerOrClientError(status))
|
|
86143
|
+
continue;
|
|
86144
|
+
if (!url || isNoiseUrl(url))
|
|
86145
|
+
continue;
|
|
86146
|
+
if (status === 401 || status === 403)
|
|
86147
|
+
continue;
|
|
86148
|
+
return {
|
|
86149
|
+
kind: "write_api_failed",
|
|
86150
|
+
method: method.toUpperCase(),
|
|
86151
|
+
url: url.slice(0, 200),
|
|
86152
|
+
status,
|
|
86153
|
+
reason: `Write API ${method.toUpperCase()} ${url.slice(0, 120)} returned ${status} while UI path appeared successful`
|
|
86154
|
+
};
|
|
86155
|
+
}
|
|
86156
|
+
return null;
|
|
86157
|
+
}
|
|
86158
|
+
function uiSuccessWithFailedWriteApi(input) {
|
|
86159
|
+
return evaluateNetworkSecondaryOracle(input) !== null;
|
|
86160
|
+
}
|
|
86161
|
+
}
|
|
86162
|
+
});
|
|
86163
|
+
|
|
84952
86164
|
// ../runner-core/dist/services/selector-registry-context.js
|
|
84953
86165
|
var require_selector_registry_context = __commonJS({
|
|
84954
86166
|
"../runner-core/dist/services/selector-registry-context.js"(exports2) {
|
|
@@ -85316,6 +86528,10 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
85316
86528
|
var ai_retry_js_1 = require_ai_retry();
|
|
85317
86529
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
85318
86530
|
var mobile_heal_postprocess_js_1 = require_mobile_heal_postprocess();
|
|
86531
|
+
var failure_triage_js_1 = require_failure_triage();
|
|
86532
|
+
var oracle_protection_js_1 = require_oracle_protection();
|
|
86533
|
+
var network_oracle_js_1 = require_network_oracle();
|
|
86534
|
+
var anr_env_js_1 = require_anr_env();
|
|
85319
86535
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
85320
86536
|
var selector_registry_context_js_1 = require_selector_registry_context();
|
|
85321
86537
|
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
@@ -85327,18 +86543,24 @@ Reason over steps + error + tree + registry and return working healed steps. Do
|
|
|
85327
86543
|
2. STEP RESULTS: which steps passed before the failure.
|
|
85328
86544
|
3. TREE: does the current screen match what the failed step expects?
|
|
85329
86545
|
3b. STALE EMPTY-STATE ASSERT: a failing assert on one-time / data-dependent UI (empty-state CTAs, first-run prompts, onboarding banners) is NOT a locator problem \u2014 the element is gone because the account now has data. REPLACE the assertion with a durable landing-screen anchor (tab label, header, nav bar) that proves the same outcome (verdict fix).
|
|
86546
|
+
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.
|
|
85330
86547
|
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.
|
|
85331
86548
|
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.
|
|
85332
86549
|
## Response \u2014 STRICT JSON, exactly one verdict:
|
|
85333
86550
|
{"verdict":"fix","steps":[<FULL healed MobileTestStep array, all steps, your changes applied, order/metadata preserved>],"reason":"what changed and why","confidence":0.0-1.0}
|
|
85334
|
-
{"verdict":"app_bug","reason":"
|
|
85335
|
-
{"verdict":"config_issue","reason":"429/rate limit/lockout/quota"}
|
|
86551
|
+
{"verdict":"app_bug","reason":"product defect with durable oracle evidence (wrong entity, validation bypass, wrong count) \u2014 NOT ANR/System UI"}
|
|
86552
|
+
{"verdict":"config_issue","reason":"429/rate limit/lockout/quota OR ANR/System UI freeze/device unresponsive"}
|
|
85336
86553
|
{"verdict":"needs_device","reason":"cannot fix from a static tree; need live re-observation"}
|
|
85337
86554
|
{"verdict":"prereq_unmet","reason":"required entity/state absent for the account"}
|
|
85338
86555
|
{"verdict":"state_stale","reason":"leftover data blocks a baseline assertion"}
|
|
85339
86556
|
{"verdict":"test_invalid","reason":"scenario cannot be made to pass"}
|
|
85340
86557
|
{"verdict":"unhealable_dynamic","reason":"asserted value is inherently volatile (timestamps, counters, feeds) \u2014 no durable locator can exist; needs human review"}
|
|
85341
|
-
For a fix, return the COMPLETE steps array (not a diff)
|
|
86558
|
+
For a fix, return the COMPLETE steps array (not a diff).
|
|
86559
|
+
## Strong oracle protection (non-negotiable)
|
|
86560
|
+
- NEVER remove or downgrade assertStateDelta, captureState, or assertText outcome proof to go green.
|
|
86561
|
+
- NEVER replace a durable outcome assert with mere assertVisible of a tab/header.
|
|
86562
|
+
- You MAY fix locators, waits, and navigation around those oracles.
|
|
86563
|
+
- If the app truly violates the oracle on a healthy UI, return verdict "app_bug" instead of stripping the assert.`;
|
|
85342
86564
|
var DEFAULT_SCRIPT_MAX_ATTEMPTS = 6;
|
|
85343
86565
|
var RECOVERABLE_MAX = 3;
|
|
85344
86566
|
var MAX_CONSECUTIVE_INVALID_VERDICTS = 3;
|
|
@@ -85368,7 +86590,9 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
85368
86590
|
"home",
|
|
85369
86591
|
"waitForElement",
|
|
85370
86592
|
"assertVisible",
|
|
85371
|
-
"assertText"
|
|
86593
|
+
"assertText",
|
|
86594
|
+
"captureState",
|
|
86595
|
+
"assertStateDelta"
|
|
85372
86596
|
]);
|
|
85373
86597
|
var MAX_HEALED_STEPS = 300;
|
|
85374
86598
|
var TERMINAL_VERDICTS = /* @__PURE__ */ new Set([
|
|
@@ -85395,6 +86619,28 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
85395
86619
|
function isMobileHealVerdict(value) {
|
|
85396
86620
|
return MOBILE_HEAL_VERDICTS.includes(value);
|
|
85397
86621
|
}
|
|
86622
|
+
function normalizeMobileHealVerdict(raw) {
|
|
86623
|
+
const v = raw.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
86624
|
+
if (isMobileHealVerdict(v))
|
|
86625
|
+
return v;
|
|
86626
|
+
if (v === "appbug" || v === "application_bug" || v === "bug_found")
|
|
86627
|
+
return "app_bug";
|
|
86628
|
+
if (v === "config" || v === "configuration_issue" || v === "env" || v === "environment")
|
|
86629
|
+
return "config_issue";
|
|
86630
|
+
if (v === "prereq" || v === "prerequisite" || v === "prereq_missing")
|
|
86631
|
+
return "prereq_unmet";
|
|
86632
|
+
if (v === "stale" || v === "stale_state")
|
|
86633
|
+
return "state_stale";
|
|
86634
|
+
if (v === "invalid_test" || v === "bad_test")
|
|
86635
|
+
return "test_invalid";
|
|
86636
|
+
if (v === "unhealable" || v === "dynamic")
|
|
86637
|
+
return "unhealable_dynamic";
|
|
86638
|
+
if (v === "need_device" || v === "needs_appium" || v === "needs_live")
|
|
86639
|
+
return "needs_device";
|
|
86640
|
+
if (v === "healed" || v === "ok" || v === "pass" || v === "passed")
|
|
86641
|
+
return "fix";
|
|
86642
|
+
return null;
|
|
86643
|
+
}
|
|
85398
86644
|
function isLocatorNotFound(error2) {
|
|
85399
86645
|
return /not found|no such element/i.test(error2 ?? "");
|
|
85400
86646
|
}
|
|
@@ -85676,15 +86922,43 @@ For a fix, return the COMPLETE steps array (not a diff).`;
|
|
|
85676
86922
|
const sub = extractBracesSubstring(cleaned);
|
|
85677
86923
|
obj = sub ? safeJsonParse(sub) : void 0;
|
|
85678
86924
|
}
|
|
85679
|
-
if (!isRecord(obj))
|
|
86925
|
+
if (!isRecord(obj)) {
|
|
86926
|
+
const line = cleaned.match(/\bverdict\s*[:=]\s*["']?([a-zA-Z_ -]+)["']?/i);
|
|
86927
|
+
if (line) {
|
|
86928
|
+
const verdict2 = normalizeMobileHealVerdict(line[1]);
|
|
86929
|
+
if (verdict2) {
|
|
86930
|
+
return {
|
|
86931
|
+
verdict: verdict2,
|
|
86932
|
+
reason: cleaned.slice(0, 2e3)
|
|
86933
|
+
};
|
|
86934
|
+
}
|
|
86935
|
+
}
|
|
86936
|
+
const prefix = cleaned.match(/^\s*(APP_BUG|CONFIG_ISSUE|PREREQ_UNMET|TEST_INVALID|STATE_STALE)\s*:\s*(.*)$/im);
|
|
86937
|
+
if (prefix) {
|
|
86938
|
+
const map2 = {
|
|
86939
|
+
APP_BUG: "app_bug",
|
|
86940
|
+
CONFIG_ISSUE: "config_issue",
|
|
86941
|
+
PREREQ_UNMET: "prereq_unmet",
|
|
86942
|
+
TEST_INVALID: "test_invalid",
|
|
86943
|
+
STATE_STALE: "state_stale"
|
|
86944
|
+
};
|
|
86945
|
+
const verdict2 = map2[prefix[1].toUpperCase()];
|
|
86946
|
+
if (verdict2) {
|
|
86947
|
+
return { verdict: verdict2, reason: (prefix[2] || cleaned).slice(0, 2e3) };
|
|
86948
|
+
}
|
|
86949
|
+
}
|
|
85680
86950
|
return null;
|
|
85681
|
-
|
|
85682
|
-
|
|
86951
|
+
}
|
|
86952
|
+
const rawVerdict = obj.verdict ?? obj.Verdict ?? obj.status;
|
|
86953
|
+
if (typeof rawVerdict !== "string")
|
|
86954
|
+
return null;
|
|
86955
|
+
const verdict = normalizeMobileHealVerdict(rawVerdict);
|
|
86956
|
+
if (!verdict)
|
|
85683
86957
|
return null;
|
|
85684
86958
|
return {
|
|
85685
|
-
verdict
|
|
86959
|
+
verdict,
|
|
85686
86960
|
steps: coerceSteps(obj.steps),
|
|
85687
|
-
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
86961
|
+
reason: typeof obj.reason === "string" ? obj.reason : typeof obj.rationale === "string" ? obj.rationale : typeof obj.message === "string" ? obj.message : void 0,
|
|
85688
86962
|
confidence: typeof obj.confidence === "number" ? obj.confidence : void 0
|
|
85689
86963
|
};
|
|
85690
86964
|
}
|
|
@@ -86033,7 +87307,7 @@ ${args.directive}`);
|
|
|
86033
87307
|
};
|
|
86034
87308
|
}
|
|
86035
87309
|
markRecoverableTurn();
|
|
86036
|
-
pendingDirective =
|
|
87310
|
+
pendingDirective = 'SCHEMA REPAIR: Your last response was not a valid heal verdict. Reply with ONLY one JSON object (no markdown fences, no prose) of the form:\n{"verdict":"<one of: fix|app_bug|config_issue|needs_device|prereq_unmet|state_stale|test_invalid|unhealable_dynamic>","reason":"<short string>","steps":[/* optional MobileTestStep[] when verdict is fix */],"confidence":0.0}\nUse lowercase snake_case for verdict. Do not invent other verdict values.';
|
|
86037
87311
|
continue;
|
|
86038
87312
|
}
|
|
86039
87313
|
consecutiveInvalidVerdicts = 0;
|
|
@@ -86062,6 +87336,13 @@ ${args.directive}`);
|
|
|
86062
87336
|
pendingDirective = 'You returned verdict "fix" but no usable steps array. Return the COMPLETE healed MobileTestStep array.';
|
|
86063
87337
|
continue;
|
|
86064
87338
|
}
|
|
87339
|
+
const oracleRejection = (0, oracle_protection_js_1.rejectOracleWeakeningPatch)(steps, healedSteps);
|
|
87340
|
+
if (oracleRejection) {
|
|
87341
|
+
markRecoverableTurn();
|
|
87342
|
+
lastVerdict = "fix";
|
|
87343
|
+
pendingDirective = `ORACLE PROTECTION: ${oracleRejection}. Keep all assertStateDelta/assertText/captureState outcome proof; fix locators/navigation only. If the app violates the oracle, return verdict "app_bug".`;
|
|
87344
|
+
continue;
|
|
87345
|
+
}
|
|
86065
87346
|
if (activeFailingSource && isLocatorNotFound(activeError) && !isDrift(activeError)) {
|
|
86066
87347
|
const failingSourceSnapshot = activeFailingSource;
|
|
86067
87348
|
const missing = timedSync("device-op", `phase1-attempt-${attempt}-static-verify`, options.onProgress, () => {
|
|
@@ -86205,6 +87486,28 @@ ${args.directive}`);
|
|
|
86205
87486
|
if (invalidAuthSetup) {
|
|
86206
87487
|
return { passed: false, verdict: "test_invalid", healReason: invalidAuthSetup };
|
|
86207
87488
|
}
|
|
87489
|
+
const networkBug = (0, network_oracle_js_1.uiSuccessWithFailedWriteApi)({
|
|
87490
|
+
apiCalls: ctx.apiCalls,
|
|
87491
|
+
uiPathSucceeded: ctx.uiPathSucceeded
|
|
87492
|
+
});
|
|
87493
|
+
const triage = (0, failure_triage_js_1.triageMobileFailure)({
|
|
87494
|
+
steps,
|
|
87495
|
+
error: ctx.error,
|
|
87496
|
+
logs: ctx.logs,
|
|
87497
|
+
tags: ctx.tags,
|
|
87498
|
+
failureCount: ctx.failureCount,
|
|
87499
|
+
uiSuccessWithFailedWriteApi: networkBug
|
|
87500
|
+
});
|
|
87501
|
+
options.onProgress?.(`Pre-heal triage: ${triage.verdict} \u2014 ${triage.reason}`);
|
|
87502
|
+
const terminal = (0, failure_triage_js_1.triageToHealTerminal)(triage);
|
|
87503
|
+
if (terminal) {
|
|
87504
|
+
return {
|
|
87505
|
+
passed: false,
|
|
87506
|
+
verdict: terminal.verdict,
|
|
87507
|
+
healReason: terminal.healReason,
|
|
87508
|
+
extraTags: terminal.extraTags
|
|
87509
|
+
};
|
|
87510
|
+
}
|
|
86208
87511
|
const preflight = await tryRegistryPreflight(steps, target, ctx, options);
|
|
86209
87512
|
if (preflight)
|
|
86210
87513
|
return preflight;
|
|
@@ -86225,6 +87528,18 @@ ${args.directive}`);
|
|
|
86225
87528
|
providerUnavailable: true
|
|
86226
87529
|
};
|
|
86227
87530
|
}
|
|
87531
|
+
if (script.verdict === "app_bug" && (0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(script.healReason ?? ctx.error, ctx.logs)) {
|
|
87532
|
+
return {
|
|
87533
|
+
passed: false,
|
|
87534
|
+
verdict: "config_issue",
|
|
87535
|
+
healedSteps: script.healedSteps,
|
|
87536
|
+
healedAppiumCode: script.healedAppiumCode,
|
|
87537
|
+
healReason: `ANR / System UI freeze (environment) \u2014 ${script.healReason ?? ctx.error ?? "device unresponsive"}`,
|
|
87538
|
+
confidence: script.confidence,
|
|
87539
|
+
extraTags: script.extraTags,
|
|
87540
|
+
removeTags: [...script.removeTags ?? [], "@bug-found"].filter((t, i, a) => a.indexOf(t) === i)
|
|
87541
|
+
};
|
|
87542
|
+
}
|
|
86228
87543
|
const shouldEscalate = script.verdict === "needs_device" || script.verdict === "fix" && !script.passed;
|
|
86229
87544
|
if (shouldEscalate && options.runLiveHeal) {
|
|
86230
87545
|
try {
|
|
@@ -86454,6 +87769,11 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
86454
87769
|
if (parsed.verdict !== "fix" || !parsed.steps)
|
|
86455
87770
|
continue;
|
|
86456
87771
|
const healedSteps = parsed.steps;
|
|
87772
|
+
const liveOracleRejection = (0, oracle_protection_js_1.rejectOracleWeakeningPatch)(args.steps, healedSteps);
|
|
87773
|
+
if (liveOracleRejection) {
|
|
87774
|
+
options.onProgress?.(`ORACLE PROTECTION (live): ${liveOracleRejection}`);
|
|
87775
|
+
continue;
|
|
87776
|
+
}
|
|
86457
87777
|
let verify;
|
|
86458
87778
|
try {
|
|
86459
87779
|
verify = await timed("reexec", `phase2-pass-${pass}-verify`, options.onProgress, () => runNativeTest({ steps: healedSteps, target, testVariables }));
|
|
@@ -353976,7 +355296,7 @@ var require_package4 = __commonJS({
|
|
|
353976
355296
|
"package.json"(exports2, module2) {
|
|
353977
355297
|
module2.exports = {
|
|
353978
355298
|
name: "@validate.qa/runner",
|
|
353979
|
-
version: "1.0.
|
|
355299
|
+
version: "1.0.90",
|
|
353980
355300
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353981
355301
|
bin: {
|
|
353982
355302
|
"validate-runner": "dist/cli.js"
|
|
@@ -354837,6 +356157,68 @@ async function waitForUiSettle(sessionPath, opts = {}) {
|
|
|
354837
356157
|
}
|
|
354838
356158
|
return { settled: false, lastFingerprint, frames };
|
|
354839
356159
|
}
|
|
356160
|
+
var SYSTEM_UI_DISMISS_LABELS = [
|
|
356161
|
+
"OK",
|
|
356162
|
+
"Got it",
|
|
356163
|
+
"GOT IT",
|
|
356164
|
+
"Continue",
|
|
356165
|
+
"Close",
|
|
356166
|
+
"Dismiss",
|
|
356167
|
+
"I understand",
|
|
356168
|
+
"Accept",
|
|
356169
|
+
// Android 15+ / 16 KB page-size compatibility dialog primary action.
|
|
356170
|
+
"OK, got it"
|
|
356171
|
+
];
|
|
356172
|
+
function extractElementId(value) {
|
|
356173
|
+
if (!value || typeof value !== "object") return null;
|
|
356174
|
+
const v = value;
|
|
356175
|
+
const elementId = v.ELEMENT ?? v["element-6066-11e4-a52e-4f735466cecf"] ?? null;
|
|
356176
|
+
if (typeof elementId !== "string" || !ELEMENT_ID_PATTERN.test(elementId)) return null;
|
|
356177
|
+
return elementId;
|
|
356178
|
+
}
|
|
356179
|
+
async function dismissBlockingSystemUi(sessionPath, platform3, opts = {}) {
|
|
356180
|
+
const maxDismisses = Math.max(1, Math.min(5, opts.maxDismisses ?? 3));
|
|
356181
|
+
const dismissed = [];
|
|
356182
|
+
for (let i = 0; i < maxDismisses; i++) {
|
|
356183
|
+
let tapped = false;
|
|
356184
|
+
for (const label of SYSTEM_UI_DISMISS_LABELS) {
|
|
356185
|
+
try {
|
|
356186
|
+
const strategies = platform3 === "ANDROID" ? [
|
|
356187
|
+
{ using: "-android uiautomator", value: `new UiSelector().text("${label.replace(/"/g, '\\"')}")` },
|
|
356188
|
+
{ using: "accessibility id", value: label }
|
|
356189
|
+
] : [
|
|
356190
|
+
{ using: "accessibility id", value: label },
|
|
356191
|
+
{ using: "-ios predicate string", value: `label == "${label.replace(/"/g, '\\"')}" OR name == "${label.replace(/"/g, '\\"')}"` }
|
|
356192
|
+
];
|
|
356193
|
+
for (const strategy of strategies) {
|
|
356194
|
+
try {
|
|
356195
|
+
const elResult = await appiumRequest(`${sessionPath}/element`, {
|
|
356196
|
+
method: "POST",
|
|
356197
|
+
body: JSON.stringify(strategy)
|
|
356198
|
+
});
|
|
356199
|
+
const elementId = extractElementId(readValue(elResult));
|
|
356200
|
+
if (!elementId) continue;
|
|
356201
|
+
await appiumRequest(`${sessionPath}/element/${elementId}/click`, {
|
|
356202
|
+
method: "POST",
|
|
356203
|
+
body: "{}"
|
|
356204
|
+
});
|
|
356205
|
+
dismissed.push(label);
|
|
356206
|
+
tapped = true;
|
|
356207
|
+
await waitForUiSettle(sessionPath, { timeoutMs: 1200, pollMs: 200, minStableFrames: 1 });
|
|
356208
|
+
break;
|
|
356209
|
+
} catch (err) {
|
|
356210
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
356211
|
+
}
|
|
356212
|
+
}
|
|
356213
|
+
if (tapped) break;
|
|
356214
|
+
} catch (err) {
|
|
356215
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
356216
|
+
}
|
|
356217
|
+
}
|
|
356218
|
+
if (!tapped) break;
|
|
356219
|
+
}
|
|
356220
|
+
return { dismissed, ok: dismissed.length > 0 };
|
|
356221
|
+
}
|
|
354840
356222
|
async function waitForColdBootSettle(sessionPath) {
|
|
354841
356223
|
await waitForUiSettle(sessionPath, {
|
|
354842
356224
|
timeoutMs: MOBILE_COLD_BOOT_SETTLE_MS,
|
|
@@ -357574,6 +358956,107 @@ async function waitForColdBootContent(sessionId, log2, timeoutMs = 2e4) {
|
|
|
357574
358956
|
}
|
|
357575
358957
|
log2(` Cold-boot wait timed out after ${timeoutMs}ms (last tree had ${lastCount} node(s)) \u2014 proceeding anyway.`);
|
|
357576
358958
|
}
|
|
358959
|
+
async function dismissSystemUiAfterColdBoot(sessionId, platform3, log2) {
|
|
358960
|
+
try {
|
|
358961
|
+
const result = await dismissBlockingSystemUi(`/session/${sessionId}`, platform3);
|
|
358962
|
+
if (result.dismissed.length > 0) {
|
|
358963
|
+
log2(` Dismissed system UI: ${result.dismissed.join(" \u2192 ")}`);
|
|
358964
|
+
}
|
|
358965
|
+
} catch (err) {
|
|
358966
|
+
log2(` System UI dismiss skipped (${err instanceof Error ? err.message : String(err)})`);
|
|
358967
|
+
}
|
|
358968
|
+
}
|
|
358969
|
+
async function captureMobileStateValues(sessionId, kind) {
|
|
358970
|
+
const res = await wdRequest(`/session/${sessionId}/source`);
|
|
358971
|
+
const xml = typeof res?.value === "string" ? res.value : "";
|
|
358972
|
+
if (kind === "badge_count") {
|
|
358973
|
+
const nums = /* @__PURE__ */ new Set();
|
|
358974
|
+
for (const m of xml.matchAll(/\b(\d{1,6})\b/g)) {
|
|
358975
|
+
nums.add(m[1]);
|
|
358976
|
+
}
|
|
358977
|
+
const preferred = /* @__PURE__ */ new Set();
|
|
358978
|
+
for (const m of xml.matchAll(/(?:count|badge|open|unread|item|invoice|result|pending)[^>]{0,80}?\b(\d{1,6})\b/gi)) {
|
|
358979
|
+
preferred.add(m[1]);
|
|
358980
|
+
}
|
|
358981
|
+
for (const m of xml.matchAll(/\b(\d{1,6})\b[^>]{0,80}?(?:count|badge|open|unread|items?)/gi)) {
|
|
358982
|
+
preferred.add(m[1]);
|
|
358983
|
+
}
|
|
358984
|
+
return preferred.size > 0 ? [...preferred] : [...nums].slice(0, 20);
|
|
358985
|
+
}
|
|
358986
|
+
const labels = /* @__PURE__ */ new Set();
|
|
358987
|
+
const attrRe = /(?:text|content-desc|name|label)="([^"]{2,80})"/gi;
|
|
358988
|
+
for (const m of xml.matchAll(attrRe)) {
|
|
358989
|
+
const t = m[1].trim();
|
|
358990
|
+
if (t.length < 2 || t.length > 80) continue;
|
|
358991
|
+
if (/^(true|false|null|OK|Cancel)$/i.test(t)) continue;
|
|
358992
|
+
labels.add(t);
|
|
358993
|
+
}
|
|
358994
|
+
return [...labels].slice(0, 80);
|
|
358995
|
+
}
|
|
358996
|
+
function assertMobileStateDelta(before, after, expect, key, log2) {
|
|
358997
|
+
const beforeSet = new Set(before);
|
|
358998
|
+
const afterSet = new Set(after);
|
|
358999
|
+
const removed = before.filter((v) => !afterSet.has(v));
|
|
359000
|
+
const added = after.filter((v) => !beforeSet.has(v));
|
|
359001
|
+
log2(` assertStateDelta key=${key} expect=${expect} removed=${removed.length} added=${added.length}`);
|
|
359002
|
+
const exp = expect.toLowerCase().trim();
|
|
359003
|
+
if (exp === "changed" || exp === "any_change") {
|
|
359004
|
+
if (removed.length === 0 && added.length === 0) {
|
|
359005
|
+
throw new Error(`assertStateDelta(${key}): expected state to change, but capture is identical (${before.length} values)`);
|
|
359006
|
+
}
|
|
359007
|
+
return;
|
|
359008
|
+
}
|
|
359009
|
+
if (exp === "removed" || exp === "entity_removed" || exp.startsWith("removed:")) {
|
|
359010
|
+
const target = exp.startsWith("removed:") ? expect.slice("removed:".length).trim() : "";
|
|
359011
|
+
if (target) {
|
|
359012
|
+
if (!removed.includes(target) && afterSet.has(target)) {
|
|
359013
|
+
throw new Error(`assertStateDelta(${key}): expected "${target}" removed, but it is still present`);
|
|
359014
|
+
}
|
|
359015
|
+
if (!beforeSet.has(target)) {
|
|
359016
|
+
throw new Error(`assertStateDelta(${key}): expected "${target}" to have been in the prior capture`);
|
|
359017
|
+
}
|
|
359018
|
+
if (afterSet.has(target)) {
|
|
359019
|
+
throw new Error(`assertStateDelta(${key}): "${target}" was not removed`);
|
|
359020
|
+
}
|
|
359021
|
+
return;
|
|
359022
|
+
}
|
|
359023
|
+
if (removed.length === 0) {
|
|
359024
|
+
throw new Error(`assertStateDelta(${key}): expected at least one entity removed`);
|
|
359025
|
+
}
|
|
359026
|
+
return;
|
|
359027
|
+
}
|
|
359028
|
+
if (exp === "count_decreased" || exp === "count-1") {
|
|
359029
|
+
const b = before.map(Number).filter((n) => Number.isFinite(n));
|
|
359030
|
+
const a = after.map(Number).filter((n) => Number.isFinite(n));
|
|
359031
|
+
const bMax = b.length ? Math.max(...b) : NaN;
|
|
359032
|
+
const aMax = a.length ? Math.max(...a) : NaN;
|
|
359033
|
+
if (!Number.isFinite(bMax) || !Number.isFinite(aMax) || aMax >= bMax) {
|
|
359034
|
+
throw new Error(`assertStateDelta(${key}): expected count decrease (before max=${bMax}, after max=${aMax})`);
|
|
359035
|
+
}
|
|
359036
|
+
return;
|
|
359037
|
+
}
|
|
359038
|
+
if (exp === "count_increased" || exp === "count+1") {
|
|
359039
|
+
const b = before.map(Number).filter((n) => Number.isFinite(n));
|
|
359040
|
+
const a = after.map(Number).filter((n) => Number.isFinite(n));
|
|
359041
|
+
const bMax = b.length ? Math.max(...b) : NaN;
|
|
359042
|
+
const aMax = a.length ? Math.max(...a) : NaN;
|
|
359043
|
+
if (!Number.isFinite(bMax) || !Number.isFinite(aMax) || aMax <= bMax) {
|
|
359044
|
+
throw new Error(`assertStateDelta(${key}): expected count increase (before max=${bMax}, after max=${aMax})`);
|
|
359045
|
+
}
|
|
359046
|
+
return;
|
|
359047
|
+
}
|
|
359048
|
+
if (exp === "unchanged" || exp === "persisted") {
|
|
359049
|
+
if (removed.length > 0 || added.length > 0) {
|
|
359050
|
+
throw new Error(
|
|
359051
|
+
`assertStateDelta(${key}): expected persistence/unchanged, but removed=[${removed.slice(0, 5).join(", ")}] added=[${added.slice(0, 5).join(", ")}]`
|
|
359052
|
+
);
|
|
359053
|
+
}
|
|
359054
|
+
return;
|
|
359055
|
+
}
|
|
359056
|
+
if (removed.length === 0 && added.length === 0) {
|
|
359057
|
+
throw new Error(`assertStateDelta(${key}): expected change (expect=${expect}), state identical`);
|
|
359058
|
+
}
|
|
359059
|
+
}
|
|
357577
359060
|
async function restoreTargetAppIfExternal(sessionId, target, log2, source, failOnExternal) {
|
|
357578
359061
|
let boundary = null;
|
|
357579
359062
|
try {
|
|
@@ -358719,6 +360202,7 @@ async function executeMobileTest(options) {
|
|
|
358719
360202
|
}
|
|
358720
360203
|
const stepResults = [];
|
|
358721
360204
|
const screenshots = [];
|
|
360205
|
+
const stateBag = /* @__PURE__ */ new Map();
|
|
358722
360206
|
let apiCalls = [];
|
|
358723
360207
|
let sessionId = null;
|
|
358724
360208
|
let selectedDevice;
|
|
@@ -358929,6 +360413,9 @@ async function executeMobileTest(options) {
|
|
|
358929
360413
|
if (clearedAppDataBeforeLaunch) {
|
|
358930
360414
|
await waitForColdBootContent(sessionId, log2);
|
|
358931
360415
|
}
|
|
360416
|
+
if (options.target.launchMode !== "RESUME") {
|
|
360417
|
+
await dismissSystemUiAfterColdBoot(sessionId, options.target.platform, log2);
|
|
360418
|
+
}
|
|
358932
360419
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
358933
360420
|
try {
|
|
358934
360421
|
log2(`Opening deep link: ${options.target.deeplink}`);
|
|
@@ -359023,6 +360510,25 @@ async function executeMobileTest(options) {
|
|
|
359023
360510
|
case "assertText":
|
|
359024
360511
|
await executeAssertText(sessionId, step, runPlatform, isCrossPlatform);
|
|
359025
360512
|
break;
|
|
360513
|
+
case "captureState": {
|
|
360514
|
+
const key = String(step.metadata?.stateKey ?? step.target?.primary ?? `state-${step.order}`);
|
|
360515
|
+
const kind = String(step.metadata?.stateKind ?? "list_ids");
|
|
360516
|
+
const values = await captureMobileStateValues(sessionId, kind);
|
|
360517
|
+
stateBag.set(key, { kind, values, capturedAt: Date.now() });
|
|
360518
|
+
log2(` captureState key=${key} kind=${kind} n=${values.length}`);
|
|
360519
|
+
break;
|
|
360520
|
+
}
|
|
360521
|
+
case "assertStateDelta": {
|
|
360522
|
+
const key = String(step.metadata?.stateKey ?? step.target?.primary ?? "");
|
|
360523
|
+
const expect = String(step.metadata?.expect ?? step.assertion ?? "changed");
|
|
360524
|
+
const prior = stateBag.get(key);
|
|
360525
|
+
if (!prior) {
|
|
360526
|
+
throw new Error(`assertStateDelta: no prior captureState for key "${key}"`);
|
|
360527
|
+
}
|
|
360528
|
+
const current = await captureMobileStateValues(sessionId, prior.kind);
|
|
360529
|
+
assertMobileStateDelta(prior.values, current, expect, key, log2);
|
|
360530
|
+
break;
|
|
360531
|
+
}
|
|
359026
360532
|
default: {
|
|
359027
360533
|
const _exhaustive = step.action;
|
|
359028
360534
|
status = "failed";
|
|
@@ -359201,6 +360707,10 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
359201
360707
|
);
|
|
359202
360708
|
}
|
|
359203
360709
|
}
|
|
360710
|
+
try {
|
|
360711
|
+
await dismissBlockingSystemUi(`/session/${sessionId}`, target.platform);
|
|
360712
|
+
} catch {
|
|
360713
|
+
}
|
|
359204
360714
|
let stopped = false;
|
|
359205
360715
|
return {
|
|
359206
360716
|
sessionId,
|
|
@@ -359338,6 +360848,8 @@ async function driveHarvestStep(sessionId, step, elementId, platform3) {
|
|
|
359338
360848
|
case "waitForElement":
|
|
359339
360849
|
case "assertVisible":
|
|
359340
360850
|
case "assertText":
|
|
360851
|
+
case "captureState":
|
|
360852
|
+
case "assertStateDelta":
|
|
359341
360853
|
return true;
|
|
359342
360854
|
}
|
|
359343
360855
|
}
|
|
@@ -359371,6 +360883,7 @@ async function harvestPlatformLocators(options) {
|
|
|
359371
360883
|
try {
|
|
359372
360884
|
options.onSessionReady?.(session.sessionId, session.deviceId, platform3);
|
|
359373
360885
|
await freshLaunchTargetApp(session.sessionId, platform3, options.target.appId, log2);
|
|
360886
|
+
await dismissSystemUiAfterColdBoot(session.sessionId, platform3, log2);
|
|
359374
360887
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
359375
360888
|
await openDeepLink(session.sessionId, platform3, options.target.deeplink, options.target.appId);
|
|
359376
360889
|
}
|
|
@@ -359646,7 +361159,9 @@ function decideMobileDeviceGate(input) {
|
|
|
359646
361159
|
}
|
|
359647
361160
|
}
|
|
359648
361161
|
const isHealForHeldTest = runMode === "heal" && !!testCaseId && healHoldTestCaseIds.has(testCaseId);
|
|
359649
|
-
const
|
|
361162
|
+
const rawReservedForHeals = isHealForHeldTest ? Math.max(0, healHoldTestCaseIds.size - 1) : healHoldTestCaseIds.size;
|
|
361163
|
+
const maxHealReserve = capacity <= 1 ? capacity : Math.max(1, capacity - 1);
|
|
361164
|
+
const reservedForHeals = Math.min(rawReservedForHeals, maxHealReserve);
|
|
359650
361165
|
if (activeMobileOps + reservedForHeals >= capacity) {
|
|
359651
361166
|
const reason = reservedForHeals > 0 ? `capacity reserved for pending auto-heal: ${activeMobileOps}+${reservedForHeals}/${capacity}` : `mobile capacity reached: ${activeMobileOps}/${capacity} in use`;
|
|
359652
361167
|
return { claim: false, disposition: "defer", reason };
|
|
@@ -361482,6 +362997,11 @@ var MobileBridgeDriver = class {
|
|
|
361482
362997
|
} catch (err) {
|
|
361483
362998
|
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
361484
362999
|
}
|
|
363000
|
+
try {
|
|
363001
|
+
await dismissBlockingSystemUi(this.sessionPath, this.platform);
|
|
363002
|
+
} catch (err) {
|
|
363003
|
+
if (isRecoverableAppiumSessionFailure(err) || isAppiumDriverDeadError(err)) throw err;
|
|
363004
|
+
}
|
|
361485
363005
|
});
|
|
361486
363006
|
}
|
|
361487
363007
|
/**
|
|
@@ -361519,6 +363039,11 @@ var MobileBridgeDriver = class {
|
|
|
361519
363039
|
} catch (err) {
|
|
361520
363040
|
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
361521
363041
|
}
|
|
363042
|
+
try {
|
|
363043
|
+
await dismissBlockingSystemUi(this.sessionPath, this.platform);
|
|
363044
|
+
} catch (err) {
|
|
363045
|
+
if (isRecoverableAppiumSessionFailure(err) || isAppiumDriverDeadError(err)) throw err;
|
|
363046
|
+
}
|
|
361522
363047
|
return true;
|
|
361523
363048
|
} catch (err) {
|
|
361524
363049
|
if (isRecoverableAppiumSessionFailure(err)) throw err;
|
|
@@ -364878,6 +366403,17 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
364878
366403
|
}
|
|
364879
366404
|
};
|
|
364880
366405
|
log.info("\u{1FA7A} Mobile AI heal \u2014 Phase 1 (script) \u2192 Phase 2 (live device, lazy)...");
|
|
366406
|
+
const tagList = Array.isArray(run2.tags) ? run2.tags : [];
|
|
366407
|
+
const failureCount = Math.max(
|
|
366408
|
+
1,
|
|
366409
|
+
typeof run2.consecutiveFails === "number" && run2.consecutiveFails > 0 ? run2.consecutiveFails : tagList.includes("@bug-found") ? 2 : 1
|
|
366410
|
+
);
|
|
366411
|
+
const reproduceApiCalls = Array.isArray(nativeVerify.last?.apiCalls) ? nativeVerify.last.apiCalls.map((c) => ({
|
|
366412
|
+
method: typeof c.method === "string" ? c.method : void 0,
|
|
366413
|
+
url: typeof c.url === "string" ? c.url : void 0,
|
|
366414
|
+
status: typeof c.status === "number" ? c.status : void 0
|
|
366415
|
+
})) : void 0;
|
|
366416
|
+
const uiPathSucceeded = Array.isArray(reproduce.stepResults) && reproduce.stepResults.filter((s) => s.status === "passed").length >= Math.max(1, reproduce.stepResults.length - 1);
|
|
364881
366417
|
const heal = await (0, import_runner_core14.executeMobileTestWithHealing)(
|
|
364882
366418
|
mobileSteps,
|
|
364883
366419
|
healTarget,
|
|
@@ -364886,7 +366422,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
364886
366422
|
logs: reproduce.logs,
|
|
364887
366423
|
failingSource: reproduce.failingSource,
|
|
364888
366424
|
testVariables: run2.testVariables,
|
|
364889
|
-
testName: run2.testName
|
|
366425
|
+
testName: run2.testName,
|
|
366426
|
+
tags: tagList,
|
|
366427
|
+
failureCount,
|
|
366428
|
+
apiCalls: reproduceApiCalls,
|
|
366429
|
+
uiPathSucceeded
|
|
364890
366430
|
},
|
|
364891
366431
|
healerOptions
|
|
364892
366432
|
);
|
|
@@ -364895,7 +366435,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
364895
366435
|
heal,
|
|
364896
366436
|
finalError: finalRes?.error ?? null,
|
|
364897
366437
|
reproduceError,
|
|
364898
|
-
testVariables: run2.testVariables
|
|
366438
|
+
testVariables: run2.testVariables,
|
|
366439
|
+
originalSteps: mobileSteps
|
|
364899
366440
|
});
|
|
364900
366441
|
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
364901
366442
|
status: outcome.status,
|