@validate.qa/runner 1.0.39 → 1.0.41

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 +603 -39
  2. package/dist/cli.mjs +603 -39
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -270,7 +270,8 @@ var require_ai_provider = __commonJS({
270
270
  { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
271
271
  { id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
272
272
  { id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 },
273
- { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
273
+ { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 },
274
+ { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", provider: "deepseek", description: "DeepSeek - V4 Pro, higher-quality reasoning tier, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
274
275
  ];
275
276
  var PROVIDER_DEFAULT_MODEL = {
276
277
  openai: "gpt-4o",
@@ -330,7 +331,8 @@ var require_ai_provider = __commonJS({
330
331
  "claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
331
332
  "nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
332
333
  "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
333
- "deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
334
+ "deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" },
335
+ "deepseek-v4-pro": { strong: "deepseek-v4-pro", fast: "deepseek-v4-pro", agent: "deepseek-v4-pro" }
334
336
  };
335
337
  var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
336
338
  function withModelContext(modelId, fn) {
@@ -392,7 +394,7 @@ var require_ai_provider = __commonJS({
392
394
  if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
393
395
  return { chat_template_kwargs: { enable_thinking: false } };
394
396
  }
395
- if (modelId === "deepseek-v4-flash") {
397
+ if (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro") {
396
398
  return { thinking: { type: "disabled" } };
397
399
  }
398
400
  return {};
@@ -1784,6 +1786,83 @@ var init_constants = __esm({
1784
1786
  }
1785
1787
  });
1786
1788
 
1789
+ // ../shared/dist/project-key.js
1790
+ function isValidProjectKey(key) {
1791
+ return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
1792
+ }
1793
+ function randomChar(rng) {
1794
+ return KEY_ALPHABET[Math.floor(rng() * KEY_ALPHABET.length)] ?? "0";
1795
+ }
1796
+ function randomKey(length, rng) {
1797
+ let out = "";
1798
+ for (let i = 0; i < length; i++)
1799
+ out += randomChar(rng);
1800
+ return out;
1801
+ }
1802
+ function alnumLower(name) {
1803
+ return (name ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
1804
+ }
1805
+ function deriveProjectKey(name, rng = Math.random) {
1806
+ const base2 = alnumLower(name).slice(0, 3);
1807
+ return base2.length >= PROJECT_KEY_MIN_LENGTH ? base2 : randomKey(3, rng);
1808
+ }
1809
+ function generateUniqueProjectKey(name, taken, rng = Math.random) {
1810
+ const isFree = (k) => isValidProjectKey(k) && !taken.has(k);
1811
+ const primary = deriveProjectKey(name, rng);
1812
+ if (isFree(primary))
1813
+ return primary;
1814
+ const prefix2 = alnumLower(name).slice(0, 2);
1815
+ for (const suffixLen of [1, 2]) {
1816
+ for (let attempt = 0; attempt < 40; attempt++) {
1817
+ const stem = prefix2.length >= 1 ? prefix2 : deriveProjectKey(name, rng).slice(0, 2);
1818
+ const candidate = stem + randomKey(suffixLen, rng);
1819
+ if (isFree(candidate))
1820
+ return candidate;
1821
+ }
1822
+ }
1823
+ for (let len = 4; len <= PROJECT_KEY_MAX_LENGTH; len++) {
1824
+ for (let attempt = 0; attempt < 100; attempt++) {
1825
+ const candidate = randomKey(len, rng);
1826
+ if (isFree(candidate))
1827
+ return candidate;
1828
+ }
1829
+ }
1830
+ throw new Error("Unable to generate a unique project key");
1831
+ }
1832
+ var RESERVED_PROJECT_KEYS, PROJECT_KEY_MIN_LENGTH, PROJECT_KEY_MAX_LENGTH, PROJECT_KEY_PATTERN, KEY_ALPHABET;
1833
+ var init_project_key = __esm({
1834
+ "../shared/dist/project-key.js"() {
1835
+ "use strict";
1836
+ RESERVED_PROJECT_KEYS = /* @__PURE__ */ new Set([
1837
+ "tests",
1838
+ "sessions",
1839
+ "settings",
1840
+ "reports",
1841
+ "account",
1842
+ "notifications",
1843
+ "support",
1844
+ "site-map",
1845
+ "api-intelligence",
1846
+ "selector-registry",
1847
+ "project-knowledge",
1848
+ "test-data",
1849
+ "audit",
1850
+ "validate-copilot",
1851
+ "qa-copilot",
1852
+ "insights",
1853
+ "regression",
1854
+ "reviews",
1855
+ "admin",
1856
+ "login",
1857
+ "admin-view"
1858
+ ]);
1859
+ PROJECT_KEY_MIN_LENGTH = 2;
1860
+ PROJECT_KEY_MAX_LENGTH = 12;
1861
+ PROJECT_KEY_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])$/;
1862
+ KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
1863
+ }
1864
+ });
1865
+
1787
1866
  // ../shared/dist/mobile-deeplink.js
1788
1867
  function isSafeDeepLink(value) {
1789
1868
  if (typeof value !== "string")
@@ -3302,6 +3381,7 @@ var init_audit_pricing = __esm({
3302
3381
  "MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
3303
3382
  "MiniMax-M2": { input: 0.3, output: 1.2 },
3304
3383
  "deepseek-v4-flash": { input: 0.14, output: 0.28 },
3384
+ "deepseek-v4-pro": { input: 0.42, output: 0.84 },
3305
3385
  // Whisper bills per-minute, not per-token (OpenAI + Groq)
3306
3386
  "whisper-1": null,
3307
3387
  "whisper-large-v3": null,
@@ -4972,6 +5052,10 @@ __export(dist_exports, {
4972
5052
  PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
4973
5053
  PLAN_ORDER: () => PLAN_ORDER,
4974
5054
  PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
5055
+ PROJECT_KEY_MAX_LENGTH: () => PROJECT_KEY_MAX_LENGTH,
5056
+ PROJECT_KEY_MIN_LENGTH: () => PROJECT_KEY_MIN_LENGTH,
5057
+ PROJECT_KEY_PATTERN: () => PROJECT_KEY_PATTERN,
5058
+ RESERVED_PROJECT_KEYS: () => RESERVED_PROJECT_KEYS,
4975
5059
  SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
4976
5060
  SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
4977
5061
  STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
@@ -5005,6 +5089,7 @@ __export(dist_exports, {
5005
5089
  controlSignature: () => controlSignature,
5006
5090
  countInnerTests: () => countInnerTests,
5007
5091
  decideHarPosted: () => decideHarPosted,
5092
+ deriveProjectKey: () => deriveProjectKey,
5008
5093
  detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
5009
5094
  detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
5010
5095
  effectiveTestCount: () => effectiveTestCount,
@@ -5021,6 +5106,7 @@ __export(dist_exports, {
5021
5106
  formatLimit: () => formatLimit,
5022
5107
  fromDbPricingPlan: () => fromDbPricingPlan,
5023
5108
  generateMobileTestFromSession: () => generateMobileTestFromSession,
5109
+ generateUniqueProjectKey: () => generateUniqueProjectKey,
5024
5110
  getDefaultPlanForTier: () => getDefaultPlanForTier,
5025
5111
  getDeploymentTierForPlan: () => getDeploymentTierForPlan,
5026
5112
  getPlanChangeType: () => getPlanChangeType,
@@ -5050,6 +5136,7 @@ __export(dist_exports, {
5050
5136
  isTrialExpired: () => isTrialExpired,
5051
5137
  isUIVariant: () => isUIVariant,
5052
5138
  isValidMobileAppId: () => isValidMobileAppId,
5139
+ isValidProjectKey: () => isValidProjectKey,
5053
5140
  isValidSttProvider: () => isValidSttProvider,
5054
5141
  isValidTtsModel: () => isValidTtsModel,
5055
5142
  isValidTtsVoiceId: () => isValidTtsVoiceId,
@@ -5095,6 +5182,7 @@ var init_dist = __esm({
5095
5182
  "use strict";
5096
5183
  init_types();
5097
5184
  init_constants();
5185
+ init_project_key();
5098
5186
  init_mobile_test_generation();
5099
5187
  init_mobile_step_healer();
5100
5188
  init_mobile_cross_platform();
@@ -5879,6 +5967,44 @@ var require_ai_queue = __commonJS({
5879
5967
  }
5880
5968
  });
5881
5969
 
5970
+ // ../runner-core/dist/services/mobile/mobile-session-keepalive.js
5971
+ var require_mobile_session_keepalive = __commonJS({
5972
+ "../runner-core/dist/services/mobile/mobile-session-keepalive.js"(exports2) {
5973
+ "use strict";
5974
+ Object.defineProperty(exports2, "__esModule", { value: true });
5975
+ exports2.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
5976
+ exports2.computeKeepAliveIntervalMs = computeKeepAliveIntervalMs;
5977
+ exports2.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
5978
+ function computeKeepAliveIntervalMs(keepaliveMsRaw, newCommandTimeoutSecondsRaw) {
5979
+ const explicit = keepaliveMsRaw?.trim() ? Number(keepaliveMsRaw) : NaN;
5980
+ if (Number.isFinite(explicit) && explicit >= 0)
5981
+ return Math.floor(explicit);
5982
+ const timeoutSeconds = Number(newCommandTimeoutSecondsRaw);
5983
+ const reapWindowMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) * 1e3 : 6e5;
5984
+ return Math.min(12e4, Math.max(5e3, Math.floor(reapWindowMs / 3)));
5985
+ }
5986
+ exports2.MOBILE_KEEPALIVE_INTERVAL_MS = computeKeepAliveIntervalMs(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS, process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
5987
+ async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports2.MOBILE_KEEPALIVE_INTERVAL_MS) {
5988
+ if (intervalMs <= 0)
5989
+ return fn();
5990
+ const timer = setInterval(() => {
5991
+ try {
5992
+ const probe = driver.keepAlivePing ? driver.keepAlivePing() : driver.windowSize();
5993
+ void probe.catch(() => {
5994
+ });
5995
+ } catch {
5996
+ }
5997
+ }, intervalMs);
5998
+ timer.unref?.();
5999
+ try {
6000
+ return await fn();
6001
+ } finally {
6002
+ clearInterval(timer);
6003
+ }
6004
+ }
6005
+ }
6006
+ });
6007
+
5882
6008
  // ../runner-core/dist/services/mobile/mobile-observation.js
5883
6009
  var require_mobile_observation = __commonJS({
5884
6010
  "../runner-core/dist/services/mobile/mobile-observation.js"(exports2) {
@@ -64612,6 +64738,7 @@ var require_mobile_explorer = __commonJS({
64612
64738
  var ai_provider_js_1 = require_ai_provider();
64613
64739
  var ai_queue_js_1 = require_ai_queue();
64614
64740
  var ai_retry_js_1 = require_ai_retry();
64741
+ var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
64615
64742
  var mobile_source_parser_js_1 = require_mobile_source_parser();
64616
64743
  var mobile_observation_js_1 = require_mobile_observation();
64617
64744
  var mobile_exploration_state_js_1 = require_mobile_exploration_state();
@@ -66121,7 +66248,7 @@ ${statePacket}` }
66121
66248
  const requestMessageCount = messages.length;
66122
66249
  const requestMessages = messages.slice();
66123
66250
  const callStart = Date.now();
66124
- const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
66251
+ const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
66125
66252
  arm();
66126
66253
  return aiClient.chat.completions.create({
66127
66254
  model: agentModel,
@@ -66134,7 +66261,7 @@ ${statePacket}` }
66134
66261
  label: `mobile-explore-iter-${iteration}`,
66135
66262
  deferTimeoutUntilArmed: true,
66136
66263
  onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile explore AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
66137
- });
66264
+ }));
66138
66265
  const iterDuration = Date.now() - callStart;
66139
66266
  if (!response?.choices?.[0]?.message) {
66140
66267
  throw new Error(`AI explore call returned no response (queue timeout or provider overload) at iteration ${iteration}`);
@@ -68213,6 +68340,7 @@ var require_discover_ai_planner = __commonJS({
68213
68340
  "use strict";
68214
68341
  Object.defineProperty(exports2, "__esModule", { value: true });
68215
68342
  exports2.SCENARIO_SYSTEM_PROMPT = void 0;
68343
+ exports2.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
68216
68344
  exports2.getSnapshotConfidence = getSnapshotConfidence;
68217
68345
  exports2.compressSiteMap = compressSiteMap;
68218
68346
  exports2.formatEvidenceForPlanner = formatEvidenceForPlanner;
@@ -68224,10 +68352,25 @@ var require_discover_ai_planner = __commonJS({
68224
68352
  var ai_retry_js_1 = require_ai_retry();
68225
68353
  var strip_json_fences_js_1 = require_strip_json_fences();
68226
68354
  var scenario_plan_validator_js_1 = require_scenario_plan_validator();
68227
- var DEFAULT_AI_PLANNER_MAX_TOKENS = 8192;
68228
- var MINIMAX_AI_PLANNER_MAX_TOKENS = 16384;
68355
+ var GENEROUS_AI_PLANNER_MAX_TOKENS = 65536;
68356
+ var CONSERVATIVE_AI_PLANNER_MAX_TOKENS = 16384;
68229
68357
  function getAIPlannerMaxTokens(modelId) {
68230
- return (0, ai_provider_js_1.getProviderForModel)(modelId) === "minimax" ? MINIMAX_AI_PLANNER_MAX_TOKENS : DEFAULT_AI_PLANNER_MAX_TOKENS;
68358
+ switch ((0, ai_provider_js_1.getProviderForModel)(modelId)) {
68359
+ case "deepseek":
68360
+ // V4 output ceiling = 384K — verified safe
68361
+ case "minimax":
68362
+ return GENEROUS_AI_PLANNER_MAX_TOKENS;
68363
+ case "xai":
68364
+ // grok-build-0.1 ceiling unproven (Grok-Fast/3 = 16K) — DON'T risk a 400
68365
+ case "openai":
68366
+ // gpt-4o max output = 16384 → more would 400
68367
+ case "anthropic":
68368
+ // OpenAI-compat proxy — keep conservative
68369
+ case "nvidia":
68370
+ // NIM output limits vary — keep conservative
68371
+ default:
68372
+ return CONSERVATIVE_AI_PLANNER_MAX_TOKENS;
68373
+ }
68231
68374
  }
68232
68375
  var discover_planner_js_1 = require_discover_planner();
68233
68376
  function getSnapshotConfidence(sourceKind) {
@@ -68779,7 +68922,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68779
68922
  4. **Multi-page journeys** - only when transitions were actually observed
68780
68923
  5. **Bug reports** - for observed 500 errors, crashes, or broken features
68781
68924
  6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
68782
- 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, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test.
68925
+ 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, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength self-check (applies to EVERY scenario type):** before emitting a scenario, ask \u2014 would every expectedOutcome still pass if the primary action did nothing and only the page and its existing chrome rendered? If yes, it is too weak. Add at least one evidence-backed post-condition that would FAIL in that no-op case: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, handled validation copy, or a concrete session signal. A route alone suffices only when navigation or redirect is itself the behavior under test; a newly revealed control suffices only when revealing that control is the behavior under test. Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
68783
68926
  8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
68784
68927
  9. **No duplicates** - check existing test names before adding
68785
68928
  10. **All tests start from the landing page** - every generated test navigates to \`/\` first and uses the app's own navigation (clicking links/buttons) to reach the target page. Steps must reflect this: say "Navigate to landing page and click Sign In" not "Navigate to /auth/login". Deep-linking to internal routes causes hydration failures in SPAs. \`targetPages\` lists the pages involved in the flow.
@@ -68831,7 +68974,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68831
68974
  **YOUR responsibility:**
68832
68975
  - Scenario \`name\`, \`type\`, \`goal\`, \`prerequisites\`
68833
68976
  - \`steps\` written as user-language intent ("create a new conversation", "click the Submit button to log in"), naming observed UI labels where they help describe intent
68834
- - \`expectedOutcomes\` citing observed durable labels per Rule 12 (asserting on stable signals is encouraged)
68977
+ - \`expectedOutcomes\` expressing the evidence-backed product post-condition per Rule 7 (and Rule 11 for happy paths, plus Rule 12's concrete session signal for authentication happy paths); citing observed durable labels when asserting on stable signals is encouraged
68835
68978
 
68836
68979
  **NOT your responsibility:**
68837
68980
  - Exact Playwright selectors (\`getByRole(...)\`, \`getByText(...)\`, \`page.locator(...)\`, etc.) \u2014 live-generate picks locators against the live snapshot
@@ -68852,7 +68995,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68852
68995
  - GOOD specificity: "create a new conversation by sending a message to a connection established earlier in this scenario"
68853
68996
  - BAD specificity: "click the button labeled exactly 'Send' in the bottom-right of the chat"
68854
68997
 
68855
- Citing observed labels in \`expectedOutcomes\` (per Rule 12) is fine \u2014 that's asserting on stable product signals, not implementation specifics.
68998
+ Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
68856
68999
 
68857
69000
  ## Coverage Strategy
68858
69001
  - For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
@@ -69438,6 +69581,24 @@ Each entry is exactly one of:
69438
69581
  }
69439
69582
  async function runAIPlanner(siteMap, evidence, context, options) {
69440
69583
  const logs2 = [];
69584
+ const failurePlan = (kind, reason, legacyFallback2) => {
69585
+ const noFallback = context.mode === "survey" && process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
69586
+ if (noFallback) {
69587
+ logs2.push(`Planner ${kind} \u2014 NO-FALLBACK: failing survey cleanly instead of fabricating slug features (${reason})`);
69588
+ return {
69589
+ scenarios: [],
69590
+ featureGroups: [],
69591
+ reasoning: "",
69592
+ logs: logs2,
69593
+ coverageGaps: [],
69594
+ plannerFailed: true,
69595
+ plannerFailureKind: kind,
69596
+ fallbackReason: reason
69597
+ };
69598
+ }
69599
+ return { ...legacyFallback2(), fallbackReason: reason };
69600
+ };
69601
+ let extraPlannerCallUsed = false;
69441
69602
  const compressed = compressSiteMap(siteMap);
69442
69603
  const agentModel = options.modelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.modelOverride, "agent") : (0, ai_provider_js_1.getModel)("agent");
69443
69604
  const plannerMaxTokens = getAIPlannerMaxTokens(agentModel);
@@ -69473,7 +69634,7 @@ Each entry is exactly one of:
69473
69634
  } catch (err) {
69474
69635
  const message = err instanceof Error ? err.message : String(err);
69475
69636
  logs2.push(`AI planner failed after retries: ${message}`);
69476
- return { ...buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget), fallbackReason: (0, ai_retry_js_1.describeAIFailure)(err) };
69637
+ return failurePlan("transport", (0, ai_retry_js_1.describeAIFailure)(err), () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
69477
69638
  }
69478
69639
  try {
69479
69640
  options.onAICall?.({
@@ -69492,20 +69653,104 @@ Each entry is exactly one of:
69492
69653
  });
69493
69654
  } catch {
69494
69655
  }
69495
- const raw = response?.choices?.[0]?.message?.content;
69496
- if (!raw) {
69656
+ const choice = response?.choices?.[0];
69657
+ const raw = choice?.message?.content;
69658
+ const finishReason = (choice?.finish_reason ?? "").toString().toLowerCase();
69659
+ const completionTokens = response.usage?.completion_tokens;
69660
+ const TRUNCATION_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "max_output_tokens", "token_limit"]);
69661
+ const definitelyTruncated = TRUNCATION_FINISH.has(finishReason);
69662
+ const atOrOverCap = completionTokens != null && completionTokens >= plannerMaxTokens;
69663
+ const blocked = finishReason === "content_filter" || finishReason === "tool_calls";
69664
+ const usage = response.usage;
69665
+ logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens (finish=${finishReason || "?"})`);
69666
+ const legacyFallback = () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
69667
+ if (blocked) {
69668
+ return failurePlan("blocked", "AI response was blocked (content_filter/tool_calls)", legacyFallback);
69669
+ }
69670
+ if (definitelyTruncated) {
69671
+ return failurePlan("truncated", "AI planner output truncated at max_tokens (finish_reason=length)", legacyFallback);
69672
+ }
69673
+ const trimmed = raw?.trim();
69674
+ if (!trimmed) {
69497
69675
  logs2.push("AI planner returned empty response");
69498
- return { ...buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget), fallbackReason: "AI returned an empty response" };
69676
+ return atOrOverCap ? failurePlan("truncated", "AI planner output truncated at max_tokens (empty body at cap)", legacyFallback) : failurePlan("empty", "AI returned an empty response", legacyFallback);
69499
69677
  }
69500
- const usage = response.usage;
69501
- logs2.push(`AI planner: ${usage?.prompt_tokens ?? "?"} input, ${usage?.completion_tokens ?? "?"} output tokens`);
69502
69678
  let aiOutput;
69503
69679
  try {
69504
- aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(raw));
69680
+ aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(trimmed));
69505
69681
  } catch (err) {
69506
69682
  const message = err instanceof Error ? err.message : String(err);
69507
69683
  logs2.push(`AI planner returned invalid JSON: ${message}`);
69508
- return { ...buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget), fallbackReason: "AI returned unreadable output" };
69684
+ if (atOrOverCap) {
69685
+ logs2.push("AI planner output at/over the token cap and unparseable \u2014 treating as truncated (skipping repair)");
69686
+ return failurePlan("truncated", "AI planner output truncated at max_tokens (unparseable at cap)", legacyFallback);
69687
+ }
69688
+ const repairPrompt = `Your previous response was not valid JSON (parser error: ${message}). Return ONLY the corrected JSON object \u2014 no prose, no markdown fences \u2014 matching the exact schema of your previous attempt.`;
69689
+ const repairStart = Date.now();
69690
+ let repairResponse = null;
69691
+ try {
69692
+ extraPlannerCallUsed = true;
69693
+ repairResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
69694
+ arm();
69695
+ return aiClient.chat.completions.create({
69696
+ model: agentModel,
69697
+ messages: [
69698
+ ...messages,
69699
+ { role: "assistant", content: raw },
69700
+ { role: "user", content: repairPrompt }
69701
+ ],
69702
+ max_tokens: plannerMaxTokens,
69703
+ temperature: 0.3,
69704
+ ...(0, ai_provider_js_1.getJsonObjectResponseFormat)(agentModel),
69705
+ ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(agentModel)
69706
+ }, { signal });
69707
+ }), {
69708
+ label: "ai-planner-repair",
69709
+ deferTimeoutUntilArmed: true,
69710
+ onRetry: (attempt, reason, delay) => logs2.push(`AI planner repair call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
69711
+ });
69712
+ } catch (repairErr) {
69713
+ const repairMessage = repairErr instanceof Error ? repairErr.message : String(repairErr);
69714
+ logs2.push(`AI planner repair failed (${repairMessage})`);
69715
+ }
69716
+ if (repairResponse) {
69717
+ try {
69718
+ options.onAICall?.({
69719
+ iteration: 1,
69720
+ groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
69721
+ callType: "discoveryAIPlannerRepair",
69722
+ model: agentModel,
69723
+ provider: (0, ai_provider_js_1.getProviderForModel)(agentModel),
69724
+ durationMs: Date.now() - repairStart,
69725
+ promptTokens: repairResponse.usage?.prompt_tokens,
69726
+ completionTokens: repairResponse.usage?.completion_tokens,
69727
+ totalTokens: repairResponse.usage?.total_tokens,
69728
+ assistantMessage: repairResponse.choices[0]?.message?.content,
69729
+ requestMessageCount: messages.length + 2,
69730
+ requestMessages: [
69731
+ ...messages,
69732
+ { role: "assistant", content: raw },
69733
+ { role: "user", content: repairPrompt }
69734
+ ]
69735
+ });
69736
+ } catch {
69737
+ }
69738
+ const repairedRaw = repairResponse.choices[0]?.message?.content?.trim();
69739
+ if (repairedRaw) {
69740
+ try {
69741
+ aiOutput = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(repairedRaw));
69742
+ } catch (repairParseErr) {
69743
+ const repairMessage = repairParseErr instanceof Error ? repairParseErr.message : String(repairParseErr);
69744
+ logs2.push(`AI planner repair returned invalid JSON (${repairMessage})`);
69745
+ return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
69746
+ }
69747
+ } else {
69748
+ logs2.push("AI planner repair returned empty response");
69749
+ return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
69750
+ }
69751
+ } else {
69752
+ return failurePlan("malformed", "AI returned unreadable output", legacyFallback);
69753
+ }
69509
69754
  }
69510
69755
  let { scenarios, featureGroups, warnings, coverageGaps } = validateAIPlannerOutput(aiOutput, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
69511
69756
  logs2.push(`AI planner: ${scenarios.length} scenarios, ${featureGroups.length} feature groups (${warnings.length} warnings, ${coverageGaps.length} coverage gaps)`);
@@ -69515,7 +69760,7 @@ Each entry is exactly one of:
69515
69760
  let topLevelWarning;
69516
69761
  const totalV1 = scenarios.length + coverageGaps.length;
69517
69762
  const rejectionRateV1 = totalV1 > 0 ? coverageGaps.length / totalV1 : 0;
69518
- if (rejectionRateV1 > 0.5 && totalV1 >= 3) {
69763
+ if (rejectionRateV1 > 0.5 && totalV1 >= 3 && !extraPlannerCallUsed) {
69519
69764
  logs2.push(`AI planner: ${Math.round(rejectionRateV1 * 100)}% rejection rate (${coverageGaps.length}/${totalV1}) \u2014 running self-correction retry`);
69520
69765
  const retryContext = [
69521
69766
  `Your previous emission produced ${coverageGaps.length} coverage gaps out of ${totalV1} scenarios (${Math.round(rejectionRateV1 * 100)}% rejection rate).`,
@@ -69534,6 +69779,7 @@ Each entry is exactly one of:
69534
69779
  const retryStart = Date.now();
69535
69780
  let retryResponse = null;
69536
69781
  try {
69782
+ extraPlannerCallUsed = true;
69537
69783
  retryResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
69538
69784
  arm();
69539
69785
  return aiClient.chat.completions.create({
@@ -69633,6 +69879,22 @@ Each entry is exactly one of:
69633
69879
  };
69634
69880
  }
69635
69881
  if (context.mode === "survey" && featureGroups.length === 0 && siteMap.pages.length > 0) {
69882
+ const hasGroupWorthyEvidence = siteMap.pages.some((p) => (p.forms?.length ?? 0) > 0 || (p.elements?.length ?? 0) > 0);
69883
+ const noFallback = process.env.DISCOVERY_PLANNER_NO_FALLBACK === "true";
69884
+ if (noFallback && !hasGroupWorthyEvidence) {
69885
+ return {
69886
+ scenarios,
69887
+ featureGroups: [],
69888
+ reasoning: aiOutput.reasoning ?? "",
69889
+ logs: logs2,
69890
+ coverageGaps,
69891
+ warning: topLevelWarning,
69892
+ fallbackUsed: false
69893
+ };
69894
+ }
69895
+ if (noFallback) {
69896
+ return failurePlan("empty_groups", "AI produced no feature groups despite discovered content", () => buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget));
69897
+ }
69636
69898
  logs2.push("Survey AI returned 0 feature groups - falling back to deterministic grouping");
69637
69899
  const fallback = buildFallbackPlan(siteMap, evidence.recordedJourneys, context.mode, logs2, context.existingTestNames ?? [], context.focusAreas, context.testBudget);
69638
69900
  return {
@@ -70843,6 +71105,7 @@ var require_mobile_generator = __commonJS({
70843
71105
  var ai_provider_js_1 = require_ai_provider();
70844
71106
  var ai_queue_js_1 = require_ai_queue();
70845
71107
  var ai_retry_js_1 = require_ai_retry();
71108
+ var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
70846
71109
  var mobile_source_parser_js_1 = require_mobile_source_parser();
70847
71110
  var mobile_source_parser_js_2 = require_mobile_source_parser();
70848
71111
  var mobile_observation_js_1 = require_mobile_observation();
@@ -71750,14 +72013,14 @@ ${statePacket}`, redactionCredentials) },
71750
72013
  const requestMessageCount = messages.length;
71751
72014
  const requestMessages = messages.slice();
71752
72015
  const callStart = Date.now();
71753
- const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
72016
+ const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
71754
72017
  arm();
71755
72018
  return aiClient.chat.completions.create({ model: agentModel, messages, tools: GENERATE_TOOLS, tool_choice: "auto", max_tokens: 8e3 }, { signal });
71756
72019
  }), {
71757
72020
  label: `mobile-generate-${scenario.name.slice(0, 24)}-iter-${iteration}`,
71758
72021
  deferTimeoutUntilArmed: true,
71759
72022
  onRetry: (attempt, reason, delayMs) => log2(` \u26A0 mobile generate AI retry ${attempt} (${reason}) \u2014 waiting ${delayMs}ms`)
71760
- });
72023
+ }));
71761
72024
  const iterDuration = Date.now() - callStart;
71762
72025
  if (!response?.choices?.[0]?.message) {
71763
72026
  throw new Error(`AI generate call returned no response at iteration ${iteration}`);
@@ -72369,6 +72632,21 @@ ${statePacket}`, redactionCredentials) },
72369
72632
  };
72370
72633
  }
72371
72634
  const metadata = metadataForElement(element, screenName(), platform3);
72635
+ const expectedNeedle = expected.trim().toLowerCase();
72636
+ const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
72637
+ const visibleText = (element.text ?? "").trim().toLowerCase();
72638
+ const isSelfLabelAssertion = expectedNeedle.length > 0 && identityValues.includes(expectedNeedle) && !visibleText.includes(expectedNeedle);
72639
+ if (isSelfLabelAssertion) {
72640
+ recordAction({ type: "ASSERT_VISIBLE", selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
72641
+ log2(` assert_text DOWNGRADED to assert_visible (tautological self-label "${expected}"): ${metadata.targetLabel}`);
72642
+ return {
72643
+ ok: true,
72644
+ asserted: "visible",
72645
+ downgraded: true,
72646
+ note: `"${expected}" is the element's own accessibility identity, not product copy it displays \u2014 recorded a visibility assertion instead. To prove the outcome, assert text the screen SHOWS (an error message, a status label, a field value), not the control's own name.`,
72647
+ target: metadata.targetLabel
72648
+ };
72649
+ }
72372
72650
  if (hasStableId(element)) {
72373
72651
  recordAction({ type: "ASSERT_TEXT", value: expected, selector: (0, mobile_source_parser_js_2.preferredLocator)(element), metadata });
72374
72652
  log2(` assert_text: ${metadata.targetLabel} ~ "${expected}"`);
@@ -73062,7 +73340,13 @@ ${statePacket}`, redactionCredentials) },
73062
73340
  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;
73063
73341
  const test = {
73064
73342
  name: compiled.name,
73065
- description: recording.finish?.description ?? compiled.description,
73343
+ // Description priority: the model's live finish description, then the
73344
+ // planner's scenario goal (always a meaningful sentence describing what the
73345
+ // test proves), then the generic compiled placeholder. Without the goal
73346
+ // fallback a scenario whose finish_generate omitted a description shipped
73347
+ // with the contentless "<platform> native flow generated from a recorded
73348
+ // mobile session." string — see mobile-test-generation.ts's compiled default.
73349
+ description: recording.finish?.description?.trim() || scenario.goal?.trim() || compiled.description,
73066
73350
  framework: "APPIUM",
73067
73351
  appiumCode: compiled.appiumCode,
73068
73352
  steps: compiled.steps,
@@ -73804,6 +74088,7 @@ var require_mobile_heal_loop = __commonJS({
73804
74088
  var mobile_heal_postprocess_js_1 = require_mobile_heal_postprocess();
73805
74089
  var mobile_observation_js_1 = require_mobile_observation();
73806
74090
  var selector_registry_context_js_1 = require_selector_registry_context();
74091
+ var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
73807
74092
  exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = `You are a QA engineer fixing a failing Appium (mobile UI automation) test. You receive: the test STEPS as JSON, the failure ERROR + step-level results, and the PAGE-SOURCE accessibility tree captured at the moment of failure (this tree is your 'screenshot'), plus optional known-good selector candidates from a registry.
73808
74093
  ## You MUST try to fix the steps
73809
74094
  Reason over steps + error + tree + registry and return working healed steps. Do NOT return needs_device unless you have genuinely exhausted that reasoning (and NEVER before your final attempt).
@@ -74695,6 +74980,19 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
74695
74980
  };
74696
74981
  }
74697
74982
  const maxPasses = envInt2("MOBILE_HEAL_PHASE2_MAX_PASSES") ?? DEFAULT_PHASE2_MAX_PASSES;
74983
+ const keepAlivePingable = {
74984
+ ...typeof liveDriver.keepAlivePing === "function" ? { keepAlivePing: liveDriver.keepAlivePing.bind(liveDriver) } : {},
74985
+ // WARNING: this fallback routes through snapshot(), which sits inside
74986
+ // withSessionRecovery on the real driver — a failed tick could consume the
74987
+ // bounded recovery budget, the exact bug keepAlivePing exists to avoid.
74988
+ // Every current MobileLiveDriverLike provides keepAlivePing, so this only
74989
+ // exists for structural compatibility; implement keepAlivePing on any new
74990
+ // facade rather than relying on it.
74991
+ windowSize: async () => {
74992
+ await liveDriver.snapshot();
74993
+ return null;
74994
+ }
74995
+ };
74698
74996
  let baseSteps = args.steps;
74699
74997
  let priorError = args.priorError ?? null;
74700
74998
  let bestSteps;
@@ -74729,10 +75027,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
74729
75027
  ];
74730
75028
  let content;
74731
75029
  try {
74732
- const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
75030
+ const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(keepAlivePingable, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
74733
75031
  arm();
74734
75032
  return aiClient.chat.completions.create({ model, messages, max_tokens: 8e3, temperature: 0.1 }, { signal });
74735
- }), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true });
75033
+ }), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
74736
75034
  content = response?.choices?.[0]?.message?.content ?? "";
74737
75035
  } catch {
74738
75036
  continue;
@@ -75739,6 +76037,118 @@ var require_har_stream = __commonJS({
75739
76037
  }
75740
76038
  });
75741
76039
 
76040
+ // ../runner-core/dist/services/api-auth-headers.js
76041
+ var require_api_auth_headers = __commonJS({
76042
+ "../runner-core/dist/services/api-auth-headers.js"(exports2) {
76043
+ "use strict";
76044
+ Object.defineProperty(exports2, "__esModule", { value: true });
76045
+ exports2.parseApiAuthHeaders = parseApiAuthHeaders;
76046
+ exports2.resolveApiAuthHeaders = resolveApiAuthHeaders;
76047
+ exports2.isAppOriginRequest = isAppOriginRequest;
76048
+ exports2.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
76049
+ var HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
76050
+ function hasControlChar(v) {
76051
+ for (let i = 0; i < v.length; i++) {
76052
+ const code = v.charCodeAt(i);
76053
+ if (code < 32 || code === 127)
76054
+ return true;
76055
+ }
76056
+ return false;
76057
+ }
76058
+ function parseApiAuthHeaders(raw) {
76059
+ if (!raw || typeof raw !== "string")
76060
+ return void 0;
76061
+ const trimmed = raw.trim();
76062
+ if (!trimmed)
76063
+ return void 0;
76064
+ try {
76065
+ const parsed = JSON.parse(trimmed);
76066
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
76067
+ return void 0;
76068
+ const headers = {};
76069
+ for (const [k, v] of Object.entries(parsed)) {
76070
+ if (typeof k === "string" && HEADER_NAME_RE.test(k) && typeof v === "string" && !hasControlChar(v)) {
76071
+ headers[k] = v;
76072
+ }
76073
+ }
76074
+ return Object.keys(headers).length > 0 ? headers : void 0;
76075
+ } catch {
76076
+ return void 0;
76077
+ }
76078
+ }
76079
+ function resolveApiAuthHeaders(input) {
76080
+ if (input.explicit)
76081
+ return input.explicit;
76082
+ if (!input.isApiTest)
76083
+ return void 0;
76084
+ return parseApiAuthHeaders(input.apiAuthHeadersVar);
76085
+ }
76086
+ function isAppOriginRequest(url, baseUrl) {
76087
+ if (typeof url !== "string" || !baseUrl)
76088
+ return false;
76089
+ let appOrigin;
76090
+ try {
76091
+ appOrigin = new URL(baseUrl).origin;
76092
+ } catch {
76093
+ return false;
76094
+ }
76095
+ try {
76096
+ return new URL(url, baseUrl).origin === appOrigin;
76097
+ } catch {
76098
+ return false;
76099
+ }
76100
+ }
76101
+ function buildOriginScopedApiFixtures(headers, baseUrl) {
76102
+ let appOrigin;
76103
+ try {
76104
+ appOrigin = baseUrl ? new URL(baseUrl).origin : null;
76105
+ } catch {
76106
+ appOrigin = null;
76107
+ }
76108
+ return `export * from '@playwright/test';
76109
+ import { test as base } from '@playwright/test';
76110
+
76111
+ // Standing auth headers \u2014 attached ONLY to same-origin requests so the credential never leaks to
76112
+ // an off-origin host. Embedded at run time; this file lives in the ephemeral run dir.
76113
+ const STANDING_HEADERS = ${JSON.stringify(headers)};
76114
+ const APP_ORIGIN = ${JSON.stringify(appOrigin)};
76115
+ const BASE_URL = ${JSON.stringify(baseUrl)};
76116
+ const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'fetch'];
76117
+
76118
+ function sameOrigin(url) {
76119
+ if (!APP_ORIGIN || typeof url !== 'string') return false;
76120
+ try { return new URL(url, BASE_URL).origin === APP_ORIGIN; } catch { return false; }
76121
+ }
76122
+
76123
+ export const test = base.extend({
76124
+ request: async ({ request }, use) => {
76125
+ const scoped = new Proxy(request, {
76126
+ get(target, prop) {
76127
+ const value = target[prop];
76128
+ // HTTP verbs: wrap to add standing headers on same-origin requests only.
76129
+ if (typeof prop === 'string' && METHODS.indexOf(prop) !== -1) {
76130
+ const orig = value.bind(target);
76131
+ return (url, options) => {
76132
+ options = options || {};
76133
+ return sameOrigin(url)
76134
+ ? orig(url, { ...options, headers: { ...STANDING_HEADERS, ...(options.headers || {}) } })
76135
+ : orig(url, options);
76136
+ };
76137
+ }
76138
+ // Every other METHOD must run with 'this' bound to the REAL context \u2014 an APIRequestContext
76139
+ // uses internal slots/private fields a Proxy can't forward, so calling with 'this'=proxy
76140
+ // would throw. Non-function props pass through untouched.
76141
+ return typeof value === 'function' ? value.bind(target) : value;
76142
+ },
76143
+ });
76144
+ await use(scoped);
76145
+ },
76146
+ });
76147
+ `;
76148
+ }
76149
+ }
76150
+ });
76151
+
75742
76152
  // ../runner-core/dist/services/playwright-native.js
75743
76153
  var require_playwright_native = __commonJS({
75744
76154
  "../runner-core/dist/services/playwright-native.js"(exports2) {
@@ -75781,7 +76191,7 @@ var require_playwright_native = __commonJS({
75781
76191
  };
75782
76192
  })();
75783
76193
  Object.defineProperty(exports2, "__esModule", { value: true });
75784
- exports2.scrubHarRequestBody = void 0;
76194
+ exports2.scrubHarRequestBody = exports2.buildOriginScopedApiFixtures = exports2.isAppOriginRequest = exports2.resolveApiAuthHeaders = exports2.parseApiAuthHeaders = void 0;
75785
76195
  exports2.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
75786
76196
  exports2.parseJsonResults = parseJsonResults2;
75787
76197
  exports2.parseHarToApiSummary = parseHarToApiSummary;
@@ -75793,6 +76203,20 @@ var require_playwright_native = __commonJS({
75793
76203
  var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
75794
76204
  var api_call_redaction_js_1 = require_api_call_redaction();
75795
76205
  var har_stream_js_1 = require_har_stream();
76206
+ var api_auth_headers_js_1 = require_api_auth_headers();
76207
+ var api_auth_headers_js_2 = require_api_auth_headers();
76208
+ Object.defineProperty(exports2, "parseApiAuthHeaders", { enumerable: true, get: function() {
76209
+ return api_auth_headers_js_2.parseApiAuthHeaders;
76210
+ } });
76211
+ Object.defineProperty(exports2, "resolveApiAuthHeaders", { enumerable: true, get: function() {
76212
+ return api_auth_headers_js_2.resolveApiAuthHeaders;
76213
+ } });
76214
+ Object.defineProperty(exports2, "isAppOriginRequest", { enumerable: true, get: function() {
76215
+ return api_auth_headers_js_2.isAppOriginRequest;
76216
+ } });
76217
+ Object.defineProperty(exports2, "buildOriginScopedApiFixtures", { enumerable: true, get: function() {
76218
+ return api_auth_headers_js_2.buildOriginScopedApiFixtures;
76219
+ } });
75796
76220
  var api_call_redaction_js_2 = require_api_call_redaction();
75797
76221
  Object.defineProperty(exports2, "scrubHarRequestBody", { enumerable: true, get: function() {
75798
76222
  return api_call_redaction_js_2.scrubHarRequestBody;
@@ -75802,7 +76226,7 @@ var require_playwright_native = __commonJS({
75802
76226
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
75803
76227
  var PW_CLI = require.resolve("@playwright/test/cli");
75804
76228
  async function executeTestViaNativePlaywright2(options) {
75805
- const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76229
+ const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
75806
76230
  const logs2 = [];
75807
76231
  const screenshots = [];
75808
76232
  logs2.push(`\u{1F3AD} Native Playwright starting`);
@@ -75816,6 +76240,11 @@ var require_playwright_native = __commonJS({
75816
76240
  runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
75817
76241
  await (0, promises_1.mkdir)(runDir, { recursive: true });
75818
76242
  const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
76243
+ const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
76244
+ explicit: extraHttpHeaders,
76245
+ isApiTest: isApiTest2,
76246
+ apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
76247
+ });
75819
76248
  const cssDisableSetup = `
75820
76249
  const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
75821
76250
  let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
@@ -76009,8 +76438,17 @@ test.afterEach(async ({ page }, testInfo) => {
76009
76438
  if (!importMatch) {
76010
76439
  logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
76011
76440
  }
76012
- const instrumentedCode = isApiTest2 ? playwrightCode : importMatch ? playwrightCode.replace(importMatch[0], importMatch[0] + cssDisableSetup) : playwrightCode;
76441
+ const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
76442
+ let instrumentedCode;
76443
+ if (isApiTest2) {
76444
+ instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
76445
+ } else {
76446
+ instrumentedCode = importMatch ? playwrightCode.replace(importMatch[0], importMatch[0] + cssDisableSetup) : playwrightCode;
76447
+ }
76013
76448
  await (0, promises_1.writeFile)((0, path_1.join)(runDir, "test.spec.ts"), instrumentedCode);
76449
+ if (useScopedApiHeaders && resolvedExtraHeaders) {
76450
+ await (0, promises_1.writeFile)((0, path_1.join)(runDir, "qa-api-fixtures.ts"), (0, api_auth_headers_js_1.buildOriginScopedApiFixtures)(resolvedExtraHeaders, baseUrl));
76451
+ }
76014
76452
  const headless = process.env.PLAYWRIGHT_HEADLESS === "true" || process.env.NODE_ENV === "production" || IS_CLOUD2;
76015
76453
  logs2.push(`\u2699\uFE0F Config: baseURL=${JSON.stringify(baseUrl)}, headless=${headless}${isApiTest2 ? ", API mode (no screenshot/video)" : ""}`);
76016
76454
  const absTestDir = runDir.replace(/\\/g, "/");
@@ -76024,6 +76462,7 @@ test.afterEach(async ({ page }, testInfo) => {
76024
76462
  const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
76025
76463
  const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
76026
76464
  const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
76465
+ const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
76027
76466
  const launchArgs = IS_CLOUD2 ? "args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--disable-accelerated-2d-canvas', '--js-flags=--max-old-space-size=512']," : "args: ['--disable-blink-features=AutomationControlled'],";
76028
76467
  const config = `
76029
76468
  // Plain object export - no require('@playwright/test') needed.
@@ -76037,6 +76476,7 @@ module.exports = {
76037
76476
  use: {
76038
76477
  baseURL: ${JSON.stringify(baseUrl)},
76039
76478
  ${storageStateLine}
76479
+ ${extraHeadersLine}
76040
76480
  browserName: 'chromium',
76041
76481
  headless: ${headless},
76042
76482
  screenshot: ${isApiTest2 ? "'off'" : "'on'"},
@@ -76078,6 +76518,7 @@ module.exports = {
76078
76518
  // code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
76079
76519
  ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
76080
76520
  };
76521
+ delete envWithIsolation.API_AUTH_HEADERS;
76081
76522
  logs2.push("\u{1F680} Spawning playwright test...");
76082
76523
  const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
76083
76524
  let playResult;
@@ -84184,6 +84625,7 @@ var require_discover_generator = __commonJS({
84184
84625
  var ai_provider_js_1 = require_ai_provider();
84185
84626
  var ai_queue_js_1 = require_ai_queue();
84186
84627
  var ai_retry_js_1 = require_ai_retry();
84628
+ var discover_ai_planner_js_1 = require_discover_ai_planner();
84187
84629
  var snapshot_parser_js_1 = require_snapshot_parser();
84188
84630
  var scrub_credentials_js_1 = require_scrub_credentials();
84189
84631
  var exploration_parser_js_1 = require_exploration_parser();
@@ -84391,6 +84833,51 @@ var require_discover_generator = __commonJS({
84391
84833
  }
84392
84834
  return s.length;
84393
84835
  }
84836
+ function lineParenDeficit(line) {
84837
+ const stack = [];
84838
+ let regexSkips = 0;
84839
+ for (let i = 0; i < line.length; i++) {
84840
+ const ch = line[i];
84841
+ if (ch === "'" || ch === '"' || ch === "`") {
84842
+ i = skipQuotedLiteral(line, i) - 1;
84843
+ continue;
84844
+ }
84845
+ if (ch === "/" && line[i + 1] === "/") {
84846
+ i = skipLineComment(line, i) - 1;
84847
+ continue;
84848
+ }
84849
+ if (ch === "/" && line[i + 1] === "*") {
84850
+ i = skipBlockComment(line, i) - 1;
84851
+ continue;
84852
+ }
84853
+ if (ch === "/" && isRegexStart(line, i)) {
84854
+ regexSkips++;
84855
+ i = skipRegexLiteral(line, i) - 1;
84856
+ continue;
84857
+ }
84858
+ if (ch === "(" || ch === "{" || ch === "[") {
84859
+ stack.push(ch);
84860
+ continue;
84861
+ }
84862
+ if (ch === ")" || ch === "}" || ch === "]") {
84863
+ if (stack[stack.length - 1] !== CLOSE_TO_OPEN[ch])
84864
+ return { deficit: 0, regexSkips };
84865
+ stack.pop();
84866
+ }
84867
+ }
84868
+ const deficit = stack.length > 0 && stack.every((open) => open === "(") ? stack.length : 0;
84869
+ return { deficit, regexSkips };
84870
+ }
84871
+ function parsesAsAsyncStatement(line) {
84872
+ try {
84873
+ new Function(`return (async () => {
84874
+ ${line}
84875
+ })`);
84876
+ return true;
84877
+ } catch {
84878
+ return false;
84879
+ }
84880
+ }
84394
84881
  function closeUnclosedRegexLiterals(code) {
84395
84882
  const anchors = [
84396
84883
  { rx: /\b(?:waitForURL|toHaveURL)\(\//g, mode: "url" },
@@ -84429,12 +84916,33 @@ var require_discover_generator = __commonJS({
84429
84916
  const term = findRegexTerminator(code, slash, TERMINATORS[mode]);
84430
84917
  out.push(code.slice(cursor, slash));
84431
84918
  const raw = code.slice(slash + 1, term).replace(/\s+$/, "");
84919
+ let closedLiteral;
84432
84920
  if (mode === "url") {
84433
84921
  const escaped = raw.replace(/(?<!\\)\//g, "\\/");
84434
84922
  const finalBody = escaped.startsWith("\\/") ? escaped : `\\/${escaped}`;
84435
- out.push(`/${finalBody}/`);
84923
+ closedLiteral = `/${finalBody}/`;
84436
84924
  } else {
84437
- out.push(`/${raw}/`);
84925
+ closedLiteral = `/${raw}/`;
84926
+ }
84927
+ out.push(closedLiteral);
84928
+ if (code[term] === ")") {
84929
+ const lineStart = code.lastIndexOf("\n", slash) + 1;
84930
+ const nextNewline = code.indexOf("\n", term);
84931
+ const lineEnd = nextNewline === -1 ? code.length : nextNewline;
84932
+ const tail = code.slice(term, lineEnd);
84933
+ const physicalLine = code.slice(lineStart, lineEnd);
84934
+ const lineIsPlain = !physicalLine.includes("`") && !physicalLine.includes("/*") && !physicalLine.includes("*/");
84935
+ const soleAnchorOnLine = !hits.some((h) => h.slash !== slash && h.slash >= lineStart && h.slash < lineEnd);
84936
+ if (lineIsPlain && soleAnchorOnLine && /;\s*$/.test(tail)) {
84937
+ const repairedLine = code.slice(lineStart, slash) + "/x/" + tail;
84938
+ const { deficit, regexSkips } = lineParenDeficit(repairedLine);
84939
+ if (deficit > 0 && deficit <= 3 && regexSkips === 1) {
84940
+ const candidateLine = code.slice(lineStart, slash) + closedLiteral + ")".repeat(deficit) + tail;
84941
+ if (parsesAsAsyncStatement(candidateLine)) {
84942
+ out.push(")".repeat(deficit));
84943
+ }
84944
+ }
84945
+ }
84438
84946
  }
84439
84947
  cursor = term;
84440
84948
  }
@@ -87135,7 +87643,15 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
87135
87643
  tools: openaiTools,
87136
87644
  tool_choice: "auto",
87137
87645
  temperature: 0.2,
87138
- max_tokens: 8192
87646
+ // Shared provider-aware output ceiling (same helper the survey planner uses):
87647
+ // generous where the provider's max output is verified large (deepseek/minimax
87648
+ // 65536), conservative elsewhere (16384). The old flat 8192 truncated large
87649
+ // finish_generate emissions mid-JSON — the loop re-prompted on the parse error
87650
+ // (line ~4744) and, for a test that genuinely needs >8192 output tokens, could
87651
+ // never finish before the hard stop → zero tests for that scenario. It's a
87652
+ // ceiling, not a target (calls finish at finish_reason:'stop' well under it), so
87653
+ // this adds no per-call cost.
87654
+ max_tokens: (0, discover_ai_planner_js_1.getAIPlannerMaxTokens)(agentModel)
87139
87655
  }, { signal });
87140
87656
  }), {
87141
87657
  label: `scenario-gen-iter-${iteration}`,
@@ -321746,6 +322262,7 @@ var require_mobile_discovery_agent = __commonJS({
321746
322262
  var mobile_exploration_state_js_1 = require_mobile_exploration_state();
321747
322263
  var mobile_discovery_auth_js_1 = require_mobile_discovery_auth();
321748
322264
  var mobile_driver_errors_js_1 = require_mobile_driver_errors();
322265
+ var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
321749
322266
  var shared_1 = (init_dist(), __toCommonJS(dist_exports));
321750
322267
  var discover_planner_js_1 = require_discover_planner();
321751
322268
  function checkpointErrorMessage(err) {
@@ -322046,7 +322563,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
322046
322563
  const graph = state2.toScreenGraph();
322047
322564
  const screenMap = state2.buildInMemoryScreenMap();
322048
322565
  const testBudget = (0, discover_planner_js_1.computeTestBudget)(explore.exploreStats.pagesVisited, ctx.focusAreas);
322049
- const planResult = await runPlan({
322566
+ const planResult = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(driver, () => runPlan({
322050
322567
  graph,
322051
322568
  mode,
322052
322569
  projectId: ctx.projectId,
@@ -322070,7 +322587,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
322070
322587
  }, {
322071
322588
  modelOverride: ctx.planModel ?? ctx.generateModel,
322072
322589
  onAICall
322073
- });
322590
+ }));
322074
322591
  logs2.push(...planResult.logs);
322075
322592
  log2(`
322076
322593
  Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.length} feature groups (budget: ${testBudget})`);
@@ -322928,6 +323445,7 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
322928
323445
  if (surveyAuthRecipe) {
322929
323446
  logs2.push(`Auth-prologue: captured + verified login recipe ${surveyAuthRecipe.recipeHash} during survey \u2014 persisting for reuse on later runs`);
322930
323447
  }
323448
+ const plannerFailedError = planResult.plannerFailed ? `Survey planning failed (${planResult.plannerFailureKind ?? "unknown"}): ${planResult.fallbackReason ?? "planner did not produce a usable plan"}. Explore evidence was saved; re-run the survey to retry planning.` : void 0;
322931
323449
  return {
322932
323450
  observedOrigins: mergeObservedOrigins(exploreResult.observedOrigins, loginObservedOrigins),
322933
323451
  resolvedAppOrigin: surveyAuthRecipe?.appOrigin ?? void 0,
@@ -322943,7 +323461,8 @@ Health: ${surveyHealth.totalIssues} issue(s) - ${surveyHealth.consoleErrors.leng
322943
323461
  exploreTruncatedReason: exploreResult.truncatedReason,
322944
323462
  fallbackUsed: planResult.fallbackUsed ?? false,
322945
323463
  fallbackReason: planResult.fallbackReason,
322946
- featureGroups,
323464
+ error: plannerFailedError,
323465
+ featureGroups: plannerFailedError ? void 0 : featureGroups,
322947
323466
  apiCalls: discoveryApiCalls.length > 0 ? discoveryApiCalls : void 0,
322948
323467
  healthObservations: surveyHealth,
322949
323468
  exploreFinalMessages: exploreResult.finalMessages,
@@ -336801,7 +337320,7 @@ var require_package4 = __commonJS({
336801
337320
  "package.json"(exports2, module2) {
336802
337321
  module2.exports = {
336803
337322
  name: "@validate.qa/runner",
336804
- version: "1.0.39",
337323
+ version: "1.0.41",
336805
337324
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
336806
337325
  bin: {
336807
337326
  "validate-runner": "dist/cli.js"
@@ -336810,7 +337329,7 @@ var require_package4 = __commonJS({
336810
337329
  build: "tsup",
336811
337330
  dev: "tsx src/cli.ts",
336812
337331
  test: "node --import tsx --test src/version-check.test.ts",
336813
- "test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
337332
+ "test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
336814
337333
  prepublishOnly: "npm run build"
336815
337334
  },
336816
337335
  files: [
@@ -342958,6 +343477,17 @@ var MobileBridgeDriver = class {
342958
343477
  return size;
342959
343478
  });
342960
343479
  }
343480
+ /**
343481
+ * Recovery-free liveness probe for keep-alive ticks (mobile-session-keepalive).
343482
+ * Deliberately NOT wrapped in withSessionRecovery: keep-alive ticks are
343483
+ * fire-and-forget, so a failing probe must never consume the bounded recovery
343484
+ * budget or relaunch the app from the background — a genuinely dead session
343485
+ * surfaces (with its precise error) on the caller's next REAL command, which
343486
+ * still runs the full recovery path.
343487
+ */
343488
+ async keepAlivePing() {
343489
+ await this.assertSessionReachable();
343490
+ }
342961
343491
  // ── Action result wrapper ──────────────────────────────
342962
343492
  /**
342963
343493
  * Run an action body, let the UI tree settle, then capture the post-action
@@ -345515,6 +346045,7 @@ ${finalVerifyResult.logs ?? ""}`;
345515
346045
  setExecutorBusy(true);
345516
346046
  let session = null;
345517
346047
  let capture = null;
346048
+ let discoveryTeardownStarted = false;
345518
346049
  try {
345519
346050
  const discoverySessionTarget = {
345520
346051
  appId: mobileTarget.appId,
@@ -345566,6 +346097,9 @@ ${finalVerifyResult.logs ?? ""}`;
345566
346097
  allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
345567
346098
  maxSessionRecoveries: 1,
345568
346099
  recoverSession: async (cause) => {
346100
+ if (discoveryTeardownStarted) {
346101
+ throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
346102
+ }
345569
346103
  const lost = session;
345570
346104
  mobileOnLog(
345571
346105
  `[appium] discovery session lost (${cause.message}); recreating once on the leased device`
@@ -345579,6 +346113,13 @@ ${finalVerifyResult.logs ?? ""}`;
345579
346113
  }
345580
346114
  }
345581
346115
  const replacement = await openDiscoverySession();
346116
+ if (discoveryTeardownStarted) {
346117
+ try {
346118
+ await replacement.stop();
346119
+ } catch {
346120
+ }
346121
+ throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
346122
+ }
345582
346123
  session = replacement;
345583
346124
  attachMirrorSession(replacement.sessionId, platform3, replacement.deviceId);
345584
346125
  liveBrowserHandle.patch({
@@ -345618,10 +346159,23 @@ ${finalVerifyResult.logs ?? ""}`;
345618
346159
  onPlanReady
345619
346160
  });
345620
346161
  } finally {
346162
+ discoveryTeardownStarted = true;
346163
+ const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
346164
+ const timeboxed = async (step) => Promise.race([
346165
+ step,
346166
+ new Promise((resolve2) => {
346167
+ const t = setTimeout(() => resolve2(TEARDOWN_STEP_TIMEOUT), 45e3);
346168
+ t.unref?.();
346169
+ })
346170
+ ]);
345621
346171
  const activeCapture = capture;
345622
346172
  if (activeCapture) {
345623
346173
  try {
345624
- const apiCalls = await activeCapture.stop();
346174
+ const stopped = await timeboxed(activeCapture.stop());
346175
+ if (stopped === TEARDOWN_STEP_TIMEOUT) {
346176
+ throw new Error("capture stop timed out after 45s \u2014 continuing teardown without apiCalls");
346177
+ }
346178
+ const apiCalls = stopped;
345625
346179
  const captureHealth = activeCapture.health();
345626
346180
  if (discoveryResult) {
345627
346181
  discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session?.appId ?? mobileTarget.appId) : void 0;
@@ -345641,8 +346195,12 @@ ${finalVerifyResult.logs ?? ""}`;
345641
346195
  }
345642
346196
  if (session) {
345643
346197
  detachMirrorSession(session.deviceId);
346198
+ const closing = session;
345644
346199
  try {
345645
- await session.stop();
346200
+ const stopped = await timeboxed(closing.stop());
346201
+ if (stopped === TEARDOWN_STEP_TIMEOUT) {
346202
+ mobileOnLog(`[appium] session ${closing.sessionId} stop timed out after 45s \u2014 releasing device ${closing.deviceId} anyway; Appium may hold the session until newCommandTimeout reaps it`);
346203
+ }
345646
346204
  } catch {
345647
346205
  }
345648
346206
  }
@@ -346080,7 +346638,13 @@ ${finalVerifyResult.logs ?? ""}`;
346080
346638
  relaunch: async () => {
346081
346639
  await currentDriver().relaunch();
346082
346640
  },
346083
- resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState()
346641
+ resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState(),
346642
+ // Recovery-free liveness probe so the loop's keep-alive can span
346643
+ // long AI waits without touching the driver's recovery machinery
346644
+ // (see mobile-session-keepalive.ts).
346645
+ keepAlivePing: async () => {
346646
+ await currentDriver().keepAlivePing();
346647
+ }
346084
346648
  };
346085
346649
  const runNativeWithSessionHandoff = async (verifyArgs) => {
346086
346650
  await closeLiveSession();