@validate.qa/runner 1.0.89 → 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.
Files changed (3) hide show
  1. package/dist/cli.js +210 -20
  2. package/dist/cli.mjs +210 -20
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -80738,7 +80738,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
80738
80738
  2. **One purpose per scenario** - each scenario tests ONE thing (happy path, validation, journey, or bug)
80739
80739
  3. **Happy paths first** - then pair edge cases to the same form/flow
80740
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)
80741
- 5. **Bug reports** - for observed crashes, ANRs, or broken features
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
80742
80742
  6. **Steps are human-readable** - "Type into the email field", not "driver.findElement(AppiumBy.accessibilityId('email')).sendKeys()"
80743
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.
80744
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
@@ -81150,7 +81150,7 @@ ${context.userNotes.trim()}`);
81150
81150
  const requiredBlock = (0, scenario_taxonomy_js_1.formatRequiredScenarioKindsPrompt)(signals);
81151
81151
  if (requiredBlock) {
81152
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 when an email field was observed.\n3. For crud_identity: assert the specific entity identity is gone/changed \u2014 never only that the list screen is still visible.\n4. For count_delta: mention the before/after numeric expectation in expectedOutcomes.\n5. If a required kind is impossible given evidence, say so in "reasoning" \u2014 do not invent unobserved screens.');
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
81154
  }
81155
81155
  }
81156
81156
  const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration || a.screenId.localeCompare(b.screenId));
@@ -82026,6 +82026,123 @@ var require_discovery_lint_flags = __commonJS({
82026
82026
  }
82027
82027
  });
82028
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
+
82029
82146
  // ../runner-core/dist/services/mobile/mobile-generator.js
82030
82147
  var require_mobile_generator = __commonJS({
82031
82148
  "../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
@@ -82052,6 +82169,8 @@ var require_mobile_generator = __commonJS({
82052
82169
  var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
82053
82170
  var discovery_lint_flags_js_1 = require_discovery_lint_flags();
82054
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();
82055
82174
  var mobile_source_parser_js_1 = require_mobile_source_parser();
82056
82175
  var mobile_source_parser_js_2 = require_mobile_source_parser();
82057
82176
  var mobile_observation_js_1 = require_mobile_observation();
@@ -82197,7 +82316,7 @@ var require_mobile_generator = __commonJS({
82197
82316
  type: "function",
82198
82317
  function: {
82199
82318
  name: "mobile_capture_state",
82200
- description: 'Capture list labels or numeric badge counts into a named state key BEFORE a mutating action (delete/create/toggle). Pair with mobile_assert_state_delta afterward to prove durable identity/count outcomes \u2014 stronger than "list still visible".',
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.",
82201
82320
  parameters: {
82202
82321
  type: "object",
82203
82322
  properties: {
@@ -82559,9 +82678,11 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
82559
82678
  - Navigation scenarios: you MUST land on the destination and assert something unique to it.
82560
82679
  4. PROVE the outcome with an assert tool that matches the scenario \u2014 not ambient chrome:
82561
82680
  - mobile_assert_visible({ref}) / mobile_assert_text({ref, expected})
82562
- - For list delete/create or count changes: mobile_capture_state({stateKey, kind}) BEFORE the mutation, then mobile_assert_state_delta({stateKey, expect}) AFTER \u2014 proves identity/count change, not only "list screen still visible".
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".
82563
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.
82564
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.
82565
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.
82566
82687
 
82567
82688
  ## Rules
@@ -82570,7 +82691,7 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
82570
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.
82571
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.
82572
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.
82573
- - If the APP itself misbehaves (crash, error screen, a flow that cannot proceed as specified), do not force a green path: assert the failure you observed (e.g. the error text), then call finish_generate with suspectedBug:true and a concrete bugDescription of what is broken in the app.
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.
82574
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.
82575
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.
82576
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.
@@ -84162,6 +84283,9 @@ ${statePacket}`, redactionCredentials) },
84162
84283
  function weakAssertionTargetReason(element) {
84163
84284
  if (isAlwaysPresentContainer(element))
84164
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
+ }
84165
84289
  const hasIdentitySignal = Boolean(nonEmpty3(element.accessibilityId) || nonEmpty3(element.resourceId) || nonEmpty3(element.text) || nonEmpty3(element.label));
84166
84290
  if (!hasIdentitySignal)
84167
84291
  return "no identity locator (accessibility id / resource id / text) for the assert gate";
@@ -85023,6 +85147,30 @@ ${statePacket}`, redactionCredentials) },
85023
85147
  return [action.selector, action.value, action.innerText, action.metadata?.targetLabel].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
85024
85148
  }
85025
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
+ }
85026
85174
  if (actions.some((a) => a.type === "ASSERT_STATE_DELTA")) {
85027
85175
  return {
85028
85176
  ok: true,
@@ -85169,9 +85317,10 @@ ${statePacket}`, redactionCredentials) },
85169
85317
  }
85170
85318
  const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
85171
85319
  const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
85172
- const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0;
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;
85173
85322
  if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
85174
- 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."));
85175
85324
  }
85176
85325
  const strengthVerdict = bugWitnessHonored ? {
85177
85326
  ...baseStrengthVerdict,
@@ -85210,12 +85359,22 @@ ${statePacket}`, redactionCredentials) },
85210
85359
  const strengthBlocked = !strengthVerdict.ok || strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug" || !verified || coldResetUnavailable;
85211
85360
  {
85212
85361
  const kind = (0, scenario_taxonomy_js_1.inferScenarioKind)(scenario);
85213
- const hasStateOracle = compiled.steps.some((s) => s.action === "assertStateDelta" || s.action === "assertText" || s.action === "captureState");
85214
- const strong = verified && !strengthBlocked && strengthVerdict.severity === "none" && ((0, scenario_taxonomy_js_1.isStrongOracleKind)(kind) || hasStateOracle);
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);
85215
85371
  if (strong)
85216
85372
  tags.push("@strong-oracle");
85217
- else if (!tags.includes("@smoke") && (kind === "smoke_nav" || kind === "navigation_integrity")) {
85373
+ else if (!tags.includes("@smoke")) {
85218
85374
  tags.push("@smoke");
85375
+ if (!oracleQuality.strong) {
85376
+ log2(` tagged @smoke (not strong-oracle): ${oracleQuality.reason}`);
85377
+ }
85219
85378
  }
85220
85379
  }
85221
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;
@@ -85475,6 +85634,7 @@ var require_failure_triage = __commonJS({
85475
85634
  exports2.listStrongOracleKinds = listStrongOracleKinds;
85476
85635
  exports2.triageMobileFailure = triageMobileFailure;
85477
85636
  exports2.triageToHealTerminal = triageToHealTerminal;
85637
+ var anr_env_js_1 = require_anr_env();
85478
85638
  var STRONG_ACTIONS = /* @__PURE__ */ new Set(["assertStateDelta", "assertText", "captureState"]);
85479
85639
  var CHROME_VISIBLE_RE = /\b(tab|home|settings|back|menu|navigation|nav bar|bottom nav|status bar)\b/i;
85480
85640
  function hasStrongMobileOracle(steps) {
@@ -85564,6 +85724,15 @@ ${logs2 ?? ""}`.trim();
85564
85724
  failureCount,
85565
85725
  errorPrefix: prefix
85566
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
+ }
85567
85736
  if (prefix === "CONFIG_ISSUE" || prefix === "RATE_LIMIT_BLOCKED") {
85568
85737
  return {
85569
85738
  verdict: "config_issue",
@@ -85812,6 +85981,7 @@ var require_mobile_heal_postprocess = __commonJS({
85812
85981
  exports2.detectMobileStepSecretLeak = detectMobileStepSecretLeak;
85813
85982
  exports2.computeMobileHealOutcome = computeMobileHealOutcome2;
85814
85983
  var oracle_protection_js_1 = require_oracle_protection();
85984
+ var anr_env_js_1 = require_anr_env();
85815
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;
85816
85986
  var DETECT_KEY_RE = /password|secret|token|api[_]?key|\bpass\b|credential|bearer|\bpat\b/i;
85817
85987
  var MIN_REDACT_LEN = 4;
@@ -85882,21 +86052,26 @@ var require_mobile_heal_postprocess = __commonJS({
85882
86052
  const oracleWeakening = isVerifiedFix && args.originalSteps ? (0, oracle_protection_js_1.rejectOracleWeakeningPatch)(args.originalSteps, heal.healedSteps ?? []) : null;
85883
86053
  if (oracleWeakening)
85884
86054
  isVerifiedFix = false;
85885
- const isConfigIssue = heal.verdict === "config_issue";
85886
- const isAppBug = heal.verdict === "app_bug" || Boolean(oracleWeakening && heal.passed);
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));
85887
86058
  const extraTags = new Set((heal.extraTags ?? []).filter((t) => typeof t === "string" && t.length > 0));
85888
86059
  if (secretLeak)
85889
86060
  extraTags.add("@needs-review");
85890
- if (oracleWeakening) {
86061
+ if (oracleWeakening && !anrEnv) {
85891
86062
  extraTags.add("@needs-review");
85892
86063
  extraTags.add("@bug-found");
85893
86064
  }
86065
+ if (anrEnv) {
86066
+ extraTags.delete("@bug-found");
86067
+ extraTags.add("@needs-review");
86068
+ }
85894
86069
  if (!heal.passed && NEEDS_REVIEW_VERDICTS.has(heal.verdict))
85895
86070
  extraTags.add("@needs-review");
85896
86071
  let status;
85897
86072
  if (secretLeak)
85898
86073
  status = "FAILED";
85899
- else if (oracleWeakening)
86074
+ else if (oracleWeakening && !anrEnv)
85900
86075
  status = "FAILED";
85901
86076
  else if (isVerifiedFix)
85902
86077
  status = "PASSED";
@@ -85909,10 +86084,11 @@ var require_mobile_heal_postprocess = __commonJS({
85909
86084
  error2 = void 0;
85910
86085
  } else if (secretLeak) {
85911
86086
  error2 = `HEAL_SECRET_LEAK: healed steps contained a literal secret (${secretLeak}); patch dropped, tagged @needs-review`;
85912
- } else if (oracleWeakening) {
86087
+ } else if (oracleWeakening && !anrEnv) {
85913
86088
  error2 = `APP_BUG: heal attempted to drop strong oracle (${oracleWeakening}); patch refused`;
85914
86089
  } else if (isConfigIssue) {
85915
- error2 = `CONFIG_ISSUE: ${heal.healReason ?? args.finalError ?? args.reproduceError ?? "environment/rate-limit blocker"}`;
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}`;
85916
86092
  } else if (isAppBug) {
85917
86093
  error2 = `APP_BUG: ${heal.healReason ?? args.finalError ?? args.reproduceError ?? "application defect found during heal"}`;
85918
86094
  } else if (heal.passed && heal.verdict === "fix" && !hasFix) {
@@ -86355,6 +86531,7 @@ var require_mobile_heal_loop = __commonJS({
86355
86531
  var failure_triage_js_1 = require_failure_triage();
86356
86532
  var oracle_protection_js_1 = require_oracle_protection();
86357
86533
  var network_oracle_js_1 = require_network_oracle();
86534
+ var anr_env_js_1 = require_anr_env();
86358
86535
  var mobile_observation_js_1 = require_mobile_observation();
86359
86536
  var selector_registry_context_js_1 = require_selector_registry_context();
86360
86537
  var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
@@ -86366,12 +86543,13 @@ Reason over steps + error + tree + registry and return working healed steps. Do
86366
86543
  2. STEP RESULTS: which steps passed before the failure.
86367
86544
  3. TREE: does the current screen match what the failed step expects?
86368
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.
86369
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.
86370
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.
86371
86549
  ## Response \u2014 STRICT JSON, exactly one verdict:
86372
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}
86373
- {"verdict":"app_bug","reason":"app crashed/blank/5xx (NOT rate-limit)"}
86374
- {"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"}
86375
86553
  {"verdict":"needs_device","reason":"cannot fix from a static tree; need live re-observation"}
86376
86554
  {"verdict":"prereq_unmet","reason":"required entity/state absent for the account"}
86377
86555
  {"verdict":"state_stale","reason":"leftover data blocks a baseline assertion"}
@@ -86382,7 +86560,7 @@ For a fix, return the COMPLETE steps array (not a diff).
86382
86560
  - NEVER remove or downgrade assertStateDelta, captureState, or assertText outcome proof to go green.
86383
86561
  - NEVER replace a durable outcome assert with mere assertVisible of a tab/header.
86384
86562
  - You MAY fix locators, waits, and navigation around those oracles.
86385
- - If the app truly violates the oracle, return verdict "app_bug" instead of stripping the assert.`;
86563
+ - If the app truly violates the oracle on a healthy UI, return verdict "app_bug" instead of stripping the assert.`;
86386
86564
  var DEFAULT_SCRIPT_MAX_ATTEMPTS = 6;
86387
86565
  var RECOVERABLE_MAX = 3;
86388
86566
  var MAX_CONSECUTIVE_INVALID_VERDICTS = 3;
@@ -87350,6 +87528,18 @@ ${args.directive}`);
87350
87528
  providerUnavailable: true
87351
87529
  };
87352
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
+ }
87353
87543
  const shouldEscalate = script.verdict === "needs_device" || script.verdict === "fix" && !script.passed;
87354
87544
  if (shouldEscalate && options.runLiveHeal) {
87355
87545
  try {
@@ -355106,7 +355296,7 @@ var require_package4 = __commonJS({
355106
355296
  "package.json"(exports2, module2) {
355107
355297
  module2.exports = {
355108
355298
  name: "@validate.qa/runner",
355109
- version: "1.0.89",
355299
+ version: "1.0.90",
355110
355300
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
355111
355301
  bin: {
355112
355302
  "validate-runner": "dist/cli.js"
package/dist/cli.mjs CHANGED
@@ -80743,7 +80743,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
80743
80743
  2. **One purpose per scenario** - each scenario tests ONE thing (happy path, validation, journey, or bug)
80744
80744
  3. **Happy paths first** - then pair edge cases to the same form/flow
80745
80745
  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)
80746
- 5. **Bug reports** - for observed crashes, ANRs, or broken features
80746
+ 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
80747
80747
  6. **Steps are human-readable** - "Type into the email field", not "driver.findElement(AppiumBy.accessibilityId('email')).sendKeys()"
80748
80748
  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.
80749
80749
  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
@@ -81155,7 +81155,7 @@ ${context.userNotes.trim()}`);
81155
81155
  const requiredBlock = (0, scenario_taxonomy_js_1.formatRequiredScenarioKindsPrompt)(signals);
81156
81156
  if (requiredBlock) {
81157
81157
  sections.push(requiredBlock);
81158
- 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 when an email field was observed.\n3. For crud_identity: assert the specific entity identity is gone/changed \u2014 never only that the list screen is still visible.\n4. For count_delta: mention the before/after numeric expectation in expectedOutcomes.\n5. If a required kind is impossible given evidence, say so in "reasoning" \u2014 do not invent unobserved screens.');
81158
+ 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.');
81159
81159
  }
81160
81160
  }
81161
81161
  const orderedScreens = plannableTargetAppScreens(graph, context.appId).sort((a, b) => a.firstSeenIteration - b.firstSeenIteration || a.screenId.localeCompare(b.screenId));
@@ -82031,6 +82031,123 @@ var require_discovery_lint_flags = __commonJS({
82031
82031
  }
82032
82032
  });
82033
82033
 
82034
+ // ../runner-core/dist/services/mobile/anr-env.js
82035
+ var require_anr_env = __commonJS({
82036
+ "../runner-core/dist/services/mobile/anr-env.js"(exports) {
82037
+ "use strict";
82038
+ Object.defineProperty(exports, "__esModule", { value: true });
82039
+ exports.textLooksLikeAnrOrSystemUi = textLooksLikeAnrOrSystemUi;
82040
+ exports.isAnrOrSystemUiEnvironmentFailure = isAnrOrSystemUiEnvironmentFailure;
82041
+ exports.stepLooksLikeAnrChromeAssert = stepLooksLikeAnrChromeAssert;
82042
+ 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;
82043
+ function textLooksLikeAnrOrSystemUi(text) {
82044
+ if (!text || typeof text !== "string")
82045
+ return false;
82046
+ return ANR_TEXT_RE.test(text);
82047
+ }
82048
+ function isAnrOrSystemUiEnvironmentFailure(error2, logs2) {
82049
+ const blob = `${error2 ?? ""}
82050
+ ${logs2 ?? ""}`;
82051
+ if (textLooksLikeAnrOrSystemUi(blob))
82052
+ return true;
82053
+ if (/\bcold-boot\b/i.test(blob) && /\b(froze|hang|anr|unresponsive)\b/i.test(blob))
82054
+ return true;
82055
+ if (/\bno actionable elements\b/i.test(blob) && /\b(system|anr|responding)\b/i.test(blob)) {
82056
+ return true;
82057
+ }
82058
+ return false;
82059
+ }
82060
+ function stepLooksLikeAnrChromeAssert(step) {
82061
+ if (step.action !== "assertVisible" && step.action !== "assertText")
82062
+ return false;
82063
+ return textLooksLikeAnrOrSystemUi([step.description, step.assertion, step.value].filter(Boolean).join(" "));
82064
+ }
82065
+ }
82066
+ });
82067
+
82068
+ // ../runner-core/dist/services/mobile/oracle-quality.js
82069
+ var require_oracle_quality = __commonJS({
82070
+ "../runner-core/dist/services/mobile/oracle-quality.js"(exports) {
82071
+ "use strict";
82072
+ Object.defineProperty(exports, "__esModule", { value: true });
82073
+ exports.assessStrongOracleQuality = assessStrongOracleQuality;
82074
+ var anr_env_js_1 = require_anr_env();
82075
+ var CHROME_ONLY_RE = /\b(tab|home tab|catalog tab|forms tab|account tab|back|menu|navigation|nav bar|bottom nav|status bar|query|search field)\b/i;
82076
+ 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;
82077
+ function stepBlob(step) {
82078
+ return [step.description, step.assertion, step.value, step.target?.primary].filter(Boolean).join(" ");
82079
+ }
82080
+ function isChromeOnlyVisible(step) {
82081
+ if (step.action !== "assertVisible")
82082
+ return false;
82083
+ const blob = stepBlob(step);
82084
+ if (PRODUCT_OUTCOME_TEXT_RE.test(blob))
82085
+ return false;
82086
+ return CHROME_ONLY_RE.test(blob) || blob.length < 40;
82087
+ }
82088
+ function assessStrongOracleQuality(steps) {
82089
+ const productSteps = steps.filter((s) => s.action !== "launchApp");
82090
+ const anrAsserts = productSteps.filter(anr_env_js_1.stepLooksLikeAnrChromeAssert);
82091
+ const nonAnrAsserts = productSteps.filter((s) => (s.action === "assertVisible" || s.action === "assertText" || s.action === "assertStateDelta") && !(0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s));
82092
+ if (productSteps.length > 0 && anrAsserts.length > 0 && nonAnrAsserts.length === 0) {
82093
+ return {
82094
+ strong: false,
82095
+ unpairedCaptures: 0,
82096
+ hasStateDelta: false,
82097
+ hasStrongAssertText: false,
82098
+ hasStrongValidationVisible: false,
82099
+ anrOnly: true,
82100
+ reason: "only asserts system ANR / freeze chrome \u2014 not a product test"
82101
+ };
82102
+ }
82103
+ const captures = steps.filter((s) => s.action === "captureState").length;
82104
+ const deltas = steps.filter((s) => s.action === "assertStateDelta").length;
82105
+ const unpairedCaptures = Math.max(0, captures - deltas);
82106
+ const hasStateDelta = deltas > 0;
82107
+ const hasStrongAssertText = steps.some((s) => {
82108
+ if (s.action !== "assertText")
82109
+ return false;
82110
+ if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s))
82111
+ return false;
82112
+ const blob = stepBlob(s);
82113
+ if (/^(email|password|username|name|query|search)$/i.test((s.assertion ?? "").trim())) {
82114
+ return false;
82115
+ }
82116
+ return blob.length >= 3;
82117
+ });
82118
+ const hasStrongValidationVisible = steps.some((s) => {
82119
+ if (s.action !== "assertVisible")
82120
+ return false;
82121
+ if ((0, anr_env_js_1.stepLooksLikeAnrChromeAssert)(s) || isChromeOnlyVisible(s))
82122
+ return false;
82123
+ return PRODUCT_OUTCOME_TEXT_RE.test(stepBlob(s));
82124
+ });
82125
+ const strong = hasStateDelta || hasStrongAssertText || hasStrongValidationVisible;
82126
+ let reason;
82127
+ if (strong && hasStateDelta) {
82128
+ reason = unpairedCaptures > 0 ? `has assertStateDelta but ${unpairedCaptures} unpaired captureState (prefer 1:1 pairing)` : "has assertStateDelta durable state proof";
82129
+ } else if (strong && hasStrongAssertText) {
82130
+ reason = "has product assertText outcome";
82131
+ } else if (strong) {
82132
+ reason = "has validation/error assertVisible outcome";
82133
+ } else if (unpairedCaptures > 0) {
82134
+ reason = `captureState without assertStateDelta (${unpairedCaptures}) \u2014 not strong`;
82135
+ } else {
82136
+ reason = "no durable state/text/validation oracle \u2014 smoke/nav only";
82137
+ }
82138
+ return {
82139
+ strong,
82140
+ unpairedCaptures,
82141
+ hasStateDelta,
82142
+ hasStrongAssertText,
82143
+ hasStrongValidationVisible,
82144
+ anrOnly: false,
82145
+ reason
82146
+ };
82147
+ }
82148
+ }
82149
+ });
82150
+
82034
82151
  // ../runner-core/dist/services/mobile/mobile-generator.js
82035
82152
  var require_mobile_generator = __commonJS({
82036
82153
  "../runner-core/dist/services/mobile/mobile-generator.js"(exports) {
@@ -82057,6 +82174,8 @@ var require_mobile_generator = __commonJS({
82057
82174
  var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
82058
82175
  var discovery_lint_flags_js_1 = require_discovery_lint_flags();
82059
82176
  var scenario_taxonomy_js_1 = require_scenario_taxonomy();
82177
+ var anr_env_js_1 = require_anr_env();
82178
+ var oracle_quality_js_1 = require_oracle_quality();
82060
82179
  var mobile_source_parser_js_1 = require_mobile_source_parser();
82061
82180
  var mobile_source_parser_js_2 = require_mobile_source_parser();
82062
82181
  var mobile_observation_js_1 = require_mobile_observation();
@@ -82202,7 +82321,7 @@ var require_mobile_generator = __commonJS({
82202
82321
  type: "function",
82203
82322
  function: {
82204
82323
  name: "mobile_capture_state",
82205
- description: 'Capture list labels or numeric badge counts into a named state key BEFORE a mutating action (delete/create/toggle). Pair with mobile_assert_state_delta afterward to prove durable identity/count outcomes \u2014 stronger than "list still visible".',
82324
+ 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.",
82206
82325
  parameters: {
82207
82326
  type: "object",
82208
82327
  properties: {
@@ -82564,9 +82683,11 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
82564
82683
  - Navigation scenarios: you MUST land on the destination and assert something unique to it.
82565
82684
  4. PROVE the outcome with an assert tool that matches the scenario \u2014 not ambient chrome:
82566
82685
  - mobile_assert_visible({ref}) / mobile_assert_text({ref, expected})
82567
- - For list delete/create or count changes: mobile_capture_state({stateKey, kind}) BEFORE the mutation, then mobile_assert_state_delta({stateKey, expect}) AFTER \u2014 proves identity/count change, not only "list screen still visible".
82686
+ - For list delete/create or count changes: mobile_capture_state({stateKey, kind}) BEFORE the mutation, then mobile_assert_state_delta({stateKey, expect}) AFTER \u2014 BOTH are required. captureState alone does NOT prove the outcome.
82687
+ - 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".
82568
82688
  - 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.
82569
82689
  - Do NOT assert bottom-nav tab labels alone, notification badge counts ("N unread"), or one-time empty-state CTAs as the scenario outcome.
82690
+ - 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.
82570
82691
  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.
82571
82692
 
82572
82693
  ## Rules
@@ -82575,7 +82696,7 @@ The scenario name + goal + steps describe ONE product behavior from survey/disco
82575
82696
  - 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.
82576
82697
  - 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.
82577
82698
  - 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.
82578
- - If the APP itself misbehaves (crash, error screen, a flow that cannot proceed as specified), do not force a green path: assert the failure you observed (e.g. the error text), then call finish_generate with suspectedBug:true and a concrete bugDescription of what is broken in the app.
82699
+ - 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.
82579
82700
  - 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.
82580
82701
  - 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.
82581
82702
  - 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.
@@ -84167,6 +84288,9 @@ ${statePacket}`, redactionCredentials) },
84167
84288
  function weakAssertionTargetReason(element) {
84168
84289
  if (isAlwaysPresentContainer(element))
84169
84290
  return "always-present app/system container";
84291
+ if ((0, anr_env_js_1.textLooksLikeAnrOrSystemUi)([element.text, element.label, element.accessibilityId, element.resourceId].filter(Boolean).join(" "))) {
84292
+ return "system ANR / freeze dialog chrome (environment, not product)";
84293
+ }
84170
84294
  const hasIdentitySignal = Boolean(nonEmpty3(element.accessibilityId) || nonEmpty3(element.resourceId) || nonEmpty3(element.text) || nonEmpty3(element.label));
84171
84295
  if (!hasIdentitySignal)
84172
84296
  return "no identity locator (accessibility id / resource id / text) for the assert gate";
@@ -85028,6 +85152,30 @@ ${statePacket}`, redactionCredentials) },
85028
85152
  return [action.selector, action.value, action.innerText, action.metadata?.targetLabel].filter((v) => typeof v === "string" && v.trim().length > 0).join(" ");
85029
85153
  }
85030
85154
  function evaluateMobileAssertionStrength(actions) {
85155
+ const anrAsserts = actions.filter((a) => {
85156
+ if (a.type !== "ASSERT_VISIBLE" && a.type !== "ASSERT_TEXT")
85157
+ return false;
85158
+ return (0, anr_env_js_1.textLooksLikeAnrOrSystemUi)([a.value, a.innerText, a.metadata?.targetLabel, a.selector].filter(Boolean).join(" "));
85159
+ });
85160
+ if (anrAsserts.length > 0) {
85161
+ const otherAsserts = actions.filter((a) => (a.type === "ASSERT_VISIBLE" || a.type === "ASSERT_TEXT" || a.type === "ASSERT_STATE_DELTA") && !anrAsserts.includes(a));
85162
+ if (otherAsserts.length === 0) {
85163
+ return {
85164
+ ok: false,
85165
+ severity: "block",
85166
+ reason: `only asserts system ANR / "isn't responding" chrome \u2014 not a product test (discard or re-record on a healthy session)`,
85167
+ signals: {
85168
+ apiTest: false,
85169
+ interacted: actions.some((a) => a.type === "TAP" || a.type === "TYPE"),
85170
+ mutated: false,
85171
+ navOnly: false,
85172
+ hasOutcomeProof: false,
85173
+ errorBanner: false,
85174
+ terminal: null
85175
+ }
85176
+ };
85177
+ }
85178
+ }
85031
85179
  if (actions.some((a) => a.type === "ASSERT_STATE_DELTA")) {
85032
85180
  return {
85033
85181
  ok: true,
@@ -85174,9 +85322,10 @@ ${statePacket}`, redactionCredentials) },
85174
85322
  }
85175
85323
  const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
85176
85324
  const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
85177
- const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0;
85325
+ const bugLooksLikeAnr = (0, anr_env_js_1.textLooksLikeAnrOrSystemUi)(bugDescription);
85326
+ const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0 && !bugLooksLikeAnr;
85178
85327
  if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
85179
- 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."));
85328
+ 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."));
85180
85329
  }
85181
85330
  const strengthVerdict = bugWitnessHonored ? {
85182
85331
  ...baseStrengthVerdict,
@@ -85215,12 +85364,22 @@ ${statePacket}`, redactionCredentials) },
85215
85364
  const strengthBlocked = !strengthVerdict.ok || strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug" || !verified || coldResetUnavailable;
85216
85365
  {
85217
85366
  const kind = (0, scenario_taxonomy_js_1.inferScenarioKind)(scenario);
85218
- const hasStateOracle = compiled.steps.some((s) => s.action === "assertStateDelta" || s.action === "assertText" || s.action === "captureState");
85219
- const strong = verified && !strengthBlocked && strengthVerdict.severity === "none" && ((0, scenario_taxonomy_js_1.isStrongOracleKind)(kind) || hasStateOracle);
85367
+ const oracleQuality = (0, oracle_quality_js_1.assessStrongOracleQuality)(compiled.steps);
85368
+ if (oracleQuality.anrOnly) {
85369
+ log2(` discarding ANR-chrome-only recording: ${oracleQuality.reason}`);
85370
+ return null;
85371
+ }
85372
+ if (oracleQuality.unpairedCaptures > 0) {
85373
+ log2(` oracle quality: ${oracleQuality.reason}`);
85374
+ }
85375
+ const strong = verified && !strengthBlocked && strengthVerdict.severity === "none" && oracleQuality.strong && ((0, scenario_taxonomy_js_1.isStrongOracleKind)(kind) || oracleQuality.hasStateDelta || oracleQuality.hasStrongAssertText);
85220
85376
  if (strong)
85221
85377
  tags.push("@strong-oracle");
85222
- else if (!tags.includes("@smoke") && (kind === "smoke_nav" || kind === "navigation_integrity")) {
85378
+ else if (!tags.includes("@smoke")) {
85223
85379
  tags.push("@smoke");
85380
+ if (!oracleQuality.strong) {
85381
+ log2(` tagged @smoke (not strong-oracle): ${oracleQuality.reason}`);
85382
+ }
85224
85383
  }
85225
85384
  }
85226
85385
  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;
@@ -85480,6 +85639,7 @@ var require_failure_triage = __commonJS({
85480
85639
  exports.listStrongOracleKinds = listStrongOracleKinds;
85481
85640
  exports.triageMobileFailure = triageMobileFailure;
85482
85641
  exports.triageToHealTerminal = triageToHealTerminal;
85642
+ var anr_env_js_1 = require_anr_env();
85483
85643
  var STRONG_ACTIONS = /* @__PURE__ */ new Set(["assertStateDelta", "assertText", "captureState"]);
85484
85644
  var CHROME_VISIBLE_RE = /\b(tab|home|settings|back|menu|navigation|nav bar|bottom nav|status bar)\b/i;
85485
85645
  function hasStrongMobileOracle(steps) {
@@ -85569,6 +85729,15 @@ ${logs2 ?? ""}`.trim();
85569
85729
  failureCount,
85570
85730
  errorPrefix: prefix
85571
85731
  };
85732
+ if ((0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(input.error, input.logs)) {
85733
+ return {
85734
+ verdict: "config_issue",
85735
+ reason: "ANR / System UI freeze (device or app hung) \u2014 treat as environment, not product bug",
85736
+ shouldHeal: false,
85737
+ healVerdict: "config_issue",
85738
+ evidence
85739
+ };
85740
+ }
85572
85741
  if (prefix === "CONFIG_ISSUE" || prefix === "RATE_LIMIT_BLOCKED") {
85573
85742
  return {
85574
85743
  verdict: "config_issue",
@@ -85817,6 +85986,7 @@ var require_mobile_heal_postprocess = __commonJS({
85817
85986
  exports.detectMobileStepSecretLeak = detectMobileStepSecretLeak;
85818
85987
  exports.computeMobileHealOutcome = computeMobileHealOutcome2;
85819
85988
  var oracle_protection_js_1 = require_oracle_protection();
85989
+ var anr_env_js_1 = require_anr_env();
85820
85990
  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;
85821
85991
  var DETECT_KEY_RE = /password|secret|token|api[_]?key|\bpass\b|credential|bearer|\bpat\b/i;
85822
85992
  var MIN_REDACT_LEN = 4;
@@ -85887,21 +86057,26 @@ var require_mobile_heal_postprocess = __commonJS({
85887
86057
  const oracleWeakening = isVerifiedFix && args.originalSteps ? (0, oracle_protection_js_1.rejectOracleWeakeningPatch)(args.originalSteps, heal.healedSteps ?? []) : null;
85888
86058
  if (oracleWeakening)
85889
86059
  isVerifiedFix = false;
85890
- const isConfigIssue = heal.verdict === "config_issue";
85891
- const isAppBug = heal.verdict === "app_bug" || Boolean(oracleWeakening && heal.passed);
86060
+ const anrEnv = (0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(heal.healReason ?? args.finalError ?? args.reproduceError, typeof args.finalError === "string" ? args.finalError : void 0);
86061
+ const isConfigIssue = heal.verdict === "config_issue" || heal.verdict === "app_bug" && anrEnv;
86062
+ const isAppBug = !anrEnv && (heal.verdict === "app_bug" || Boolean(oracleWeakening && heal.passed));
85892
86063
  const extraTags = new Set((heal.extraTags ?? []).filter((t) => typeof t === "string" && t.length > 0));
85893
86064
  if (secretLeak)
85894
86065
  extraTags.add("@needs-review");
85895
- if (oracleWeakening) {
86066
+ if (oracleWeakening && !anrEnv) {
85896
86067
  extraTags.add("@needs-review");
85897
86068
  extraTags.add("@bug-found");
85898
86069
  }
86070
+ if (anrEnv) {
86071
+ extraTags.delete("@bug-found");
86072
+ extraTags.add("@needs-review");
86073
+ }
85899
86074
  if (!heal.passed && NEEDS_REVIEW_VERDICTS.has(heal.verdict))
85900
86075
  extraTags.add("@needs-review");
85901
86076
  let status;
85902
86077
  if (secretLeak)
85903
86078
  status = "FAILED";
85904
- else if (oracleWeakening)
86079
+ else if (oracleWeakening && !anrEnv)
85905
86080
  status = "FAILED";
85906
86081
  else if (isVerifiedFix)
85907
86082
  status = "PASSED";
@@ -85914,10 +86089,11 @@ var require_mobile_heal_postprocess = __commonJS({
85914
86089
  error2 = void 0;
85915
86090
  } else if (secretLeak) {
85916
86091
  error2 = `HEAL_SECRET_LEAK: healed steps contained a literal secret (${secretLeak}); patch dropped, tagged @needs-review`;
85917
- } else if (oracleWeakening) {
86092
+ } else if (oracleWeakening && !anrEnv) {
85918
86093
  error2 = `APP_BUG: heal attempted to drop strong oracle (${oracleWeakening}); patch refused`;
85919
86094
  } else if (isConfigIssue) {
85920
- error2 = `CONFIG_ISSUE: ${heal.healReason ?? args.finalError ?? args.reproduceError ?? "environment/rate-limit blocker"}`;
86095
+ const reason = heal.healReason ?? args.finalError ?? args.reproduceError ?? "environment/rate-limit blocker";
86096
+ error2 = anrEnv ? `CONFIG_ISSUE: ANR / System UI freeze (not a product assertion failure) \u2014 ${reason}` : `CONFIG_ISSUE: ${reason}`;
85921
86097
  } else if (isAppBug) {
85922
86098
  error2 = `APP_BUG: ${heal.healReason ?? args.finalError ?? args.reproduceError ?? "application defect found during heal"}`;
85923
86099
  } else if (heal.passed && heal.verdict === "fix" && !hasFix) {
@@ -86360,6 +86536,7 @@ var require_mobile_heal_loop = __commonJS({
86360
86536
  var failure_triage_js_1 = require_failure_triage();
86361
86537
  var oracle_protection_js_1 = require_oracle_protection();
86362
86538
  var network_oracle_js_1 = require_network_oracle();
86539
+ var anr_env_js_1 = require_anr_env();
86363
86540
  var mobile_observation_js_1 = require_mobile_observation();
86364
86541
  var selector_registry_context_js_1 = require_selector_registry_context();
86365
86542
  var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
@@ -86371,12 +86548,13 @@ Reason over steps + error + tree + registry and return working healed steps. Do
86371
86548
  2. STEP RESULTS: which steps passed before the failure.
86372
86549
  3. TREE: does the current screen match what the failed step expects?
86373
86550
  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).
86551
+ 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.
86374
86552
  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.
86375
86553
  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.
86376
86554
  ## Response \u2014 STRICT JSON, exactly one verdict:
86377
86555
  {"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}
86378
- {"verdict":"app_bug","reason":"app crashed/blank/5xx (NOT rate-limit)"}
86379
- {"verdict":"config_issue","reason":"429/rate limit/lockout/quota"}
86556
+ {"verdict":"app_bug","reason":"product defect with durable oracle evidence (wrong entity, validation bypass, wrong count) \u2014 NOT ANR/System UI"}
86557
+ {"verdict":"config_issue","reason":"429/rate limit/lockout/quota OR ANR/System UI freeze/device unresponsive"}
86380
86558
  {"verdict":"needs_device","reason":"cannot fix from a static tree; need live re-observation"}
86381
86559
  {"verdict":"prereq_unmet","reason":"required entity/state absent for the account"}
86382
86560
  {"verdict":"state_stale","reason":"leftover data blocks a baseline assertion"}
@@ -86387,7 +86565,7 @@ For a fix, return the COMPLETE steps array (not a diff).
86387
86565
  - NEVER remove or downgrade assertStateDelta, captureState, or assertText outcome proof to go green.
86388
86566
  - NEVER replace a durable outcome assert with mere assertVisible of a tab/header.
86389
86567
  - You MAY fix locators, waits, and navigation around those oracles.
86390
- - If the app truly violates the oracle, return verdict "app_bug" instead of stripping the assert.`;
86568
+ - If the app truly violates the oracle on a healthy UI, return verdict "app_bug" instead of stripping the assert.`;
86391
86569
  var DEFAULT_SCRIPT_MAX_ATTEMPTS = 6;
86392
86570
  var RECOVERABLE_MAX = 3;
86393
86571
  var MAX_CONSECUTIVE_INVALID_VERDICTS = 3;
@@ -87355,6 +87533,18 @@ ${args.directive}`);
87355
87533
  providerUnavailable: true
87356
87534
  };
87357
87535
  }
87536
+ if (script.verdict === "app_bug" && (0, anr_env_js_1.isAnrOrSystemUiEnvironmentFailure)(script.healReason ?? ctx.error, ctx.logs)) {
87537
+ return {
87538
+ passed: false,
87539
+ verdict: "config_issue",
87540
+ healedSteps: script.healedSteps,
87541
+ healedAppiumCode: script.healedAppiumCode,
87542
+ healReason: `ANR / System UI freeze (environment) \u2014 ${script.healReason ?? ctx.error ?? "device unresponsive"}`,
87543
+ confidence: script.confidence,
87544
+ extraTags: script.extraTags,
87545
+ removeTags: [...script.removeTags ?? [], "@bug-found"].filter((t, i, a) => a.indexOf(t) === i)
87546
+ };
87547
+ }
87358
87548
  const shouldEscalate = script.verdict === "needs_device" || script.verdict === "fix" && !script.passed;
87359
87549
  if (shouldEscalate && options.runLiveHeal) {
87360
87550
  try {
@@ -355111,7 +355301,7 @@ var require_package4 = __commonJS({
355111
355301
  "package.json"(exports, module) {
355112
355302
  module.exports = {
355113
355303
  name: "@validate.qa/runner",
355114
- version: "1.0.89",
355304
+ version: "1.0.90",
355115
355305
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
355116
355306
  bin: {
355117
355307
  "validate-runner": "dist/cli.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validate.qa/runner",
3
- "version": "1.0.89",
3
+ "version": "1.0.90",
4
4
  "description": "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
5
5
  "bin": {
6
6
  "validate-runner": "dist/cli.js"