@validate.qa/runner 1.0.60 → 1.0.61

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 +273 -10
  2. package/dist/cli.mjs +273 -10
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2236,6 +2236,16 @@ function selectorForValue(value) {
2236
2236
  function isIdentityMobileXpath(value) {
2237
2237
  return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
2238
2238
  }
2239
+ function mobileLocatorBundleHasIdentity(bundle) {
2240
+ if (!bundle)
2241
+ return false;
2242
+ if (nonEmpty(bundle.accessibilityId) || nonEmpty(bundle.resourceId))
2243
+ return true;
2244
+ if (nonEmpty(bundle.text) || nonEmpty(bundle.contentDesc))
2245
+ return true;
2246
+ const xpath = nonEmpty(bundle.xpath);
2247
+ return !!xpath && isIdentityMobileXpath(xpath);
2248
+ }
2239
2249
  function selectorCandidatesForStep(step) {
2240
2250
  const target = step.target;
2241
2251
  if (!target)
@@ -2571,7 +2581,7 @@ function generateMobileTestFromSession(input) {
2571
2581
  const warnings = [];
2572
2582
  const unrunnable = interactionSteps.length - runnableInteractions.length;
2573
2583
  if (unrunnable > 0) {
2574
- warnings.push(`${unrunnable} recorded step(s) could not be turned into a runnable action (missing locator/coordinate) and were kept best-effort.`);
2584
+ warnings.push(`${unrunnable} recorded step(s) could not be turned into a runnable action (missing a replayable identity locator or coordinate) and were kept best-effort.`);
2575
2585
  }
2576
2586
  const locatorCoverage = interactionSteps.filter((step) => step.target?.primary).length;
2577
2587
  const assertionCoverage = steps.filter((step) => step.action === "assertVisible" || step.action === "assertText").length;
@@ -2600,10 +2610,29 @@ function generateMobileTestFromSession(input) {
2600
2610
  warnings
2601
2611
  };
2602
2612
  }
2613
+ function isGateStepResolvable(step) {
2614
+ const metadata = step.metadata;
2615
+ if (metadata?.appContext === "WEBVIEW") {
2616
+ return selectorCandidatesForStep(step).length > 0;
2617
+ }
2618
+ const target = step.target;
2619
+ if (!target)
2620
+ return false;
2621
+ if (mobileLocatorBundleHasIdentity(target.locatorBundle))
2622
+ return true;
2623
+ return [target.primary, ...target.fallbacks ?? []].some((value) => {
2624
+ const trimmed = nonEmpty(value);
2625
+ if (!trimmed || trimmed.startsWith("css="))
2626
+ return false;
2627
+ if (trimmed.startsWith("/") || trimmed.startsWith("(//"))
2628
+ return isIdentityMobileXpath(trimmed);
2629
+ return true;
2630
+ });
2631
+ }
2603
2632
  function isRunnableStep(step) {
2604
2633
  const bounds = step.metadata?.bounds;
2605
2634
  const hasCoord = !!bounds && [bounds.x, bounds.y].every((n) => typeof n === "number" && Number.isFinite(n));
2606
- const hasLocator = !!nonEmpty(step.target?.primary);
2635
+ const hasExecutableLocator = selectorCandidatesForStep(step).length > 0;
2607
2636
  switch (step.action) {
2608
2637
  case "launchApp":
2609
2638
  case "back":
@@ -2613,18 +2642,18 @@ function isRunnableStep(step) {
2613
2642
  return true;
2614
2643
  case "tap":
2615
2644
  case "clear":
2616
- return hasLocator || hasCoord;
2645
+ return hasExecutableLocator || hasCoord;
2617
2646
  case "type": {
2618
2647
  const keyboardSubmitOnly = typeof step.value === "string" && /^[\r\n]+$/.test(step.value);
2619
2648
  if (keyboardSubmitOnly)
2620
2649
  return true;
2621
- return (hasLocator || hasCoord) && !!nonEmpty(step.value ?? void 0);
2650
+ return (hasExecutableLocator || hasCoord) && !!nonEmpty(step.value ?? void 0);
2622
2651
  }
2623
2652
  case "waitForElement":
2624
2653
  case "assertVisible":
2625
- return hasLocator;
2654
+ return isGateStepResolvable(step);
2626
2655
  case "assertText":
2627
- return hasLocator && !!nonEmpty(step.assertion ?? void 0);
2656
+ return isGateStepResolvable(step) && !!nonEmpty(step.assertion ?? void 0);
2628
2657
  default:
2629
2658
  return false;
2630
2659
  }
@@ -5897,6 +5926,7 @@ __export(dist_exports, {
5897
5926
  jwtExpiryMs: () => jwtExpiryMs,
5898
5927
  mediumForMobilePlatform: () => mediumForMobilePlatform,
5899
5928
  mobileCredentialPlaceholder: () => mobileCredentialPlaceholder,
5929
+ mobileLocatorBundleHasIdentity: () => mobileLocatorBundleHasIdentity,
5900
5930
  mobilePlatformFromMedium: () => mobilePlatformFromMedium,
5901
5931
  normalizeControlName: () => normalizeControlName,
5902
5932
  normalizeTtsVoiceId: () => normalizeTtsVoiceId,
@@ -78263,6 +78293,12 @@ ${creds.join(", ")}`);
78263
78293
  A verified login recipe EXISTS for this app \u2014 the harness automatically signs in (and asserts the signed-in state) before every authenticated test. This SUPERSEDES the "all tests start from the landing page and sign in" rule for the non-auth-flow scenarios described here.
78264
78294
  For a scenario that REQUIRES an authenticated session but is NOT itself exercising the authentication / session lifecycle, you MAY assume the test begins ALREADY SIGNED IN at ${startRoute}. Do NOT author sign-in navigation, sign-in clicks, or credential-entry steps for it \u2014 begin at the first real feature action. BUT if the feature is not already on ${startRoute}, DO still include the in-app navigation from the signed-in landing to the feature page.
78265
78295
  Plan these LOGGED-OUT instead (keep their own establishment steps; do NOT assume a session): any scenario that exercises the auth / session / account-access lifecycle itself \u2014 signing in, signing up / registration, password reset, email / OTP / MFA / 2FA verification, SSO or magic-link sign-in, re-authentication, and SIGNING OUT (which starts signed-in and ends signed-out). Also plan logged-out any journey whose first steps must run without a session (e.g. "register a new account, then use it" \u2014 keep the registration steps).`);
78296
+ }
78297
+ if (context.emailVerifyArmed) {
78298
+ sections.push(`## Verification Inbox Available (runtime email capability)
78299
+ This runner CAN read THIS test's verification email at runtime \u2014 a one-time code AND a verification link \u2014 for a fresh throwaway account the harness registers per run. That lets a SIGN-UP email-verification flow RUN TO COMPLETION instead of stopping at the "enter the code" / "check your email" screen (a survey has no inbox, so exploration could only reach that screen). Plan it as a HAPPY_PATH:
78300
+ - Sign-up + email verification: register a brand-new account, RETRIEVE the emailed code or link, and submit/open it to complete verification.
78301
+ Write "retrieve the emailed code / open the emailed link" as an ordinary plain-language step; the generator supplies the runtime mechanism. This is an exception to Rule 1 ONLY for the ACTION of completing the email step \u2014 it does NOT license inventing an outcome. For the durable assertion, Rule 12 still governs: assert only the post-verification result the evidence supports \u2014 a specific observed control, a confirmation message the explorer actually saw, or a STRUCTURAL signal (the verification screen is replaced, the URL leaves the verify route, the login form is no longer shown). If no concrete signed-in signal was observed, assert the observed transition and NAME the missing signal as a coverage gap in reasoning \u2014 do NOT fabricate a Logout/account control exploration never saw (this app may land on a login page or a confirmation page after verifying, not a signed-in dashboard). Keep it LOGGED-OUT with its own registration steps, and still plan the sign-up / verification negative cases the EVIDENCE supports (e.g. an already-registered email, or an invalid / mismatched verification code) as separate scenarios.`);
78266
78302
  }
78267
78303
  sections.push(`## Constraints
78268
78304
  - Test budget: ${context.testBudget} scenarios maximum`);
@@ -94874,6 +94910,7 @@ var require_discover_generator = __commonJS({
94874
94910
  "use strict";
94875
94911
  Object.defineProperty(exports2, "__esModule", { value: true });
94876
94912
  exports2.MIN_BUG_DESCRIPTION_LENGTH = exports2.VALID_PLAYWRIGHT_GETBY_METHODS = exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
94913
+ exports2.scenarioLooksLikeRegistration = scenarioLooksLikeRegistration;
94877
94914
  exports2.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
94878
94915
  exports2.detectScenarioSignals = detectScenarioSignals;
94879
94916
  exports2.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
@@ -94886,6 +94923,7 @@ var require_discover_generator = __commonJS({
94886
94923
  exports2.stripImportDeclarations = stripImportDeclarations;
94887
94924
  exports2.stripStringsAndComments = stripStringsAndComments;
94888
94925
  exports2.gotoBasePathFromBaseUrl = gotoBasePathFromBaseUrl;
94926
+ exports2.repairInboxHelperScalarDestructure = repairInboxHelperScalarDestructure;
94889
94927
  exports2.sanitizeCode = sanitizeCode;
94890
94928
  exports2.validateTests = validateTests;
94891
94929
  exports2.formatEvidenceForLiveGenerate = formatEvidenceForLiveGenerate;
@@ -94932,7 +94970,8 @@ var require_discover_generator = __commonJS({
94932
94970
  var test_inbox_broker_js_1 = require_test_inbox_broker();
94933
94971
  var REGISTER_SCENARIO_RE = /\b(register|registration|sign[\s-]?up|create[\s-]?(a[n]?[\s-]?)?(new[\s-]?)?account|verify[\s-]?(your[\s-]?)?email|email[\s-]?verification|confirm[\s-]?(your[\s-]?)?(email|account)|activate[\s-]?(your[\s-]?)?account|new[\s-]?account)\b/i;
94934
94972
  function scenarioLooksLikeRegistration(scenario) {
94935
- const hay = `${scenario.name ?? ""} ${scenario.goal ?? ""} ${scenario.description ?? ""} ${scenario.entryRoute ?? ""} ${(scenario.targetPages ?? []).join(" ")}`;
94973
+ const stepText = (scenario.steps ?? []).map((s) => `${s?.action ?? ""} ${s?.target ?? ""}`).join(" ");
94974
+ const hay = `${scenario.name ?? ""} ${scenario.goal ?? ""} ${scenario.description ?? ""} ${scenario.entryRoute ?? ""} ${(scenario.targetPages ?? []).join(" ")} ${stepText}`;
94936
94975
  return REGISTER_SCENARIO_RE.test(hay);
94937
94976
  }
94938
94977
  function buildEmailVerifyGenBlock(genEmail, genPassword) {
@@ -94955,6 +94994,7 @@ EMIT (the code you put in finish_generate) \u2014 do NOT hardcode the throwaway
94955
94994
  - code flow: \`const code = await __vqaWaitForEmail('code');\` then type \`code\` into the verification field.
94956
94995
  - link flow: \`const link = await __vqaWaitForEmail('link');\` then \`await page.goto(link);\`.
94957
94996
  - if unsure: \`const email = await __vqaWaitForEmail();\` returns \`{ code, link, subject, from }\`.
94997
+ - IMPORTANT: \`__vqaWaitForEmail('code')\` returns the code STRING (and \`('link')\` the link string) \u2014 assign it directly with \`const code = \u2026\`. Do NOT destructure the scalar form (\`const { code } = await __vqaWaitForEmail('code')\` binds \`undefined\`, so \`code.split(...)\` then crashes). ONLY the no-arg \`__vqaWaitForEmail()\` returns an object you can destructure.
94958
94998
  - Tag the test \`@email-verify\`. Emit EXACTLY ONE test() in this file (one registration per run).
94959
94999
  - End with a DURABLE assertion of the post-verification signed-in state (e.g. a Logout control, an account/dashboard landmark) in your OWN test body \u2014 the helper call is not itself a proof.`;
94960
95000
  }
@@ -96054,6 +96094,200 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
96054
96094
  return void 0;
96055
96095
  }
96056
96096
  }
96097
+ function literalPositionMask(code) {
96098
+ const n = code.length;
96099
+ const mask = new Array(n).fill(false);
96100
+ const REGEX_PRECEDING = /[(,=;{}\[!&|?:+\-*%~^<>]$/;
96101
+ const REGEX_PRECEDING_KEYWORD = /\b(?:return|typeof|void|delete|in|of|new|instanceof|throw|await|yield|case)\s*$/;
96102
+ let i = 0;
96103
+ let codeSoFar = "";
96104
+ const isRegexContext = () => {
96105
+ const trimmed = codeSoFar.replace(/\s+$/, "");
96106
+ if (trimmed.length === 0)
96107
+ return true;
96108
+ if (REGEX_PRECEDING.test(trimmed))
96109
+ return true;
96110
+ if (REGEX_PRECEDING_KEYWORD.test(trimmed))
96111
+ return true;
96112
+ if (trimmed.endsWith("=>"))
96113
+ return true;
96114
+ return false;
96115
+ };
96116
+ const scanRegex = () => {
96117
+ i += 1;
96118
+ let inClass = false;
96119
+ while (i < n) {
96120
+ if (code[i] === "\\") {
96121
+ mask[i] = true;
96122
+ if (i + 1 < n)
96123
+ mask[i + 1] = true;
96124
+ i += 2;
96125
+ continue;
96126
+ }
96127
+ if (code[i] === "[") {
96128
+ inClass = true;
96129
+ mask[i] = true;
96130
+ i += 1;
96131
+ continue;
96132
+ }
96133
+ if (code[i] === "]") {
96134
+ inClass = false;
96135
+ mask[i] = true;
96136
+ i += 1;
96137
+ continue;
96138
+ }
96139
+ if (code[i] === "/" && !inClass) {
96140
+ i += 1;
96141
+ break;
96142
+ }
96143
+ if (code[i] === "\n")
96144
+ break;
96145
+ mask[i] = true;
96146
+ i += 1;
96147
+ }
96148
+ while (i < n && /[gimsuydv]/.test(code[i]))
96149
+ i += 1;
96150
+ };
96151
+ const scanQuoted = (quote) => {
96152
+ i += 1;
96153
+ while (i < n) {
96154
+ if (code[i] === "\\") {
96155
+ if (code[i + 1] === "\r" && code[i + 2] === "\n") {
96156
+ mask[i] = true;
96157
+ mask[i + 1] = true;
96158
+ mask[i + 2] = true;
96159
+ i += 3;
96160
+ continue;
96161
+ }
96162
+ mask[i] = true;
96163
+ if (i + 1 < n)
96164
+ mask[i + 1] = true;
96165
+ i += 2;
96166
+ continue;
96167
+ }
96168
+ if (code[i] === quote) {
96169
+ i += 1;
96170
+ break;
96171
+ }
96172
+ if (code[i] === "\n")
96173
+ break;
96174
+ mask[i] = true;
96175
+ i += 1;
96176
+ }
96177
+ };
96178
+ const scanTemplate = () => {
96179
+ while (i < n) {
96180
+ if (code[i] === "\\") {
96181
+ mask[i] = true;
96182
+ if (i + 1 < n)
96183
+ mask[i + 1] = true;
96184
+ i += 2;
96185
+ continue;
96186
+ }
96187
+ if (code[i] === "`") {
96188
+ i += 1;
96189
+ break;
96190
+ }
96191
+ if (code[i] === "$" && code[i + 1] === "{") {
96192
+ i += 2;
96193
+ const savedCodeSoFar = codeSoFar;
96194
+ codeSoFar = "";
96195
+ scanCode(true);
96196
+ codeSoFar = savedCodeSoFar;
96197
+ continue;
96198
+ }
96199
+ mask[i] = true;
96200
+ i += 1;
96201
+ }
96202
+ };
96203
+ function scanCode(stopAtCloseBrace) {
96204
+ let depth = 0;
96205
+ while (i < n) {
96206
+ const c = code[i];
96207
+ const next = i + 1 < n ? code[i + 1] : "";
96208
+ if (stopAtCloseBrace) {
96209
+ if (c === "}" && depth === 0) {
96210
+ i += 1;
96211
+ return;
96212
+ }
96213
+ if (c === "{") {
96214
+ depth += 1;
96215
+ codeSoFar += c;
96216
+ i += 1;
96217
+ continue;
96218
+ }
96219
+ if (c === "}") {
96220
+ depth -= 1;
96221
+ codeSoFar += c;
96222
+ i += 1;
96223
+ continue;
96224
+ }
96225
+ }
96226
+ if (c === "/" && next === "*") {
96227
+ const end = code.indexOf("*/", i + 2);
96228
+ const stop = end < 0 ? n : end + 2;
96229
+ for (let j = i; j < stop; j++)
96230
+ mask[j] = true;
96231
+ i = stop;
96232
+ continue;
96233
+ }
96234
+ if (c === "/" && next === "/") {
96235
+ const end = code.indexOf("\n", i + 2);
96236
+ const stop = end < 0 ? n : end;
96237
+ for (let j = i; j < stop; j++)
96238
+ mask[j] = true;
96239
+ i = stop;
96240
+ continue;
96241
+ }
96242
+ if (c === "/" && isRegexContext()) {
96243
+ scanRegex();
96244
+ codeSoFar += "x";
96245
+ continue;
96246
+ }
96247
+ if (c === "`") {
96248
+ i += 1;
96249
+ scanTemplate();
96250
+ codeSoFar += "x";
96251
+ continue;
96252
+ }
96253
+ if (c === '"' || c === "'") {
96254
+ scanQuoted(c);
96255
+ codeSoFar += "x";
96256
+ continue;
96257
+ }
96258
+ codeSoFar += c;
96259
+ i += 1;
96260
+ }
96261
+ }
96262
+ scanCode(false);
96263
+ return mask;
96264
+ }
96265
+ function atLineStart(code, start) {
96266
+ let j = start - 1;
96267
+ while (j >= 0 && (code[j] === " " || code[j] === " "))
96268
+ j--;
96269
+ return j < 0 || code[j] === "\n" || code[j] === "\r";
96270
+ }
96271
+ function repairInboxHelperScalarDestructure(code) {
96272
+ if (!code || code.indexOf("__vqaWaitForEmail") === -1)
96273
+ return code;
96274
+ const inLiteral = literalPositionMask(code);
96275
+ const re = /\b(const|let|var)\s*\{\s*([A-Za-z_$][\w$]*)\s*(?::\s*([A-Za-z_$][\w$]*)\s*)?\}\s*=\s*await\s+__vqaWaitForEmail\s*\(\s*(['"`])(code|link)\4\s*\)/g;
96276
+ let out = "";
96277
+ let last = 0;
96278
+ let m;
96279
+ while ((m = re.exec(code)) !== null) {
96280
+ const [full, decl, prop, alias, quote, kind] = m;
96281
+ const start = m.index;
96282
+ if (inLiteral[start] || !atLineStart(code, start) || prop !== kind)
96283
+ continue;
96284
+ out += code.slice(last, start);
96285
+ out += `${decl} ${alias || prop} = await __vqaWaitForEmail(${quote}${kind}${quote})`;
96286
+ last = start + full.length;
96287
+ }
96288
+ out += code.slice(last);
96289
+ return out;
96290
+ }
96057
96291
  function sanitizeCode(code, options) {
96058
96292
  let result = code;
96059
96293
  const isNetworkIdleWaitLine = (line) => /^\s*await page\.waitForLoadState\((['"])networkidle\1\);\s*$/.test(line);
@@ -96144,6 +96378,7 @@ ${testOpen}`;
96144
96378
  if (options?.authStamp) {
96145
96379
  result = (0, auth_recipe_js_1.upsertAuthBlock)(result, options.authStamp);
96146
96380
  }
96381
+ result = repairInboxHelperScalarDestructure(result);
96147
96382
  try {
96148
96383
  result = (0, inbox_stamp_js_1.upsertInboxBlock)(result);
96149
96384
  } catch {
@@ -334605,6 +334840,8 @@ var require_discover_agent = __commonJS({
334605
334840
  var discover_planner_js_1 = require_discover_planner();
334606
334841
  var discover_generator_js_1 = require_discover_generator();
334607
334842
  var discover_ai_planner_js_1 = require_discover_ai_planner();
334843
+ var discovery_lint_flags_js_1 = require_discovery_lint_flags();
334844
+ var verification_inbox_js_1 = require_verification_inbox();
334608
334845
  var sitemap_builder_js_1 = require_sitemap_builder();
334609
334846
  Object.defineProperty(exports2, "buildInMemorySiteMap", { enumerable: true, get: function() {
334610
334847
  return sitemap_builder_js_1.buildInMemorySiteMap;
@@ -334617,6 +334854,13 @@ var require_discover_agent = __commonJS({
334617
334854
  var agentic_cli_constants_js_1 = require_agentic_cli_constants();
334618
334855
  var canvas_detector_js_1 = require_canvas_detector();
334619
334856
  var auth_recipe_js_1 = require_auth_recipe();
334857
+ function plannerEmailVerifyArmed(ctx) {
334858
+ if (!(0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)())
334859
+ return false;
334860
+ if (ctx.variableContext?.allTestVariables?.DISCOVERY_SHOULD_REGISTER !== "true")
334861
+ return false;
334862
+ return (0, verification_inbox_js_1.isVerificationInboxConfigured)();
334863
+ }
334620
334864
  async function runCanvasPreflight(ctx, sharedBrowser, log2) {
334621
334865
  const cached = ctx.appRenderType ?? null;
334622
334866
  const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
@@ -335265,6 +335509,9 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
335265
335509
  // instead of authoring landing→login steps (which generate would override).
335266
335510
  authRecipeAvailable: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && !!ctx.authRecipe,
335267
335511
  authRedirectRoute: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) ? ctx.authRecipe?.redirectRoute ?? void 0 : void 0,
335512
+ // Email-verify capability — only for scenario-producing modes; a survey emits
335513
+ // featureGroups (no scenarios), so keep its prompt free of the round-trip block.
335514
+ emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
335268
335515
  testBudget
335269
335516
  }, {
335270
335517
  modelOverride: ctx.planModel ?? ctx.generateModel,
@@ -335849,6 +336096,12 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
335849
336096
  // authenticated state when a verified login recipe is on file for this project.
335850
336097
  authRecipeAvailable: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && !!ctx.authRecipe,
335851
336098
  authRedirectRoute: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) ? ctx.authRecipe?.redirectRoute ?? void 0 : void 0,
336099
+ // Email-verify capability: when armed, let the planner plan the full
336100
+ // register→verify and forgot-password→reset round trips to completion
336101
+ // (see the planner's "## Verification Inbox Available" block). Armed on the
336102
+ // SAME project-level predicate the generator uses (flag + register intent +
336103
+ // configured inbox), so the planner never promises what generation can't do.
336104
+ emailVerifyArmed: plannerEmailVerifyArmed(ctx),
335852
336105
  testBudget: featureTestBudget
335853
336106
  }, {
335854
336107
  modelOverride: ctx.planModel ?? ctx.generateModel,
@@ -350474,7 +350727,7 @@ var require_package4 = __commonJS({
350474
350727
  "package.json"(exports2, module2) {
350475
350728
  module2.exports = {
350476
350729
  name: "@validate.qa/runner",
350477
- version: "1.0.60",
350730
+ version: "1.0.61",
350478
350731
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
350479
350732
  bin: {
350480
350733
  "validate-runner": "dist/cli.js"
@@ -355554,7 +355807,13 @@ async function handleHealth(_req, res) {
355554
355807
  },
355555
355808
  devices,
355556
355809
  capabilities,
355557
- activeSession: state.appiumSessionId
355810
+ activeSession: state.appiumSessionId,
355811
+ // THIS runner's Appium is executing mobile (APPIUM) test run(s) right now.
355812
+ // The web pre-flight reads this to warn before /bridge/session/start would
355813
+ // 409 — it must never be inferred from server-side global run counts, which
355814
+ // include unrelated projects and runners.
355815
+ executorBusy: executorBusyCount > 0,
355816
+ recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording"
355558
355817
  });
355559
355818
  }
355560
355819
  function realDeviceCapabilities2(platform3) {
@@ -355629,7 +355888,11 @@ function validateSessionStartBody(body) {
355629
355888
  }
355630
355889
  async function handleDevices(_req, res) {
355631
355890
  const devices = discoverAllDevices({ includeApps: true });
355632
- sendJSON(res, 200, { devices });
355891
+ sendJSON(res, 200, {
355892
+ devices,
355893
+ executorBusy: executorBusyCount > 0,
355894
+ recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording"
355895
+ });
355633
355896
  }
355634
355897
  async function handleSessionStart(req, res) {
355635
355898
  const body = await parseBody(req);
package/dist/cli.mjs CHANGED
@@ -2241,6 +2241,16 @@ function selectorForValue(value) {
2241
2241
  function isIdentityMobileXpath(value) {
2242
2242
  return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
2243
2243
  }
2244
+ function mobileLocatorBundleHasIdentity(bundle) {
2245
+ if (!bundle)
2246
+ return false;
2247
+ if (nonEmpty(bundle.accessibilityId) || nonEmpty(bundle.resourceId))
2248
+ return true;
2249
+ if (nonEmpty(bundle.text) || nonEmpty(bundle.contentDesc))
2250
+ return true;
2251
+ const xpath = nonEmpty(bundle.xpath);
2252
+ return !!xpath && isIdentityMobileXpath(xpath);
2253
+ }
2244
2254
  function selectorCandidatesForStep(step) {
2245
2255
  const target = step.target;
2246
2256
  if (!target)
@@ -2576,7 +2586,7 @@ function generateMobileTestFromSession(input) {
2576
2586
  const warnings = [];
2577
2587
  const unrunnable = interactionSteps.length - runnableInteractions.length;
2578
2588
  if (unrunnable > 0) {
2579
- warnings.push(`${unrunnable} recorded step(s) could not be turned into a runnable action (missing locator/coordinate) and were kept best-effort.`);
2589
+ warnings.push(`${unrunnable} recorded step(s) could not be turned into a runnable action (missing a replayable identity locator or coordinate) and were kept best-effort.`);
2580
2590
  }
2581
2591
  const locatorCoverage = interactionSteps.filter((step) => step.target?.primary).length;
2582
2592
  const assertionCoverage = steps.filter((step) => step.action === "assertVisible" || step.action === "assertText").length;
@@ -2605,10 +2615,29 @@ function generateMobileTestFromSession(input) {
2605
2615
  warnings
2606
2616
  };
2607
2617
  }
2618
+ function isGateStepResolvable(step) {
2619
+ const metadata = step.metadata;
2620
+ if (metadata?.appContext === "WEBVIEW") {
2621
+ return selectorCandidatesForStep(step).length > 0;
2622
+ }
2623
+ const target = step.target;
2624
+ if (!target)
2625
+ return false;
2626
+ if (mobileLocatorBundleHasIdentity(target.locatorBundle))
2627
+ return true;
2628
+ return [target.primary, ...target.fallbacks ?? []].some((value) => {
2629
+ const trimmed = nonEmpty(value);
2630
+ if (!trimmed || trimmed.startsWith("css="))
2631
+ return false;
2632
+ if (trimmed.startsWith("/") || trimmed.startsWith("(//"))
2633
+ return isIdentityMobileXpath(trimmed);
2634
+ return true;
2635
+ });
2636
+ }
2608
2637
  function isRunnableStep(step) {
2609
2638
  const bounds = step.metadata?.bounds;
2610
2639
  const hasCoord = !!bounds && [bounds.x, bounds.y].every((n) => typeof n === "number" && Number.isFinite(n));
2611
- const hasLocator = !!nonEmpty(step.target?.primary);
2640
+ const hasExecutableLocator = selectorCandidatesForStep(step).length > 0;
2612
2641
  switch (step.action) {
2613
2642
  case "launchApp":
2614
2643
  case "back":
@@ -2618,18 +2647,18 @@ function isRunnableStep(step) {
2618
2647
  return true;
2619
2648
  case "tap":
2620
2649
  case "clear":
2621
- return hasLocator || hasCoord;
2650
+ return hasExecutableLocator || hasCoord;
2622
2651
  case "type": {
2623
2652
  const keyboardSubmitOnly = typeof step.value === "string" && /^[\r\n]+$/.test(step.value);
2624
2653
  if (keyboardSubmitOnly)
2625
2654
  return true;
2626
- return (hasLocator || hasCoord) && !!nonEmpty(step.value ?? void 0);
2655
+ return (hasExecutableLocator || hasCoord) && !!nonEmpty(step.value ?? void 0);
2627
2656
  }
2628
2657
  case "waitForElement":
2629
2658
  case "assertVisible":
2630
- return hasLocator;
2659
+ return isGateStepResolvable(step);
2631
2660
  case "assertText":
2632
- return hasLocator && !!nonEmpty(step.assertion ?? void 0);
2661
+ return isGateStepResolvable(step) && !!nonEmpty(step.assertion ?? void 0);
2633
2662
  default:
2634
2663
  return false;
2635
2664
  }
@@ -5902,6 +5931,7 @@ __export(dist_exports, {
5902
5931
  jwtExpiryMs: () => jwtExpiryMs,
5903
5932
  mediumForMobilePlatform: () => mediumForMobilePlatform,
5904
5933
  mobileCredentialPlaceholder: () => mobileCredentialPlaceholder,
5934
+ mobileLocatorBundleHasIdentity: () => mobileLocatorBundleHasIdentity,
5905
5935
  mobilePlatformFromMedium: () => mobilePlatformFromMedium,
5906
5936
  normalizeControlName: () => normalizeControlName,
5907
5937
  normalizeTtsVoiceId: () => normalizeTtsVoiceId,
@@ -78268,6 +78298,12 @@ ${creds.join(", ")}`);
78268
78298
  A verified login recipe EXISTS for this app \u2014 the harness automatically signs in (and asserts the signed-in state) before every authenticated test. This SUPERSEDES the "all tests start from the landing page and sign in" rule for the non-auth-flow scenarios described here.
78269
78299
  For a scenario that REQUIRES an authenticated session but is NOT itself exercising the authentication / session lifecycle, you MAY assume the test begins ALREADY SIGNED IN at ${startRoute}. Do NOT author sign-in navigation, sign-in clicks, or credential-entry steps for it \u2014 begin at the first real feature action. BUT if the feature is not already on ${startRoute}, DO still include the in-app navigation from the signed-in landing to the feature page.
78270
78300
  Plan these LOGGED-OUT instead (keep their own establishment steps; do NOT assume a session): any scenario that exercises the auth / session / account-access lifecycle itself \u2014 signing in, signing up / registration, password reset, email / OTP / MFA / 2FA verification, SSO or magic-link sign-in, re-authentication, and SIGNING OUT (which starts signed-in and ends signed-out). Also plan logged-out any journey whose first steps must run without a session (e.g. "register a new account, then use it" \u2014 keep the registration steps).`);
78301
+ }
78302
+ if (context.emailVerifyArmed) {
78303
+ sections.push(`## Verification Inbox Available (runtime email capability)
78304
+ This runner CAN read THIS test's verification email at runtime \u2014 a one-time code AND a verification link \u2014 for a fresh throwaway account the harness registers per run. That lets a SIGN-UP email-verification flow RUN TO COMPLETION instead of stopping at the "enter the code" / "check your email" screen (a survey has no inbox, so exploration could only reach that screen). Plan it as a HAPPY_PATH:
78305
+ - Sign-up + email verification: register a brand-new account, RETRIEVE the emailed code or link, and submit/open it to complete verification.
78306
+ Write "retrieve the emailed code / open the emailed link" as an ordinary plain-language step; the generator supplies the runtime mechanism. This is an exception to Rule 1 ONLY for the ACTION of completing the email step \u2014 it does NOT license inventing an outcome. For the durable assertion, Rule 12 still governs: assert only the post-verification result the evidence supports \u2014 a specific observed control, a confirmation message the explorer actually saw, or a STRUCTURAL signal (the verification screen is replaced, the URL leaves the verify route, the login form is no longer shown). If no concrete signed-in signal was observed, assert the observed transition and NAME the missing signal as a coverage gap in reasoning \u2014 do NOT fabricate a Logout/account control exploration never saw (this app may land on a login page or a confirmation page after verifying, not a signed-in dashboard). Keep it LOGGED-OUT with its own registration steps, and still plan the sign-up / verification negative cases the EVIDENCE supports (e.g. an already-registered email, or an invalid / mismatched verification code) as separate scenarios.`);
78271
78307
  }
78272
78308
  sections.push(`## Constraints
78273
78309
  - Test budget: ${context.testBudget} scenarios maximum`);
@@ -94879,6 +94915,7 @@ var require_discover_generator = __commonJS({
94879
94915
  "use strict";
94880
94916
  Object.defineProperty(exports, "__esModule", { value: true });
94881
94917
  exports.MIN_BUG_DESCRIPTION_LENGTH = exports.VALID_PLAYWRIGHT_GETBY_METHODS = exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
94918
+ exports.scenarioLooksLikeRegistration = scenarioLooksLikeRegistration;
94882
94919
  exports.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
94883
94920
  exports.detectScenarioSignals = detectScenarioSignals;
94884
94921
  exports.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
@@ -94891,6 +94928,7 @@ var require_discover_generator = __commonJS({
94891
94928
  exports.stripImportDeclarations = stripImportDeclarations;
94892
94929
  exports.stripStringsAndComments = stripStringsAndComments;
94893
94930
  exports.gotoBasePathFromBaseUrl = gotoBasePathFromBaseUrl;
94931
+ exports.repairInboxHelperScalarDestructure = repairInboxHelperScalarDestructure;
94894
94932
  exports.sanitizeCode = sanitizeCode;
94895
94933
  exports.validateTests = validateTests;
94896
94934
  exports.formatEvidenceForLiveGenerate = formatEvidenceForLiveGenerate;
@@ -94937,7 +94975,8 @@ var require_discover_generator = __commonJS({
94937
94975
  var test_inbox_broker_js_1 = require_test_inbox_broker();
94938
94976
  var REGISTER_SCENARIO_RE = /\b(register|registration|sign[\s-]?up|create[\s-]?(a[n]?[\s-]?)?(new[\s-]?)?account|verify[\s-]?(your[\s-]?)?email|email[\s-]?verification|confirm[\s-]?(your[\s-]?)?(email|account)|activate[\s-]?(your[\s-]?)?account|new[\s-]?account)\b/i;
94939
94977
  function scenarioLooksLikeRegistration(scenario) {
94940
- const hay = `${scenario.name ?? ""} ${scenario.goal ?? ""} ${scenario.description ?? ""} ${scenario.entryRoute ?? ""} ${(scenario.targetPages ?? []).join(" ")}`;
94978
+ const stepText = (scenario.steps ?? []).map((s) => `${s?.action ?? ""} ${s?.target ?? ""}`).join(" ");
94979
+ const hay = `${scenario.name ?? ""} ${scenario.goal ?? ""} ${scenario.description ?? ""} ${scenario.entryRoute ?? ""} ${(scenario.targetPages ?? []).join(" ")} ${stepText}`;
94941
94980
  return REGISTER_SCENARIO_RE.test(hay);
94942
94981
  }
94943
94982
  function buildEmailVerifyGenBlock(genEmail, genPassword) {
@@ -94960,6 +94999,7 @@ EMIT (the code you put in finish_generate) \u2014 do NOT hardcode the throwaway
94960
94999
  - code flow: \`const code = await __vqaWaitForEmail('code');\` then type \`code\` into the verification field.
94961
95000
  - link flow: \`const link = await __vqaWaitForEmail('link');\` then \`await page.goto(link);\`.
94962
95001
  - if unsure: \`const email = await __vqaWaitForEmail();\` returns \`{ code, link, subject, from }\`.
95002
+ - IMPORTANT: \`__vqaWaitForEmail('code')\` returns the code STRING (and \`('link')\` the link string) \u2014 assign it directly with \`const code = \u2026\`. Do NOT destructure the scalar form (\`const { code } = await __vqaWaitForEmail('code')\` binds \`undefined\`, so \`code.split(...)\` then crashes). ONLY the no-arg \`__vqaWaitForEmail()\` returns an object you can destructure.
94963
95003
  - Tag the test \`@email-verify\`. Emit EXACTLY ONE test() in this file (one registration per run).
94964
95004
  - End with a DURABLE assertion of the post-verification signed-in state (e.g. a Logout control, an account/dashboard landmark) in your OWN test body \u2014 the helper call is not itself a proof.`;
94965
95005
  }
@@ -96059,6 +96099,200 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
96059
96099
  return void 0;
96060
96100
  }
96061
96101
  }
96102
+ function literalPositionMask(code) {
96103
+ const n = code.length;
96104
+ const mask = new Array(n).fill(false);
96105
+ const REGEX_PRECEDING = /[(,=;{}\[!&|?:+\-*%~^<>]$/;
96106
+ const REGEX_PRECEDING_KEYWORD = /\b(?:return|typeof|void|delete|in|of|new|instanceof|throw|await|yield|case)\s*$/;
96107
+ let i = 0;
96108
+ let codeSoFar = "";
96109
+ const isRegexContext = () => {
96110
+ const trimmed = codeSoFar.replace(/\s+$/, "");
96111
+ if (trimmed.length === 0)
96112
+ return true;
96113
+ if (REGEX_PRECEDING.test(trimmed))
96114
+ return true;
96115
+ if (REGEX_PRECEDING_KEYWORD.test(trimmed))
96116
+ return true;
96117
+ if (trimmed.endsWith("=>"))
96118
+ return true;
96119
+ return false;
96120
+ };
96121
+ const scanRegex = () => {
96122
+ i += 1;
96123
+ let inClass = false;
96124
+ while (i < n) {
96125
+ if (code[i] === "\\") {
96126
+ mask[i] = true;
96127
+ if (i + 1 < n)
96128
+ mask[i + 1] = true;
96129
+ i += 2;
96130
+ continue;
96131
+ }
96132
+ if (code[i] === "[") {
96133
+ inClass = true;
96134
+ mask[i] = true;
96135
+ i += 1;
96136
+ continue;
96137
+ }
96138
+ if (code[i] === "]") {
96139
+ inClass = false;
96140
+ mask[i] = true;
96141
+ i += 1;
96142
+ continue;
96143
+ }
96144
+ if (code[i] === "/" && !inClass) {
96145
+ i += 1;
96146
+ break;
96147
+ }
96148
+ if (code[i] === "\n")
96149
+ break;
96150
+ mask[i] = true;
96151
+ i += 1;
96152
+ }
96153
+ while (i < n && /[gimsuydv]/.test(code[i]))
96154
+ i += 1;
96155
+ };
96156
+ const scanQuoted = (quote) => {
96157
+ i += 1;
96158
+ while (i < n) {
96159
+ if (code[i] === "\\") {
96160
+ if (code[i + 1] === "\r" && code[i + 2] === "\n") {
96161
+ mask[i] = true;
96162
+ mask[i + 1] = true;
96163
+ mask[i + 2] = true;
96164
+ i += 3;
96165
+ continue;
96166
+ }
96167
+ mask[i] = true;
96168
+ if (i + 1 < n)
96169
+ mask[i + 1] = true;
96170
+ i += 2;
96171
+ continue;
96172
+ }
96173
+ if (code[i] === quote) {
96174
+ i += 1;
96175
+ break;
96176
+ }
96177
+ if (code[i] === "\n")
96178
+ break;
96179
+ mask[i] = true;
96180
+ i += 1;
96181
+ }
96182
+ };
96183
+ const scanTemplate = () => {
96184
+ while (i < n) {
96185
+ if (code[i] === "\\") {
96186
+ mask[i] = true;
96187
+ if (i + 1 < n)
96188
+ mask[i + 1] = true;
96189
+ i += 2;
96190
+ continue;
96191
+ }
96192
+ if (code[i] === "`") {
96193
+ i += 1;
96194
+ break;
96195
+ }
96196
+ if (code[i] === "$" && code[i + 1] === "{") {
96197
+ i += 2;
96198
+ const savedCodeSoFar = codeSoFar;
96199
+ codeSoFar = "";
96200
+ scanCode(true);
96201
+ codeSoFar = savedCodeSoFar;
96202
+ continue;
96203
+ }
96204
+ mask[i] = true;
96205
+ i += 1;
96206
+ }
96207
+ };
96208
+ function scanCode(stopAtCloseBrace) {
96209
+ let depth = 0;
96210
+ while (i < n) {
96211
+ const c = code[i];
96212
+ const next = i + 1 < n ? code[i + 1] : "";
96213
+ if (stopAtCloseBrace) {
96214
+ if (c === "}" && depth === 0) {
96215
+ i += 1;
96216
+ return;
96217
+ }
96218
+ if (c === "{") {
96219
+ depth += 1;
96220
+ codeSoFar += c;
96221
+ i += 1;
96222
+ continue;
96223
+ }
96224
+ if (c === "}") {
96225
+ depth -= 1;
96226
+ codeSoFar += c;
96227
+ i += 1;
96228
+ continue;
96229
+ }
96230
+ }
96231
+ if (c === "/" && next === "*") {
96232
+ const end = code.indexOf("*/", i + 2);
96233
+ const stop = end < 0 ? n : end + 2;
96234
+ for (let j = i; j < stop; j++)
96235
+ mask[j] = true;
96236
+ i = stop;
96237
+ continue;
96238
+ }
96239
+ if (c === "/" && next === "/") {
96240
+ const end = code.indexOf("\n", i + 2);
96241
+ const stop = end < 0 ? n : end;
96242
+ for (let j = i; j < stop; j++)
96243
+ mask[j] = true;
96244
+ i = stop;
96245
+ continue;
96246
+ }
96247
+ if (c === "/" && isRegexContext()) {
96248
+ scanRegex();
96249
+ codeSoFar += "x";
96250
+ continue;
96251
+ }
96252
+ if (c === "`") {
96253
+ i += 1;
96254
+ scanTemplate();
96255
+ codeSoFar += "x";
96256
+ continue;
96257
+ }
96258
+ if (c === '"' || c === "'") {
96259
+ scanQuoted(c);
96260
+ codeSoFar += "x";
96261
+ continue;
96262
+ }
96263
+ codeSoFar += c;
96264
+ i += 1;
96265
+ }
96266
+ }
96267
+ scanCode(false);
96268
+ return mask;
96269
+ }
96270
+ function atLineStart(code, start) {
96271
+ let j = start - 1;
96272
+ while (j >= 0 && (code[j] === " " || code[j] === " "))
96273
+ j--;
96274
+ return j < 0 || code[j] === "\n" || code[j] === "\r";
96275
+ }
96276
+ function repairInboxHelperScalarDestructure(code) {
96277
+ if (!code || code.indexOf("__vqaWaitForEmail") === -1)
96278
+ return code;
96279
+ const inLiteral = literalPositionMask(code);
96280
+ const re = /\b(const|let|var)\s*\{\s*([A-Za-z_$][\w$]*)\s*(?::\s*([A-Za-z_$][\w$]*)\s*)?\}\s*=\s*await\s+__vqaWaitForEmail\s*\(\s*(['"`])(code|link)\4\s*\)/g;
96281
+ let out = "";
96282
+ let last = 0;
96283
+ let m;
96284
+ while ((m = re.exec(code)) !== null) {
96285
+ const [full, decl, prop, alias, quote, kind] = m;
96286
+ const start = m.index;
96287
+ if (inLiteral[start] || !atLineStart(code, start) || prop !== kind)
96288
+ continue;
96289
+ out += code.slice(last, start);
96290
+ out += `${decl} ${alias || prop} = await __vqaWaitForEmail(${quote}${kind}${quote})`;
96291
+ last = start + full.length;
96292
+ }
96293
+ out += code.slice(last);
96294
+ return out;
96295
+ }
96062
96296
  function sanitizeCode(code, options) {
96063
96297
  let result = code;
96064
96298
  const isNetworkIdleWaitLine = (line) => /^\s*await page\.waitForLoadState\((['"])networkidle\1\);\s*$/.test(line);
@@ -96149,6 +96383,7 @@ ${testOpen}`;
96149
96383
  if (options?.authStamp) {
96150
96384
  result = (0, auth_recipe_js_1.upsertAuthBlock)(result, options.authStamp);
96151
96385
  }
96386
+ result = repairInboxHelperScalarDestructure(result);
96152
96387
  try {
96153
96388
  result = (0, inbox_stamp_js_1.upsertInboxBlock)(result);
96154
96389
  } catch {
@@ -334610,6 +334845,8 @@ var require_discover_agent = __commonJS({
334610
334845
  var discover_planner_js_1 = require_discover_planner();
334611
334846
  var discover_generator_js_1 = require_discover_generator();
334612
334847
  var discover_ai_planner_js_1 = require_discover_ai_planner();
334848
+ var discovery_lint_flags_js_1 = require_discovery_lint_flags();
334849
+ var verification_inbox_js_1 = require_verification_inbox();
334613
334850
  var sitemap_builder_js_1 = require_sitemap_builder();
334614
334851
  Object.defineProperty(exports, "buildInMemorySiteMap", { enumerable: true, get: function() {
334615
334852
  return sitemap_builder_js_1.buildInMemorySiteMap;
@@ -334622,6 +334859,13 @@ var require_discover_agent = __commonJS({
334622
334859
  var agentic_cli_constants_js_1 = require_agentic_cli_constants();
334623
334860
  var canvas_detector_js_1 = require_canvas_detector();
334624
334861
  var auth_recipe_js_1 = require_auth_recipe();
334862
+ function plannerEmailVerifyArmed(ctx) {
334863
+ if (!(0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)())
334864
+ return false;
334865
+ if (ctx.variableContext?.allTestVariables?.DISCOVERY_SHOULD_REGISTER !== "true")
334866
+ return false;
334867
+ return (0, verification_inbox_js_1.isVerificationInboxConfigured)();
334868
+ }
334625
334869
  async function runCanvasPreflight(ctx, sharedBrowser, log2) {
334626
334870
  const cached = ctx.appRenderType ?? null;
334627
334871
  const engine = (0, agentic_cli_constants_js_1.getAgenticEngine)(ctx.exploreModel);
@@ -335270,6 +335514,9 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
335270
335514
  // instead of authoring landing→login steps (which generate would override).
335271
335515
  authRecipeAvailable: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && !!ctx.authRecipe,
335272
335516
  authRedirectRoute: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) ? ctx.authRecipe?.redirectRoute ?? void 0 : void 0,
335517
+ // Email-verify capability — only for scenario-producing modes; a survey emits
335518
+ // featureGroups (no scenarios), so keep its prompt free of the round-trip block.
335519
+ emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
335273
335520
  testBudget
335274
335521
  }, {
335275
335522
  modelOverride: ctx.planModel ?? ctx.generateModel,
@@ -335854,6 +336101,12 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
335854
336101
  // authenticated state when a verified login recipe is on file for this project.
335855
336102
  authRecipeAvailable: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) && !!ctx.authRecipe,
335856
336103
  authRedirectRoute: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) ? ctx.authRecipe?.redirectRoute ?? void 0 : void 0,
336104
+ // Email-verify capability: when armed, let the planner plan the full
336105
+ // register→verify and forgot-password→reset round trips to completion
336106
+ // (see the planner's "## Verification Inbox Available" block). Armed on the
336107
+ // SAME project-level predicate the generator uses (flag + register intent +
336108
+ // configured inbox), so the planner never promises what generation can't do.
336109
+ emailVerifyArmed: plannerEmailVerifyArmed(ctx),
335857
336110
  testBudget: featureTestBudget
335858
336111
  }, {
335859
336112
  modelOverride: ctx.planModel ?? ctx.generateModel,
@@ -350479,7 +350732,7 @@ var require_package4 = __commonJS({
350479
350732
  "package.json"(exports, module) {
350480
350733
  module.exports = {
350481
350734
  name: "@validate.qa/runner",
350482
- version: "1.0.60",
350735
+ version: "1.0.61",
350483
350736
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
350484
350737
  bin: {
350485
350738
  "validate-runner": "dist/cli.js"
@@ -355562,7 +355815,13 @@ async function handleHealth(_req, res) {
355562
355815
  },
355563
355816
  devices,
355564
355817
  capabilities,
355565
- activeSession: state.appiumSessionId
355818
+ activeSession: state.appiumSessionId,
355819
+ // THIS runner's Appium is executing mobile (APPIUM) test run(s) right now.
355820
+ // The web pre-flight reads this to warn before /bridge/session/start would
355821
+ // 409 — it must never be inferred from server-side global run counts, which
355822
+ // include unrelated projects and runners.
355823
+ executorBusy: executorBusyCount > 0,
355824
+ recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording"
355566
355825
  });
355567
355826
  }
355568
355827
  function realDeviceCapabilities2(platform3) {
@@ -355637,7 +355896,11 @@ function validateSessionStartBody(body) {
355637
355896
  }
355638
355897
  async function handleDevices(_req, res) {
355639
355898
  const devices = discoverAllDevices({ includeApps: true });
355640
- sendJSON(res, 200, { devices });
355899
+ sendJSON(res, 200, {
355900
+ devices,
355901
+ executorBusy: executorBusyCount > 0,
355902
+ recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording"
355903
+ });
355641
355904
  }
355642
355905
  async function handleSessionStart(req, res) {
355643
355906
  const body = await parseBody(req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validate.qa/runner",
3
- "version": "1.0.60",
3
+ "version": "1.0.61",
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"