@validate.qa/runner 1.0.8 → 1.0.12

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 +150 -39
  2. package/dist/cli.mjs +150 -39
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -263,14 +263,16 @@ var require_ai_provider = __commonJS({
263
263
  { id: "minimax-m2.7", label: "MiniMax M2.7", provider: "minimax", description: "Fast M2.7 tier (Highspeed); auto-fails over to standard M2.7, ~200K ctx", contextWindow: 2e5 },
264
264
  { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
265
265
  { id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
266
- { 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 }
266
+ { 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 },
267
+ { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
267
268
  ];
268
269
  var PROVIDER_DEFAULT_MODEL = {
269
270
  openai: "gpt-4o",
270
271
  xai: "grok-build-0.1",
271
272
  anthropic: "claude-sonnet-4-6",
272
273
  minimax: "minimax-m3",
273
- nvidia: "nvidia-nemotron-nano"
274
+ nvidia: "nvidia-nemotron-nano",
275
+ deepseek: "deepseek-v4-flash"
274
276
  };
275
277
  function getContextConfig(modelId) {
276
278
  const info = exports2.AI_MODEL_CATALOG.find((m) => m.id === modelId);
@@ -321,7 +323,8 @@ var require_ai_provider = __commonJS({
321
323
  "minimax-m2.7-highspeed": { strong: "MiniMax-M2.7-highspeed", fast: "MiniMax-M2.7-highspeed", agent: "MiniMax-M2.7-highspeed" },
322
324
  "claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
323
325
  "nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
324
- "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" }
326
+ "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
327
+ "deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
325
328
  };
326
329
  var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
327
330
  function withModelContext(modelId, fn) {
@@ -383,6 +386,9 @@ var require_ai_provider = __commonJS({
383
386
  if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
384
387
  return { chat_template_kwargs: { enable_thinking: false } };
385
388
  }
389
+ if (modelId === "deepseek-v4-flash") {
390
+ return { thinking: { type: "disabled" } };
391
+ }
386
392
  return {};
387
393
  }
388
394
  exports2.MINIMAX_FALLBACK_CHAIN = ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"];
@@ -484,6 +490,11 @@ var require_ai_provider = __commonJS({
484
490
  if (!apiKey)
485
491
  throw new Error("NVIDIA_API_KEY is required when using an NVIDIA model");
486
492
  client = new openai_1.default({ apiKey, baseURL: process.env.NVIDIA_API_BASE ?? "https://integrate.api.nvidia.com/v1" });
493
+ } else if (provider === "deepseek") {
494
+ const apiKey = process.env.DEEPSEEK_API_KEY;
495
+ if (!apiKey)
496
+ throw new Error("DEEPSEEK_API_KEY is required when using a DeepSeek model");
497
+ client = new openai_1.default({ apiKey, baseURL: process.env.DEEPSEEK_API_BASE ?? "https://api.deepseek.com" });
487
498
  } else {
488
499
  const apiKey = process.env.OPENAI_API_KEY;
489
500
  if (!apiKey)
@@ -517,6 +528,8 @@ var require_ai_provider = __commonJS({
517
528
  return !!process.env.ANTHROPIC_API_KEY?.trim();
518
529
  if (provider === "nvidia")
519
530
  return !!process.env.NVIDIA_API_KEY?.trim();
531
+ if (provider === "deepseek")
532
+ return !!process.env.DEEPSEEK_API_KEY?.trim();
520
533
  return false;
521
534
  }
522
535
  function getAvailableModels() {
@@ -1544,6 +1557,21 @@ function buildAppiumCode(testName, target, steps) {
1544
1557
  ` await driver.keys(text);`,
1545
1558
  ` }`,
1546
1559
  `}`,
1560
+ ``,
1561
+ `async function __textCandidates(el: WebdriverIO.Element) {`,
1562
+ ` const __values: string[] = [];`,
1563
+ ` const __push = (__value: unknown) => {`,
1564
+ ` if (typeof __value !== 'string') return;`,
1565
+ ` const __trimmed = __value.trim();`,
1566
+ ` if (__trimmed.length === 0 || __values.includes(__trimmed)) return;`,
1567
+ ` __values.push(__trimmed);`,
1568
+ ` };`,
1569
+ ` __push(await el.getText().catch(() => ''));`,
1570
+ ` for (const __attr of ['text', 'content-desc', 'contentDescription', 'name', 'label', 'value']) {`,
1571
+ ` __push(await el.getAttribute(__attr).catch(() => ''));`,
1572
+ ` }`,
1573
+ ` return __values;`,
1574
+ `}`,
1547
1575
  ...hasSwipe ? [
1548
1576
  ``,
1549
1577
  `// Read the device window with a few retries (matches the runtime executor).`,
@@ -1630,8 +1658,8 @@ function buildAppiumCode(testName, target, steps) {
1630
1658
  } else if (step.action === "assertText" && hasSelector) {
1631
1659
  lines.push(` {`);
1632
1660
  lines.push(` const __expected = ${toJsStringLiteral(step.assertion ?? "")};`);
1633
- lines.push(` const __actual = await (await findFirst(driver, ${candidatesLiteral})).getText();`);
1634
- lines.push(` if (!__actual.includes(__expected)) {`);
1661
+ lines.push(` const __actual = await __textCandidates(await findFirst(driver, ${candidatesLiteral}));`);
1662
+ lines.push(` if (!__actual.some((__value) => __value.includes(__expected))) {`);
1635
1663
  lines.push(` throw new Error('Assertion failed: expected text to contain ' + JSON.stringify(__expected) + ' but got ' + JSON.stringify(__actual));`);
1636
1664
  lines.push(` }`);
1637
1665
  lines.push(` }`);
@@ -12038,7 +12066,7 @@ var require_kill_tree = __commonJS({
12038
12066
  var node_child_process_1 = require("child_process");
12039
12067
  var node_util_1 = require("util");
12040
12068
  var node_fs_1 = require("fs");
12041
- var execFileAsync2 = (0, node_util_1.promisify)(node_child_process_1.execFile);
12069
+ var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
12042
12070
  var PS_TIMEOUT_MS = 2e3;
12043
12071
  var PS_BUFFER = 1024 * 1024;
12044
12072
  async function gracefulCloseWithTreeKill(options) {
@@ -12092,7 +12120,7 @@ var require_kill_tree = __commonJS({
12092
12120
  timer.unref?.();
12093
12121
  let stdout;
12094
12122
  try {
12095
- const result = await execFileAsync2("ps", ["-A", "-o", "pid=,ppid=,command="], {
12123
+ const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
12096
12124
  maxBuffer: PS_BUFFER,
12097
12125
  signal: controller.signal
12098
12126
  });
@@ -12130,7 +12158,7 @@ var require_kill_tree = __commonJS({
12130
12158
  timer.unref?.();
12131
12159
  let stdout;
12132
12160
  try {
12133
- const result = await execFileAsync2("ps", ["-A", "-o", "pid=,ppid=,command="], {
12161
+ const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
12134
12162
  maxBuffer: PS_BUFFER,
12135
12163
  signal: controller.signal
12136
12164
  });
@@ -12207,7 +12235,7 @@ var require_kill_tree = __commonJS({
12207
12235
  timer.unref?.();
12208
12236
  let stdout;
12209
12237
  try {
12210
- const result = await execFileAsync2("ps", ["-A", "-o", "pid=,ppid="], {
12238
+ const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid="], {
12211
12239
  maxBuffer: PS_BUFFER,
12212
12240
  signal: controller.signal
12213
12241
  });
@@ -311576,6 +311604,13 @@ var require_mobile_discovery_agent = __commonJS({
311576
311604
  var mobile_generator_js_1 = require_mobile_generator();
311577
311605
  var mobile_exploration_state_js_1 = require_mobile_exploration_state();
311578
311606
  var discover_planner_js_1 = require_discover_planner();
311607
+ function checkpointErrorMessage(err) {
311608
+ return err instanceof Error ? err.message : String(err);
311609
+ }
311610
+ function isTerminalDiscoveryCheckpointError(err) {
311611
+ const message = checkpointErrorMessage(err);
311612
+ return /Discovery plan checkpoint rejected|Discovery run is .*not RUNNING|not RUNNING|cancelled/i.test(message);
311613
+ }
311579
311614
  var EXPLORE_BUDGET_DEEP = 60;
311580
311615
  var EXPLORE_BUDGET_SURVEY = 40;
311581
311616
  function platformForMedium(medium) {
@@ -311706,14 +311741,22 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
311706
311741
  const plans = toPlans(scenarios);
311707
311742
  const coverageGaps = planResult.coverageGaps && planResult.coverageGaps.length > 0 ? planResult.coverageGaps : void 0;
311708
311743
  if (callbacks?.onPlanReady) {
311709
- await callbacks.onPlanReady({
311710
- scenarios,
311711
- featureGroups,
311712
- fallbackUsed: planResult.fallbackUsed ?? false,
311713
- fallbackReason: planResult.fallbackReason
311714
- }).then(() => log2("Plan checkpoint: scenario plan POSTed to server")).catch((err) => {
311715
- log2(`Plan checkpoint POST failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
311716
- });
311744
+ try {
311745
+ await callbacks.onPlanReady({
311746
+ scenarios,
311747
+ featureGroups,
311748
+ fallbackUsed: planResult.fallbackUsed ?? false,
311749
+ fallbackReason: planResult.fallbackReason
311750
+ });
311751
+ log2("Plan checkpoint: scenario plan POSTed to server");
311752
+ } catch (err) {
311753
+ const message = checkpointErrorMessage(err);
311754
+ if (isTerminalDiscoveryCheckpointError(err)) {
311755
+ log2(`Plan checkpoint rejected for inactive discovery run: ${message}`);
311756
+ throw err;
311757
+ }
311758
+ log2(`Plan checkpoint POST failed (non-fatal): ${message}`);
311759
+ }
311717
311760
  }
311718
311761
  if (isSurvey) {
311719
311762
  const duration2 = (Date.now() - startTime) / 1e3;
@@ -321095,7 +321138,7 @@ var require_update = __commonJS({
321095
321138
  var require_opts_arg = __commonJS({
321096
321139
  "../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports2, module2) {
321097
321140
  "use strict";
321098
- var { promisify: promisify2 } = require("util");
321141
+ var { promisify: promisify3 } = require("util");
321099
321142
  var fs = require("fs");
321100
321143
  var optsArg = (opts) => {
321101
321144
  if (!opts)
@@ -321109,9 +321152,9 @@ var require_opts_arg = __commonJS({
321109
321152
  else
321110
321153
  throw new TypeError("invalid options argument");
321111
321154
  opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
321112
- opts.mkdirAsync = promisify2(opts.mkdir);
321155
+ opts.mkdirAsync = promisify3(opts.mkdir);
321113
321156
  opts.stat = opts.stat || opts.fs.stat || fs.stat;
321114
- opts.statAsync = promisify2(opts.stat);
321157
+ opts.statAsync = promisify3(opts.stat);
321115
321158
  opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
321116
321159
  opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
321117
321160
  return opts;
@@ -325204,7 +325247,7 @@ var require_process_inventory = __commonJS({
325204
325247
  var node_child_process_1 = require("child_process");
325205
325248
  var node_os_1 = __importDefault(require("os"));
325206
325249
  var node_util_1 = require("util");
325207
- var execFileAsync2 = (0, node_util_1.promisify)(node_child_process_1.execFile);
325250
+ var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
325208
325251
  var DEFAULT_LIMIT = 60;
325209
325252
  var DEFAULT_TIMEOUT_MS = 800;
325210
325253
  var MAX_COMMAND_LENGTH = 320;
@@ -325266,7 +325309,7 @@ var require_process_inventory = __commonJS({
325266
325309
  const timer = setTimeout(() => controller.abort(), timeoutMs);
325267
325310
  timer.unref?.();
325268
325311
  try {
325269
- const { stdout } = await execFileAsync2(command, args, {
325312
+ const { stdout } = await execFileAsync3(command, args, {
325270
325313
  maxBuffer: 1024 * 1024,
325271
325314
  signal: controller.signal
325272
325315
  });
@@ -326288,7 +326331,7 @@ var require_package4 = __commonJS({
326288
326331
  "package.json"(exports2, module2) {
326289
326332
  module2.exports = {
326290
326333
  name: "@validate.qa/runner",
326291
- version: "1.0.8",
326334
+ version: "1.0.12",
326292
326335
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
326293
326336
  bin: {
326294
326337
  "validate-runner": "dist/cli.js"
@@ -326421,6 +326464,8 @@ function shouldUseAuthState(tags, playwrightCode) {
326421
326464
 
326422
326465
  // src/services/appium-executor.ts
326423
326466
  init_dist();
326467
+ var import_node_child_process2 = require("child_process");
326468
+ var import_node_util2 = require("util");
326424
326469
  var import_runner_core8 = __toESM(require_dist2());
326425
326470
 
326426
326471
  // src/services/appium-lifecycle.ts
@@ -327601,6 +327646,8 @@ async function startMobileNetworkCapture(options) {
327601
327646
 
327602
327647
  // src/services/appium-executor.ts
327603
327648
  var DEFAULT_STEP_TIMEOUT_MS = 1e4;
327649
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
327650
+ var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
327604
327651
  var WebDriverTransportError = class extends Error {
327605
327652
  isTransport = true;
327606
327653
  constructor(message) {
@@ -327922,6 +327969,18 @@ async function sendKeysToFocusedInput(sessionId, value) {
327922
327969
  body: JSON.stringify({ text: value, value: Array.from(value) })
327923
327970
  });
327924
327971
  }
327972
+ function encodeAndroidInputText(value) {
327973
+ return value.replace(/%/g, "%25").replace(/\s/g, "%s");
327974
+ }
327975
+ async function sendAndroidTextToFocusedInput(deviceId, value) {
327976
+ if (!deviceId) {
327977
+ throw new Error("Android keyboard fallback requires a device id");
327978
+ }
327979
+ await execFileAsync2("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
327980
+ timeout: 1e4,
327981
+ windowsHide: true
327982
+ });
327983
+ }
327925
327984
  async function trySendElementValue(sessionId, elementId, value) {
327926
327985
  try {
327927
327986
  await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
@@ -327970,7 +328029,7 @@ async function executeTap(sessionId, step) {
327970
328029
  }
327971
328030
  await tapCoordinates(sessionId, bounds);
327972
328031
  }
327973
- async function executeType(sessionId, step) {
328032
+ async function executeType(sessionId, step, context) {
327974
328033
  if (step.value === void 0 || step.value === null || step.value === "") return;
327975
328034
  const value = String(step.value);
327976
328035
  const bounds = boundsFromStep(step);
@@ -328019,6 +328078,7 @@ async function executeType(sessionId, step) {
328019
328078
  if (bounds) {
328020
328079
  if (!tappedBounds) {
328021
328080
  await tapCoordinates(sessionId, bounds);
328081
+ tappedBounds = true;
328022
328082
  await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
328023
328083
  }
328024
328084
  try {
@@ -328029,6 +328089,17 @@ async function executeType(sessionId, step) {
328029
328089
  lastInputError = error2;
328030
328090
  }
328031
328091
  }
328092
+ if (context.platform === "ANDROID" && tappedBounds) {
328093
+ try {
328094
+ context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
328095
+ await sendAndroidTextToFocusedInput(context.deviceId, value);
328096
+ await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
328097
+ return;
328098
+ } catch (error2) {
328099
+ if (isTransportError(error2)) throw error2;
328100
+ lastInputError = error2;
328101
+ }
328102
+ }
328032
328103
  throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
328033
328104
  }
328034
328105
  async function executeClear(sessionId, step) {
@@ -328123,9 +328194,29 @@ async function executeAssertText(sessionId, step) {
328123
328194
  const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
328124
328195
  const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
328125
328196
  const actualText = typeof result?.value === "string" ? result.value : "";
328126
- if (!actualText.includes(step.assertion)) {
328127
- throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in "${actualText}"`);
328197
+ const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
328198
+ if (!textCandidates.some((value) => value.includes(step.assertion))) {
328199
+ throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in ${JSON.stringify(textCandidates)}`);
328200
+ }
328201
+ }
328202
+ async function readElementTextCandidates(sessionId, elementId, actualText) {
328203
+ const values = [];
328204
+ const push = (value) => {
328205
+ if (typeof value !== "string") return;
328206
+ const trimmed = value.trim();
328207
+ if (trimmed.length === 0 || values.includes(trimmed)) return;
328208
+ values.push(trimmed);
328209
+ };
328210
+ push(actualText);
328211
+ for (const attr of MOBILE_TEXT_ATTRIBUTE_NAMES) {
328212
+ try {
328213
+ const result = await wdRequest(`/session/${sessionId}/element/${elementId}/attribute/${encodeURIComponent(attr)}`);
328214
+ push(result?.value);
328215
+ } catch (err) {
328216
+ if (isTransportError(err)) throw err;
328217
+ }
328128
328218
  }
328219
+ return values;
328129
328220
  }
328130
328221
  async function executeMobileTest(options) {
328131
328222
  const logLines = [];
@@ -328250,7 +328341,11 @@ async function executeMobileTest(options) {
328250
328341
  await executeTap(sessionId, step);
328251
328342
  break;
328252
328343
  case "type":
328253
- await executeType(sessionId, step);
328344
+ await executeType(sessionId, step, {
328345
+ platform: options.target.platform,
328346
+ deviceId: selectedDevice.id,
328347
+ log: log2
328348
+ });
328254
328349
  break;
328255
328350
  case "clear":
328256
328351
  await executeClear(sessionId, step);
@@ -332009,23 +332104,39 @@ ${finalVerifyResult.logs ?? ""}`;
332009
332104
  log.fail("ERROR - No mobile steps");
332010
332105
  return;
332011
332106
  }
332107
+ const maxRetries = typeof run2.retryCount === "number" && Number.isFinite(run2.retryCount) ? Math.max(0, Math.min(2, Math.floor(run2.retryCount))) : 0;
332012
332108
  setExecutorBusy(true);
332013
- let result2;
332109
+ let result2 = null;
332110
+ const attemptLogs = [];
332014
332111
  try {
332015
- result2 = await executeMobileTest({
332016
- steps: mobileSteps,
332017
- target: {
332018
- appId: target.appId,
332019
- platform: target.platform,
332020
- platformVersion: target.platformVersion,
332021
- recordedDeviceId: target.recordedDeviceId,
332022
- launchMode: target.launchMode,
332023
- deeplink: target.deeplink
332024
- }
332025
- });
332112
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
332113
+ if (attempt > 0) {
332114
+ log.info(`Retrying mobile test after failed Appium step (${attempt}/${maxRetries})`);
332115
+ }
332116
+ result2 = await executeMobileTest({
332117
+ steps: mobileSteps,
332118
+ target: {
332119
+ appId: target.appId,
332120
+ platform: target.platform,
332121
+ platformVersion: target.platformVersion,
332122
+ recordedDeviceId: target.recordedDeviceId,
332123
+ launchMode: target.launchMode,
332124
+ deeplink: target.deeplink
332125
+ }
332126
+ });
332127
+ attemptLogs.push(`--- Appium attempt ${attempt + 1}/${maxRetries + 1} ---
332128
+ ${result2.logs}`);
332129
+ if (result2.passed || result2.environmental || attempt >= maxRetries) break;
332130
+ }
332026
332131
  } finally {
332027
332132
  setExecutorBusy(false);
332028
332133
  }
332134
+ if (!result2) {
332135
+ throw new Error("Appium execution did not produce a result.");
332136
+ }
332137
+ if (attemptLogs.length > 1) {
332138
+ result2 = { ...result2, logs: attemptLogs.join("\n\n") };
332139
+ }
332029
332140
  const duration2 = Date.now() - startTime;
332030
332141
  const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
332031
332142
  const reportedError = result2.environmental ? `CONFIG_ISSUE: ${result2.environmental}` : result2.error ?? void 0;
package/dist/cli.mjs CHANGED
@@ -268,14 +268,16 @@ var require_ai_provider = __commonJS({
268
268
  { id: "minimax-m2.7", label: "MiniMax M2.7", provider: "minimax", description: "Fast M2.7 tier (Highspeed); auto-fails over to standard M2.7, ~200K ctx", contextWindow: 2e5 },
269
269
  { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
270
270
  { id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
271
- { 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 }
271
+ { 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 },
272
+ { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
272
273
  ];
273
274
  var PROVIDER_DEFAULT_MODEL = {
274
275
  openai: "gpt-4o",
275
276
  xai: "grok-build-0.1",
276
277
  anthropic: "claude-sonnet-4-6",
277
278
  minimax: "minimax-m3",
278
- nvidia: "nvidia-nemotron-nano"
279
+ nvidia: "nvidia-nemotron-nano",
280
+ deepseek: "deepseek-v4-flash"
279
281
  };
280
282
  function getContextConfig(modelId) {
281
283
  const info = exports.AI_MODEL_CATALOG.find((m) => m.id === modelId);
@@ -326,7 +328,8 @@ var require_ai_provider = __commonJS({
326
328
  "minimax-m2.7-highspeed": { strong: "MiniMax-M2.7-highspeed", fast: "MiniMax-M2.7-highspeed", agent: "MiniMax-M2.7-highspeed" },
327
329
  "claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
328
330
  "nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
329
- "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" }
331
+ "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
332
+ "deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
330
333
  };
331
334
  var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
332
335
  function withModelContext(modelId, fn) {
@@ -388,6 +391,9 @@ var require_ai_provider = __commonJS({
388
391
  if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
389
392
  return { chat_template_kwargs: { enable_thinking: false } };
390
393
  }
394
+ if (modelId === "deepseek-v4-flash") {
395
+ return { thinking: { type: "disabled" } };
396
+ }
391
397
  return {};
392
398
  }
393
399
  exports.MINIMAX_FALLBACK_CHAIN = ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"];
@@ -489,6 +495,11 @@ var require_ai_provider = __commonJS({
489
495
  if (!apiKey)
490
496
  throw new Error("NVIDIA_API_KEY is required when using an NVIDIA model");
491
497
  client = new openai_1.default({ apiKey, baseURL: process.env.NVIDIA_API_BASE ?? "https://integrate.api.nvidia.com/v1" });
498
+ } else if (provider === "deepseek") {
499
+ const apiKey = process.env.DEEPSEEK_API_KEY;
500
+ if (!apiKey)
501
+ throw new Error("DEEPSEEK_API_KEY is required when using a DeepSeek model");
502
+ client = new openai_1.default({ apiKey, baseURL: process.env.DEEPSEEK_API_BASE ?? "https://api.deepseek.com" });
492
503
  } else {
493
504
  const apiKey = process.env.OPENAI_API_KEY;
494
505
  if (!apiKey)
@@ -522,6 +533,8 @@ var require_ai_provider = __commonJS({
522
533
  return !!process.env.ANTHROPIC_API_KEY?.trim();
523
534
  if (provider === "nvidia")
524
535
  return !!process.env.NVIDIA_API_KEY?.trim();
536
+ if (provider === "deepseek")
537
+ return !!process.env.DEEPSEEK_API_KEY?.trim();
525
538
  return false;
526
539
  }
527
540
  function getAvailableModels() {
@@ -1549,6 +1562,21 @@ function buildAppiumCode(testName, target, steps) {
1549
1562
  ` await driver.keys(text);`,
1550
1563
  ` }`,
1551
1564
  `}`,
1565
+ ``,
1566
+ `async function __textCandidates(el: WebdriverIO.Element) {`,
1567
+ ` const __values: string[] = [];`,
1568
+ ` const __push = (__value: unknown) => {`,
1569
+ ` if (typeof __value !== 'string') return;`,
1570
+ ` const __trimmed = __value.trim();`,
1571
+ ` if (__trimmed.length === 0 || __values.includes(__trimmed)) return;`,
1572
+ ` __values.push(__trimmed);`,
1573
+ ` };`,
1574
+ ` __push(await el.getText().catch(() => ''));`,
1575
+ ` for (const __attr of ['text', 'content-desc', 'contentDescription', 'name', 'label', 'value']) {`,
1576
+ ` __push(await el.getAttribute(__attr).catch(() => ''));`,
1577
+ ` }`,
1578
+ ` return __values;`,
1579
+ `}`,
1552
1580
  ...hasSwipe ? [
1553
1581
  ``,
1554
1582
  `// Read the device window with a few retries (matches the runtime executor).`,
@@ -1635,8 +1663,8 @@ function buildAppiumCode(testName, target, steps) {
1635
1663
  } else if (step.action === "assertText" && hasSelector) {
1636
1664
  lines.push(` {`);
1637
1665
  lines.push(` const __expected = ${toJsStringLiteral(step.assertion ?? "")};`);
1638
- lines.push(` const __actual = await (await findFirst(driver, ${candidatesLiteral})).getText();`);
1639
- lines.push(` if (!__actual.includes(__expected)) {`);
1666
+ lines.push(` const __actual = await __textCandidates(await findFirst(driver, ${candidatesLiteral}));`);
1667
+ lines.push(` if (!__actual.some((__value) => __value.includes(__expected))) {`);
1640
1668
  lines.push(` throw new Error('Assertion failed: expected text to contain ' + JSON.stringify(__expected) + ' but got ' + JSON.stringify(__actual));`);
1641
1669
  lines.push(` }`);
1642
1670
  lines.push(` }`);
@@ -12043,7 +12071,7 @@ var require_kill_tree = __commonJS({
12043
12071
  var node_child_process_1 = __require("child_process");
12044
12072
  var node_util_1 = __require("util");
12045
12073
  var node_fs_1 = __require("fs");
12046
- var execFileAsync2 = (0, node_util_1.promisify)(node_child_process_1.execFile);
12074
+ var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
12047
12075
  var PS_TIMEOUT_MS = 2e3;
12048
12076
  var PS_BUFFER = 1024 * 1024;
12049
12077
  async function gracefulCloseWithTreeKill(options) {
@@ -12097,7 +12125,7 @@ var require_kill_tree = __commonJS({
12097
12125
  timer.unref?.();
12098
12126
  let stdout;
12099
12127
  try {
12100
- const result = await execFileAsync2("ps", ["-A", "-o", "pid=,ppid=,command="], {
12128
+ const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
12101
12129
  maxBuffer: PS_BUFFER,
12102
12130
  signal: controller.signal
12103
12131
  });
@@ -12135,7 +12163,7 @@ var require_kill_tree = __commonJS({
12135
12163
  timer.unref?.();
12136
12164
  let stdout;
12137
12165
  try {
12138
- const result = await execFileAsync2("ps", ["-A", "-o", "pid=,ppid=,command="], {
12166
+ const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
12139
12167
  maxBuffer: PS_BUFFER,
12140
12168
  signal: controller.signal
12141
12169
  });
@@ -12212,7 +12240,7 @@ var require_kill_tree = __commonJS({
12212
12240
  timer.unref?.();
12213
12241
  let stdout;
12214
12242
  try {
12215
- const result = await execFileAsync2("ps", ["-A", "-o", "pid=,ppid="], {
12243
+ const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid="], {
12216
12244
  maxBuffer: PS_BUFFER,
12217
12245
  signal: controller.signal
12218
12246
  });
@@ -311581,6 +311609,13 @@ var require_mobile_discovery_agent = __commonJS({
311581
311609
  var mobile_generator_js_1 = require_mobile_generator();
311582
311610
  var mobile_exploration_state_js_1 = require_mobile_exploration_state();
311583
311611
  var discover_planner_js_1 = require_discover_planner();
311612
+ function checkpointErrorMessage(err) {
311613
+ return err instanceof Error ? err.message : String(err);
311614
+ }
311615
+ function isTerminalDiscoveryCheckpointError(err) {
311616
+ const message = checkpointErrorMessage(err);
311617
+ return /Discovery plan checkpoint rejected|Discovery run is .*not RUNNING|not RUNNING|cancelled/i.test(message);
311618
+ }
311584
311619
  var EXPLORE_BUDGET_DEEP = 60;
311585
311620
  var EXPLORE_BUDGET_SURVEY = 40;
311586
311621
  function platformForMedium(medium) {
@@ -311711,14 +311746,22 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
311711
311746
  const plans = toPlans(scenarios);
311712
311747
  const coverageGaps = planResult.coverageGaps && planResult.coverageGaps.length > 0 ? planResult.coverageGaps : void 0;
311713
311748
  if (callbacks?.onPlanReady) {
311714
- await callbacks.onPlanReady({
311715
- scenarios,
311716
- featureGroups,
311717
- fallbackUsed: planResult.fallbackUsed ?? false,
311718
- fallbackReason: planResult.fallbackReason
311719
- }).then(() => log2("Plan checkpoint: scenario plan POSTed to server")).catch((err) => {
311720
- log2(`Plan checkpoint POST failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
311721
- });
311749
+ try {
311750
+ await callbacks.onPlanReady({
311751
+ scenarios,
311752
+ featureGroups,
311753
+ fallbackUsed: planResult.fallbackUsed ?? false,
311754
+ fallbackReason: planResult.fallbackReason
311755
+ });
311756
+ log2("Plan checkpoint: scenario plan POSTed to server");
311757
+ } catch (err) {
311758
+ const message = checkpointErrorMessage(err);
311759
+ if (isTerminalDiscoveryCheckpointError(err)) {
311760
+ log2(`Plan checkpoint rejected for inactive discovery run: ${message}`);
311761
+ throw err;
311762
+ }
311763
+ log2(`Plan checkpoint POST failed (non-fatal): ${message}`);
311764
+ }
311722
311765
  }
311723
311766
  if (isSurvey) {
311724
311767
  const duration2 = (Date.now() - startTime) / 1e3;
@@ -321100,7 +321143,7 @@ var require_update = __commonJS({
321100
321143
  var require_opts_arg = __commonJS({
321101
321144
  "../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports, module) {
321102
321145
  "use strict";
321103
- var { promisify: promisify2 } = __require("util");
321146
+ var { promisify: promisify3 } = __require("util");
321104
321147
  var fs = __require("fs");
321105
321148
  var optsArg = (opts) => {
321106
321149
  if (!opts)
@@ -321114,9 +321157,9 @@ var require_opts_arg = __commonJS({
321114
321157
  else
321115
321158
  throw new TypeError("invalid options argument");
321116
321159
  opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
321117
- opts.mkdirAsync = promisify2(opts.mkdir);
321160
+ opts.mkdirAsync = promisify3(opts.mkdir);
321118
321161
  opts.stat = opts.stat || opts.fs.stat || fs.stat;
321119
- opts.statAsync = promisify2(opts.stat);
321162
+ opts.statAsync = promisify3(opts.stat);
321120
321163
  opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
321121
321164
  opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
321122
321165
  return opts;
@@ -325209,7 +325252,7 @@ var require_process_inventory = __commonJS({
325209
325252
  var node_child_process_1 = __require("child_process");
325210
325253
  var node_os_1 = __importDefault(__require("os"));
325211
325254
  var node_util_1 = __require("util");
325212
- var execFileAsync2 = (0, node_util_1.promisify)(node_child_process_1.execFile);
325255
+ var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
325213
325256
  var DEFAULT_LIMIT = 60;
325214
325257
  var DEFAULT_TIMEOUT_MS = 800;
325215
325258
  var MAX_COMMAND_LENGTH = 320;
@@ -325271,7 +325314,7 @@ var require_process_inventory = __commonJS({
325271
325314
  const timer = setTimeout(() => controller.abort(), timeoutMs);
325272
325315
  timer.unref?.();
325273
325316
  try {
325274
- const { stdout } = await execFileAsync2(command, args, {
325317
+ const { stdout } = await execFileAsync3(command, args, {
325275
325318
  maxBuffer: 1024 * 1024,
325276
325319
  signal: controller.signal
325277
325320
  });
@@ -326293,7 +326336,7 @@ var require_package4 = __commonJS({
326293
326336
  "package.json"(exports, module) {
326294
326337
  module.exports = {
326295
326338
  name: "@validate.qa/runner",
326296
- version: "1.0.8",
326339
+ version: "1.0.12",
326297
326340
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
326298
326341
  bin: {
326299
326342
  "validate-runner": "dist/cli.js"
@@ -326427,6 +326470,8 @@ function shouldUseAuthState(tags, playwrightCode) {
326427
326470
  // src/services/appium-executor.ts
326428
326471
  init_dist();
326429
326472
  var import_runner_core8 = __toESM(require_dist2());
326473
+ import { execFile as execFile2 } from "child_process";
326474
+ import { promisify as promisify2 } from "util";
326430
326475
 
326431
326476
  // src/services/appium-lifecycle.ts
326432
326477
  import { spawn, execFileSync } from "child_process";
@@ -327609,6 +327654,8 @@ async function startMobileNetworkCapture(options) {
327609
327654
 
327610
327655
  // src/services/appium-executor.ts
327611
327656
  var DEFAULT_STEP_TIMEOUT_MS = 1e4;
327657
+ var execFileAsync2 = promisify2(execFile2);
327658
+ var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
327612
327659
  var WebDriverTransportError = class extends Error {
327613
327660
  isTransport = true;
327614
327661
  constructor(message) {
@@ -327930,6 +327977,18 @@ async function sendKeysToFocusedInput(sessionId, value) {
327930
327977
  body: JSON.stringify({ text: value, value: Array.from(value) })
327931
327978
  });
327932
327979
  }
327980
+ function encodeAndroidInputText(value) {
327981
+ return value.replace(/%/g, "%25").replace(/\s/g, "%s");
327982
+ }
327983
+ async function sendAndroidTextToFocusedInput(deviceId, value) {
327984
+ if (!deviceId) {
327985
+ throw new Error("Android keyboard fallback requires a device id");
327986
+ }
327987
+ await execFileAsync2("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
327988
+ timeout: 1e4,
327989
+ windowsHide: true
327990
+ });
327991
+ }
327933
327992
  async function trySendElementValue(sessionId, elementId, value) {
327934
327993
  try {
327935
327994
  await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
@@ -327978,7 +328037,7 @@ async function executeTap(sessionId, step) {
327978
328037
  }
327979
328038
  await tapCoordinates(sessionId, bounds);
327980
328039
  }
327981
- async function executeType(sessionId, step) {
328040
+ async function executeType(sessionId, step, context) {
327982
328041
  if (step.value === void 0 || step.value === null || step.value === "") return;
327983
328042
  const value = String(step.value);
327984
328043
  const bounds = boundsFromStep(step);
@@ -328027,6 +328086,7 @@ async function executeType(sessionId, step) {
328027
328086
  if (bounds) {
328028
328087
  if (!tappedBounds) {
328029
328088
  await tapCoordinates(sessionId, bounds);
328089
+ tappedBounds = true;
328030
328090
  await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
328031
328091
  }
328032
328092
  try {
@@ -328037,6 +328097,17 @@ async function executeType(sessionId, step) {
328037
328097
  lastInputError = error2;
328038
328098
  }
328039
328099
  }
328100
+ if (context.platform === "ANDROID" && tappedBounds) {
328101
+ try {
328102
+ context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
328103
+ await sendAndroidTextToFocusedInput(context.deviceId, value);
328104
+ await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
328105
+ return;
328106
+ } catch (error2) {
328107
+ if (isTransportError(error2)) throw error2;
328108
+ lastInputError = error2;
328109
+ }
328110
+ }
328040
328111
  throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
328041
328112
  }
328042
328113
  async function executeClear(sessionId, step) {
@@ -328131,9 +328202,29 @@ async function executeAssertText(sessionId, step) {
328131
328202
  const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
328132
328203
  const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
328133
328204
  const actualText = typeof result?.value === "string" ? result.value : "";
328134
- if (!actualText.includes(step.assertion)) {
328135
- throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in "${actualText}"`);
328205
+ const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
328206
+ if (!textCandidates.some((value) => value.includes(step.assertion))) {
328207
+ throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in ${JSON.stringify(textCandidates)}`);
328208
+ }
328209
+ }
328210
+ async function readElementTextCandidates(sessionId, elementId, actualText) {
328211
+ const values = [];
328212
+ const push = (value) => {
328213
+ if (typeof value !== "string") return;
328214
+ const trimmed = value.trim();
328215
+ if (trimmed.length === 0 || values.includes(trimmed)) return;
328216
+ values.push(trimmed);
328217
+ };
328218
+ push(actualText);
328219
+ for (const attr of MOBILE_TEXT_ATTRIBUTE_NAMES) {
328220
+ try {
328221
+ const result = await wdRequest(`/session/${sessionId}/element/${elementId}/attribute/${encodeURIComponent(attr)}`);
328222
+ push(result?.value);
328223
+ } catch (err) {
328224
+ if (isTransportError(err)) throw err;
328225
+ }
328136
328226
  }
328227
+ return values;
328137
328228
  }
328138
328229
  async function executeMobileTest(options) {
328139
328230
  const logLines = [];
@@ -328258,7 +328349,11 @@ async function executeMobileTest(options) {
328258
328349
  await executeTap(sessionId, step);
328259
328350
  break;
328260
328351
  case "type":
328261
- await executeType(sessionId, step);
328352
+ await executeType(sessionId, step, {
328353
+ platform: options.target.platform,
328354
+ deviceId: selectedDevice.id,
328355
+ log: log2
328356
+ });
328262
328357
  break;
328263
328358
  case "clear":
328264
328359
  await executeClear(sessionId, step);
@@ -332017,23 +332112,39 @@ ${finalVerifyResult.logs ?? ""}`;
332017
332112
  log.fail("ERROR - No mobile steps");
332018
332113
  return;
332019
332114
  }
332115
+ const maxRetries = typeof run2.retryCount === "number" && Number.isFinite(run2.retryCount) ? Math.max(0, Math.min(2, Math.floor(run2.retryCount))) : 0;
332020
332116
  setExecutorBusy(true);
332021
- let result2;
332117
+ let result2 = null;
332118
+ const attemptLogs = [];
332022
332119
  try {
332023
- result2 = await executeMobileTest({
332024
- steps: mobileSteps,
332025
- target: {
332026
- appId: target.appId,
332027
- platform: target.platform,
332028
- platformVersion: target.platformVersion,
332029
- recordedDeviceId: target.recordedDeviceId,
332030
- launchMode: target.launchMode,
332031
- deeplink: target.deeplink
332032
- }
332033
- });
332120
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
332121
+ if (attempt > 0) {
332122
+ log.info(`Retrying mobile test after failed Appium step (${attempt}/${maxRetries})`);
332123
+ }
332124
+ result2 = await executeMobileTest({
332125
+ steps: mobileSteps,
332126
+ target: {
332127
+ appId: target.appId,
332128
+ platform: target.platform,
332129
+ platformVersion: target.platformVersion,
332130
+ recordedDeviceId: target.recordedDeviceId,
332131
+ launchMode: target.launchMode,
332132
+ deeplink: target.deeplink
332133
+ }
332134
+ });
332135
+ attemptLogs.push(`--- Appium attempt ${attempt + 1}/${maxRetries + 1} ---
332136
+ ${result2.logs}`);
332137
+ if (result2.passed || result2.environmental || attempt >= maxRetries) break;
332138
+ }
332034
332139
  } finally {
332035
332140
  setExecutorBusy(false);
332036
332141
  }
332142
+ if (!result2) {
332143
+ throw new Error("Appium execution did not produce a result.");
332144
+ }
332145
+ if (attemptLogs.length > 1) {
332146
+ result2 = { ...result2, logs: attemptLogs.join("\n\n") };
332147
+ }
332037
332148
  const duration2 = Date.now() - startTime;
332038
332149
  const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
332039
332150
  const reportedError = result2.environmental ? `CONFIG_ISSUE: ${result2.environmental}` : result2.error ?? void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validate.qa/runner",
3
- "version": "1.0.8",
3
+ "version": "1.0.12",
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"