@validate.qa/runner 1.0.40 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +239 -15
- package/dist/cli.mjs +239 -15
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1583,6 +1583,9 @@ var init_types = __esm({
|
|
|
1583
1583
|
});
|
|
1584
1584
|
|
|
1585
1585
|
// ../shared/dist/constants.js
|
|
1586
|
+
function isEmailFromIdentity(value) {
|
|
1587
|
+
return typeof value === "string" && EMAIL_FROM_IDENTITIES.includes(value);
|
|
1588
|
+
}
|
|
1586
1589
|
function isValidMobileAppId(appId) {
|
|
1587
1590
|
return appId.length > 0 && appId.length <= MOBILE_APP_ID_MAX_LENGTH && MOBILE_APP_ID_PATTERN.test(appId);
|
|
1588
1591
|
}
|
|
@@ -1641,13 +1644,22 @@ function escapeEnvValue(value) {
|
|
|
1641
1644
|
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
1642
1645
|
return `"${escaped}"`;
|
|
1643
1646
|
}
|
|
1644
|
-
var API_PREFIX, SUPPORT_EMAIL, PLATFORM_ADMIN_EMAIL, AUDIO_CHUNK_DURATION_MS, ACTION_BATCH_INTERVAL_MS, ACTION_BATCH_MAX_SIZE, MOBILE_SCREENSHOT_INTERVAL_MS, MOBILE_ACTION_BATCH_MAX_SIZE, MOBILE_BRIDGE_PORT, MOBILE_SNAPSHOT_MAX_SIZE_BYTES, MOBILE_APP_ID_MAX_LENGTH, MOBILE_APP_ID_PATTERN, MOBILE_FOCUS_SETTLE_MS, MOBILE_POST_ACTION_SETTLE_MS, MOBILE_COLD_BOOT_SETTLE_MS, MOBILE_DEFAULT_SWIPE_DISTANCE, MOBILE_MAX_SWIPE_DISTANCE, SELECTOR_PRIORITIES, MOBILE_STEP_TYPES, BLOCKED_ENV_VARS, SPAWNED_ENV_ALLOWLIST;
|
|
1647
|
+
var API_PREFIX, SUPPORT_EMAIL, PLATFORM_ADMIN_EMAIL, NOREPLY_EMAIL, EMAIL_FROM_IDENTITIES, ADMIN_EMAIL_LIMITS, AUDIO_CHUNK_DURATION_MS, ACTION_BATCH_INTERVAL_MS, ACTION_BATCH_MAX_SIZE, MOBILE_SCREENSHOT_INTERVAL_MS, MOBILE_ACTION_BATCH_MAX_SIZE, MOBILE_BRIDGE_PORT, MOBILE_SNAPSHOT_MAX_SIZE_BYTES, MOBILE_APP_ID_MAX_LENGTH, MOBILE_APP_ID_PATTERN, MOBILE_FOCUS_SETTLE_MS, MOBILE_POST_ACTION_SETTLE_MS, MOBILE_COLD_BOOT_SETTLE_MS, MOBILE_DEFAULT_SWIPE_DISTANCE, MOBILE_MAX_SWIPE_DISTANCE, SELECTOR_PRIORITIES, MOBILE_STEP_TYPES, BLOCKED_ENV_VARS, SPAWNED_ENV_ALLOWLIST;
|
|
1645
1648
|
var init_constants = __esm({
|
|
1646
1649
|
"../shared/dist/constants.js"() {
|
|
1647
1650
|
"use strict";
|
|
1648
1651
|
API_PREFIX = "/api";
|
|
1649
1652
|
SUPPORT_EMAIL = "support@validate.qa";
|
|
1650
1653
|
PLATFORM_ADMIN_EMAIL = "admin@validate.qa";
|
|
1654
|
+
NOREPLY_EMAIL = "noreply@validate.qa";
|
|
1655
|
+
EMAIL_FROM_IDENTITIES = ["noreply", "support", "admin"];
|
|
1656
|
+
ADMIN_EMAIL_LIMITS = {
|
|
1657
|
+
recipientsPerSend: 100,
|
|
1658
|
+
recipientNameLength: 200,
|
|
1659
|
+
subjectLength: 200,
|
|
1660
|
+
bodyLength: 5e4,
|
|
1661
|
+
templateNameLength: 120
|
|
1662
|
+
};
|
|
1651
1663
|
AUDIO_CHUNK_DURATION_MS = 3e4;
|
|
1652
1664
|
ACTION_BATCH_INTERVAL_MS = 2e3;
|
|
1653
1665
|
ACTION_BATCH_MAX_SIZE = 10;
|
|
@@ -1786,6 +1798,83 @@ var init_constants = __esm({
|
|
|
1786
1798
|
}
|
|
1787
1799
|
});
|
|
1788
1800
|
|
|
1801
|
+
// ../shared/dist/project-key.js
|
|
1802
|
+
function isValidProjectKey(key) {
|
|
1803
|
+
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
1804
|
+
}
|
|
1805
|
+
function randomChar(rng) {
|
|
1806
|
+
return KEY_ALPHABET[Math.floor(rng() * KEY_ALPHABET.length)] ?? "0";
|
|
1807
|
+
}
|
|
1808
|
+
function randomKey(length, rng) {
|
|
1809
|
+
let out = "";
|
|
1810
|
+
for (let i = 0; i < length; i++)
|
|
1811
|
+
out += randomChar(rng);
|
|
1812
|
+
return out;
|
|
1813
|
+
}
|
|
1814
|
+
function alnumLower(name) {
|
|
1815
|
+
return (name ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1816
|
+
}
|
|
1817
|
+
function deriveProjectKey(name, rng = Math.random) {
|
|
1818
|
+
const base2 = alnumLower(name).slice(0, 3);
|
|
1819
|
+
return base2.length >= PROJECT_KEY_MIN_LENGTH ? base2 : randomKey(3, rng);
|
|
1820
|
+
}
|
|
1821
|
+
function generateUniqueProjectKey(name, taken, rng = Math.random) {
|
|
1822
|
+
const isFree = (k) => isValidProjectKey(k) && !taken.has(k);
|
|
1823
|
+
const primary = deriveProjectKey(name, rng);
|
|
1824
|
+
if (isFree(primary))
|
|
1825
|
+
return primary;
|
|
1826
|
+
const prefix2 = alnumLower(name).slice(0, 2);
|
|
1827
|
+
for (const suffixLen of [1, 2]) {
|
|
1828
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1829
|
+
const stem = prefix2.length >= 1 ? prefix2 : deriveProjectKey(name, rng).slice(0, 2);
|
|
1830
|
+
const candidate = stem + randomKey(suffixLen, rng);
|
|
1831
|
+
if (isFree(candidate))
|
|
1832
|
+
return candidate;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
for (let len = 4; len <= PROJECT_KEY_MAX_LENGTH; len++) {
|
|
1836
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1837
|
+
const candidate = randomKey(len, rng);
|
|
1838
|
+
if (isFree(candidate))
|
|
1839
|
+
return candidate;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
throw new Error("Unable to generate a unique project key");
|
|
1843
|
+
}
|
|
1844
|
+
var RESERVED_PROJECT_KEYS, PROJECT_KEY_MIN_LENGTH, PROJECT_KEY_MAX_LENGTH, PROJECT_KEY_PATTERN, KEY_ALPHABET;
|
|
1845
|
+
var init_project_key = __esm({
|
|
1846
|
+
"../shared/dist/project-key.js"() {
|
|
1847
|
+
"use strict";
|
|
1848
|
+
RESERVED_PROJECT_KEYS = /* @__PURE__ */ new Set([
|
|
1849
|
+
"tests",
|
|
1850
|
+
"sessions",
|
|
1851
|
+
"settings",
|
|
1852
|
+
"reports",
|
|
1853
|
+
"account",
|
|
1854
|
+
"notifications",
|
|
1855
|
+
"support",
|
|
1856
|
+
"site-map",
|
|
1857
|
+
"api-intelligence",
|
|
1858
|
+
"selector-registry",
|
|
1859
|
+
"project-knowledge",
|
|
1860
|
+
"test-data",
|
|
1861
|
+
"audit",
|
|
1862
|
+
"validate-copilot",
|
|
1863
|
+
"qa-copilot",
|
|
1864
|
+
"insights",
|
|
1865
|
+
"regression",
|
|
1866
|
+
"reviews",
|
|
1867
|
+
"admin",
|
|
1868
|
+
"login",
|
|
1869
|
+
"admin-view"
|
|
1870
|
+
]);
|
|
1871
|
+
PROJECT_KEY_MIN_LENGTH = 2;
|
|
1872
|
+
PROJECT_KEY_MAX_LENGTH = 12;
|
|
1873
|
+
PROJECT_KEY_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])$/;
|
|
1874
|
+
KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1789
1878
|
// ../shared/dist/mobile-deeplink.js
|
|
1790
1879
|
function isSafeDeepLink(value) {
|
|
1791
1880
|
if (typeof value !== "string")
|
|
@@ -4941,6 +5030,7 @@ __export(dist_exports, {
|
|
|
4941
5030
|
ACTION_BATCH_INTERVAL_MS: () => ACTION_BATCH_INTERVAL_MS,
|
|
4942
5031
|
ACTION_BATCH_MAX_SIZE: () => ACTION_BATCH_MAX_SIZE,
|
|
4943
5032
|
ADDON_DEFINITIONS: () => ADDON_DEFINITIONS,
|
|
5033
|
+
ADMIN_EMAIL_LIMITS: () => ADMIN_EMAIL_LIMITS,
|
|
4944
5034
|
AI_CREDIT_MARGIN: () => AI_CREDIT_MARGIN,
|
|
4945
5035
|
API_PREFIX: () => API_PREFIX,
|
|
4946
5036
|
AUDIO_CHUNK_DURATION_MS: () => AUDIO_CHUNK_DURATION_MS,
|
|
@@ -4954,6 +5044,7 @@ __export(dist_exports, {
|
|
|
4954
5044
|
DEFAULT_TTS_SPEED: () => DEFAULT_TTS_SPEED,
|
|
4955
5045
|
DEFAULT_TTS_VOICE_ID: () => DEFAULT_TTS_VOICE_ID,
|
|
4956
5046
|
DEFAULT_UI_VARIANT: () => DEFAULT_UI_VARIANT,
|
|
5047
|
+
EMAIL_FROM_IDENTITIES: () => EMAIL_FROM_IDENTITIES,
|
|
4957
5048
|
ENVIRONMENTAL_ERROR_PREFIXES: () => ENVIRONMENTAL_ERROR_PREFIXES,
|
|
4958
5049
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
4959
5050
|
MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
|
|
@@ -4972,9 +5063,14 @@ __export(dist_exports, {
|
|
|
4972
5063
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
4973
5064
|
MOBILE_SNAPSHOT_MAX_SIZE_BYTES: () => MOBILE_SNAPSHOT_MAX_SIZE_BYTES,
|
|
4974
5065
|
MOBILE_STEP_TYPES: () => MOBILE_STEP_TYPES,
|
|
5066
|
+
NOREPLY_EMAIL: () => NOREPLY_EMAIL,
|
|
4975
5067
|
PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
|
|
4976
5068
|
PLAN_ORDER: () => PLAN_ORDER,
|
|
4977
5069
|
PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
|
|
5070
|
+
PROJECT_KEY_MAX_LENGTH: () => PROJECT_KEY_MAX_LENGTH,
|
|
5071
|
+
PROJECT_KEY_MIN_LENGTH: () => PROJECT_KEY_MIN_LENGTH,
|
|
5072
|
+
PROJECT_KEY_PATTERN: () => PROJECT_KEY_PATTERN,
|
|
5073
|
+
RESERVED_PROJECT_KEYS: () => RESERVED_PROJECT_KEYS,
|
|
4978
5074
|
SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
|
|
4979
5075
|
SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
|
|
4980
5076
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
@@ -5008,6 +5104,7 @@ __export(dist_exports, {
|
|
|
5008
5104
|
controlSignature: () => controlSignature,
|
|
5009
5105
|
countInnerTests: () => countInnerTests,
|
|
5010
5106
|
decideHarPosted: () => decideHarPosted,
|
|
5107
|
+
deriveProjectKey: () => deriveProjectKey,
|
|
5011
5108
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5012
5109
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
5013
5110
|
effectiveTestCount: () => effectiveTestCount,
|
|
@@ -5024,6 +5121,7 @@ __export(dist_exports, {
|
|
|
5024
5121
|
formatLimit: () => formatLimit,
|
|
5025
5122
|
fromDbPricingPlan: () => fromDbPricingPlan,
|
|
5026
5123
|
generateMobileTestFromSession: () => generateMobileTestFromSession,
|
|
5124
|
+
generateUniqueProjectKey: () => generateUniqueProjectKey,
|
|
5027
5125
|
getDefaultPlanForTier: () => getDefaultPlanForTier,
|
|
5028
5126
|
getDeploymentTierForPlan: () => getDeploymentTierForPlan,
|
|
5029
5127
|
getPlanChangeType: () => getPlanChangeType,
|
|
@@ -5042,6 +5140,7 @@ __export(dist_exports, {
|
|
|
5042
5140
|
isCreditPackAmountCents: () => isCreditPackAmountCents,
|
|
5043
5141
|
isDbPricingPlan: () => isDbPricingPlan,
|
|
5044
5142
|
isEditableMobileClassName: () => isEditableMobileClassName,
|
|
5143
|
+
isEmailFromIdentity: () => isEmailFromIdentity,
|
|
5045
5144
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
5046
5145
|
isMobilePlatform: () => isMobilePlatform,
|
|
5047
5146
|
isPricingPlan: () => isPricingPlan,
|
|
@@ -5053,6 +5152,7 @@ __export(dist_exports, {
|
|
|
5053
5152
|
isTrialExpired: () => isTrialExpired,
|
|
5054
5153
|
isUIVariant: () => isUIVariant,
|
|
5055
5154
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
5155
|
+
isValidProjectKey: () => isValidProjectKey,
|
|
5056
5156
|
isValidSttProvider: () => isValidSttProvider,
|
|
5057
5157
|
isValidTtsModel: () => isValidTtsModel,
|
|
5058
5158
|
isValidTtsVoiceId: () => isValidTtsVoiceId,
|
|
@@ -5098,6 +5198,7 @@ var init_dist = __esm({
|
|
|
5098
5198
|
"use strict";
|
|
5099
5199
|
init_types();
|
|
5100
5200
|
init_constants();
|
|
5201
|
+
init_project_key();
|
|
5101
5202
|
init_mobile_test_generation();
|
|
5102
5203
|
init_mobile_step_healer();
|
|
5103
5204
|
init_mobile_cross_platform();
|
|
@@ -5888,17 +5989,27 @@ var require_mobile_session_keepalive = __commonJS({
|
|
|
5888
5989
|
"use strict";
|
|
5889
5990
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
5890
5991
|
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5992
|
+
exports2.computeKeepAliveIntervalMs = computeKeepAliveIntervalMs;
|
|
5891
5993
|
exports2.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5892
|
-
|
|
5893
|
-
const
|
|
5894
|
-
|
|
5895
|
-
|
|
5994
|
+
function computeKeepAliveIntervalMs(keepaliveMsRaw, newCommandTimeoutSecondsRaw) {
|
|
5995
|
+
const explicit = keepaliveMsRaw?.trim() ? Number(keepaliveMsRaw) : NaN;
|
|
5996
|
+
if (Number.isFinite(explicit) && explicit >= 0)
|
|
5997
|
+
return Math.floor(explicit);
|
|
5998
|
+
const timeoutSeconds = Number(newCommandTimeoutSecondsRaw);
|
|
5999
|
+
const reapWindowMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) * 1e3 : 6e5;
|
|
6000
|
+
return Math.min(12e4, Math.max(5e3, Math.floor(reapWindowMs / 3)));
|
|
6001
|
+
}
|
|
6002
|
+
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = computeKeepAliveIntervalMs(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS, process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
5896
6003
|
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports2.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5897
6004
|
if (intervalMs <= 0)
|
|
5898
6005
|
return fn();
|
|
5899
6006
|
const timer = setInterval(() => {
|
|
5900
|
-
|
|
5901
|
-
|
|
6007
|
+
try {
|
|
6008
|
+
const probe = driver.keepAlivePing ? driver.keepAlivePing() : driver.windowSize();
|
|
6009
|
+
void probe.catch(() => {
|
|
6010
|
+
});
|
|
6011
|
+
} catch {
|
|
6012
|
+
}
|
|
5902
6013
|
}, intervalMs);
|
|
5903
6014
|
timer.unref?.();
|
|
5904
6015
|
try {
|
|
@@ -73993,6 +74104,7 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
73993
74104
|
var mobile_heal_postprocess_js_1 = require_mobile_heal_postprocess();
|
|
73994
74105
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
73995
74106
|
var selector_registry_context_js_1 = require_selector_registry_context();
|
|
74107
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
73996
74108
|
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.
|
|
73997
74109
|
## You MUST try to fix the steps
|
|
73998
74110
|
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).
|
|
@@ -74884,6 +74996,19 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74884
74996
|
};
|
|
74885
74997
|
}
|
|
74886
74998
|
const maxPasses = envInt2("MOBILE_HEAL_PHASE2_MAX_PASSES") ?? DEFAULT_PHASE2_MAX_PASSES;
|
|
74999
|
+
const keepAlivePingable = {
|
|
75000
|
+
...typeof liveDriver.keepAlivePing === "function" ? { keepAlivePing: liveDriver.keepAlivePing.bind(liveDriver) } : {},
|
|
75001
|
+
// WARNING: this fallback routes through snapshot(), which sits inside
|
|
75002
|
+
// withSessionRecovery on the real driver — a failed tick could consume the
|
|
75003
|
+
// bounded recovery budget, the exact bug keepAlivePing exists to avoid.
|
|
75004
|
+
// Every current MobileLiveDriverLike provides keepAlivePing, so this only
|
|
75005
|
+
// exists for structural compatibility; implement keepAlivePing on any new
|
|
75006
|
+
// facade rather than relying on it.
|
|
75007
|
+
windowSize: async () => {
|
|
75008
|
+
await liveDriver.snapshot();
|
|
75009
|
+
return null;
|
|
75010
|
+
}
|
|
75011
|
+
};
|
|
74887
75012
|
let baseSteps = args.steps;
|
|
74888
75013
|
let priorError = args.priorError ?? null;
|
|
74889
75014
|
let bestSteps;
|
|
@@ -74918,10 +75043,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74918
75043
|
];
|
|
74919
75044
|
let content;
|
|
74920
75045
|
try {
|
|
74921
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
75046
|
+
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)(() => {
|
|
74922
75047
|
arm();
|
|
74923
75048
|
return aiClient.chat.completions.create({ model, messages, max_tokens: 8e3, temperature: 0.1 }, { signal });
|
|
74924
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true });
|
|
75049
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
74925
75050
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
74926
75051
|
} catch {
|
|
74927
75052
|
continue;
|
|
@@ -84724,6 +84849,51 @@ var require_discover_generator = __commonJS({
|
|
|
84724
84849
|
}
|
|
84725
84850
|
return s.length;
|
|
84726
84851
|
}
|
|
84852
|
+
function lineParenDeficit(line) {
|
|
84853
|
+
const stack = [];
|
|
84854
|
+
let regexSkips = 0;
|
|
84855
|
+
for (let i = 0; i < line.length; i++) {
|
|
84856
|
+
const ch = line[i];
|
|
84857
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
84858
|
+
i = skipQuotedLiteral(line, i) - 1;
|
|
84859
|
+
continue;
|
|
84860
|
+
}
|
|
84861
|
+
if (ch === "/" && line[i + 1] === "/") {
|
|
84862
|
+
i = skipLineComment(line, i) - 1;
|
|
84863
|
+
continue;
|
|
84864
|
+
}
|
|
84865
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
84866
|
+
i = skipBlockComment(line, i) - 1;
|
|
84867
|
+
continue;
|
|
84868
|
+
}
|
|
84869
|
+
if (ch === "/" && isRegexStart(line, i)) {
|
|
84870
|
+
regexSkips++;
|
|
84871
|
+
i = skipRegexLiteral(line, i) - 1;
|
|
84872
|
+
continue;
|
|
84873
|
+
}
|
|
84874
|
+
if (ch === "(" || ch === "{" || ch === "[") {
|
|
84875
|
+
stack.push(ch);
|
|
84876
|
+
continue;
|
|
84877
|
+
}
|
|
84878
|
+
if (ch === ")" || ch === "}" || ch === "]") {
|
|
84879
|
+
if (stack[stack.length - 1] !== CLOSE_TO_OPEN[ch])
|
|
84880
|
+
return { deficit: 0, regexSkips };
|
|
84881
|
+
stack.pop();
|
|
84882
|
+
}
|
|
84883
|
+
}
|
|
84884
|
+
const deficit = stack.length > 0 && stack.every((open) => open === "(") ? stack.length : 0;
|
|
84885
|
+
return { deficit, regexSkips };
|
|
84886
|
+
}
|
|
84887
|
+
function parsesAsAsyncStatement(line) {
|
|
84888
|
+
try {
|
|
84889
|
+
new Function(`return (async () => {
|
|
84890
|
+
${line}
|
|
84891
|
+
})`);
|
|
84892
|
+
return true;
|
|
84893
|
+
} catch {
|
|
84894
|
+
return false;
|
|
84895
|
+
}
|
|
84896
|
+
}
|
|
84727
84897
|
function closeUnclosedRegexLiterals(code) {
|
|
84728
84898
|
const anchors = [
|
|
84729
84899
|
{ rx: /\b(?:waitForURL|toHaveURL)\(\//g, mode: "url" },
|
|
@@ -84762,12 +84932,33 @@ var require_discover_generator = __commonJS({
|
|
|
84762
84932
|
const term = findRegexTerminator(code, slash, TERMINATORS[mode]);
|
|
84763
84933
|
out.push(code.slice(cursor, slash));
|
|
84764
84934
|
const raw = code.slice(slash + 1, term).replace(/\s+$/, "");
|
|
84935
|
+
let closedLiteral;
|
|
84765
84936
|
if (mode === "url") {
|
|
84766
84937
|
const escaped = raw.replace(/(?<!\\)\//g, "\\/");
|
|
84767
84938
|
const finalBody = escaped.startsWith("\\/") ? escaped : `\\/${escaped}`;
|
|
84768
|
-
|
|
84939
|
+
closedLiteral = `/${finalBody}/`;
|
|
84769
84940
|
} else {
|
|
84770
|
-
|
|
84941
|
+
closedLiteral = `/${raw}/`;
|
|
84942
|
+
}
|
|
84943
|
+
out.push(closedLiteral);
|
|
84944
|
+
if (code[term] === ")") {
|
|
84945
|
+
const lineStart = code.lastIndexOf("\n", slash) + 1;
|
|
84946
|
+
const nextNewline = code.indexOf("\n", term);
|
|
84947
|
+
const lineEnd = nextNewline === -1 ? code.length : nextNewline;
|
|
84948
|
+
const tail = code.slice(term, lineEnd);
|
|
84949
|
+
const physicalLine = code.slice(lineStart, lineEnd);
|
|
84950
|
+
const lineIsPlain = !physicalLine.includes("`") && !physicalLine.includes("/*") && !physicalLine.includes("*/");
|
|
84951
|
+
const soleAnchorOnLine = !hits.some((h) => h.slash !== slash && h.slash >= lineStart && h.slash < lineEnd);
|
|
84952
|
+
if (lineIsPlain && soleAnchorOnLine && /;\s*$/.test(tail)) {
|
|
84953
|
+
const repairedLine = code.slice(lineStart, slash) + "/x/" + tail;
|
|
84954
|
+
const { deficit, regexSkips } = lineParenDeficit(repairedLine);
|
|
84955
|
+
if (deficit > 0 && deficit <= 3 && regexSkips === 1) {
|
|
84956
|
+
const candidateLine = code.slice(lineStart, slash) + closedLiteral + ")".repeat(deficit) + tail;
|
|
84957
|
+
if (parsesAsAsyncStatement(candidateLine)) {
|
|
84958
|
+
out.push(")".repeat(deficit));
|
|
84959
|
+
}
|
|
84960
|
+
}
|
|
84961
|
+
}
|
|
84771
84962
|
}
|
|
84772
84963
|
cursor = term;
|
|
84773
84964
|
}
|
|
@@ -337145,7 +337336,7 @@ var require_package4 = __commonJS({
|
|
|
337145
337336
|
"package.json"(exports2, module2) {
|
|
337146
337337
|
module2.exports = {
|
|
337147
337338
|
name: "@validate.qa/runner",
|
|
337148
|
-
version: "1.0.
|
|
337339
|
+
version: "1.0.42",
|
|
337149
337340
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
337150
337341
|
bin: {
|
|
337151
337342
|
"validate-runner": "dist/cli.js"
|
|
@@ -337154,7 +337345,7 @@ var require_package4 = __commonJS({
|
|
|
337154
337345
|
build: "tsup",
|
|
337155
337346
|
dev: "tsx src/cli.ts",
|
|
337156
337347
|
test: "node --import tsx --test src/version-check.test.ts",
|
|
337157
|
-
"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",
|
|
337348
|
+
"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",
|
|
337158
337349
|
prepublishOnly: "npm run build"
|
|
337159
337350
|
},
|
|
337160
337351
|
files: [
|
|
@@ -343302,6 +343493,17 @@ var MobileBridgeDriver = class {
|
|
|
343302
343493
|
return size;
|
|
343303
343494
|
});
|
|
343304
343495
|
}
|
|
343496
|
+
/**
|
|
343497
|
+
* Recovery-free liveness probe for keep-alive ticks (mobile-session-keepalive).
|
|
343498
|
+
* Deliberately NOT wrapped in withSessionRecovery: keep-alive ticks are
|
|
343499
|
+
* fire-and-forget, so a failing probe must never consume the bounded recovery
|
|
343500
|
+
* budget or relaunch the app from the background — a genuinely dead session
|
|
343501
|
+
* surfaces (with its precise error) on the caller's next REAL command, which
|
|
343502
|
+
* still runs the full recovery path.
|
|
343503
|
+
*/
|
|
343504
|
+
async keepAlivePing() {
|
|
343505
|
+
await this.assertSessionReachable();
|
|
343506
|
+
}
|
|
343305
343507
|
// ── Action result wrapper ──────────────────────────────
|
|
343306
343508
|
/**
|
|
343307
343509
|
* Run an action body, let the UI tree settle, then capture the post-action
|
|
@@ -345859,6 +346061,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345859
346061
|
setExecutorBusy(true);
|
|
345860
346062
|
let session = null;
|
|
345861
346063
|
let capture = null;
|
|
346064
|
+
let discoveryTeardownStarted = false;
|
|
345862
346065
|
try {
|
|
345863
346066
|
const discoverySessionTarget = {
|
|
345864
346067
|
appId: mobileTarget.appId,
|
|
@@ -345910,6 +346113,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345910
346113
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
345911
346114
|
maxSessionRecoveries: 1,
|
|
345912
346115
|
recoverSession: async (cause) => {
|
|
346116
|
+
if (discoveryTeardownStarted) {
|
|
346117
|
+
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
346118
|
+
}
|
|
345913
346119
|
const lost = session;
|
|
345914
346120
|
mobileOnLog(
|
|
345915
346121
|
`[appium] discovery session lost (${cause.message}); recreating once on the leased device`
|
|
@@ -345923,6 +346129,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345923
346129
|
}
|
|
345924
346130
|
}
|
|
345925
346131
|
const replacement = await openDiscoverySession();
|
|
346132
|
+
if (discoveryTeardownStarted) {
|
|
346133
|
+
try {
|
|
346134
|
+
await replacement.stop();
|
|
346135
|
+
} catch {
|
|
346136
|
+
}
|
|
346137
|
+
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
346138
|
+
}
|
|
345926
346139
|
session = replacement;
|
|
345927
346140
|
attachMirrorSession(replacement.sessionId, platform3, replacement.deviceId);
|
|
345928
346141
|
liveBrowserHandle.patch({
|
|
@@ -345962,6 +346175,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345962
346175
|
onPlanReady
|
|
345963
346176
|
});
|
|
345964
346177
|
} finally {
|
|
346178
|
+
discoveryTeardownStarted = true;
|
|
345965
346179
|
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
345966
346180
|
const timeboxed = async (step) => Promise.race([
|
|
345967
346181
|
step,
|
|
@@ -345997,8 +346211,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345997
346211
|
}
|
|
345998
346212
|
if (session) {
|
|
345999
346213
|
detachMirrorSession(session.deviceId);
|
|
346214
|
+
const closing = session;
|
|
346000
346215
|
try {
|
|
346001
|
-
await timeboxed(
|
|
346216
|
+
const stopped = await timeboxed(closing.stop());
|
|
346217
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
346218
|
+
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`);
|
|
346219
|
+
}
|
|
346002
346220
|
} catch {
|
|
346003
346221
|
}
|
|
346004
346222
|
}
|
|
@@ -346436,7 +346654,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346436
346654
|
relaunch: async () => {
|
|
346437
346655
|
await currentDriver().relaunch();
|
|
346438
346656
|
},
|
|
346439
|
-
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState()
|
|
346657
|
+
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState(),
|
|
346658
|
+
// Recovery-free liveness probe so the loop's keep-alive can span
|
|
346659
|
+
// long AI waits without touching the driver's recovery machinery
|
|
346660
|
+
// (see mobile-session-keepalive.ts).
|
|
346661
|
+
keepAlivePing: async () => {
|
|
346662
|
+
await currentDriver().keepAlivePing();
|
|
346663
|
+
}
|
|
346440
346664
|
};
|
|
346441
346665
|
const runNativeWithSessionHandoff = async (verifyArgs) => {
|
|
346442
346666
|
await closeLiveSession();
|
package/dist/cli.mjs
CHANGED
|
@@ -1588,6 +1588,9 @@ var init_types = __esm({
|
|
|
1588
1588
|
});
|
|
1589
1589
|
|
|
1590
1590
|
// ../shared/dist/constants.js
|
|
1591
|
+
function isEmailFromIdentity(value) {
|
|
1592
|
+
return typeof value === "string" && EMAIL_FROM_IDENTITIES.includes(value);
|
|
1593
|
+
}
|
|
1591
1594
|
function isValidMobileAppId(appId) {
|
|
1592
1595
|
return appId.length > 0 && appId.length <= MOBILE_APP_ID_MAX_LENGTH && MOBILE_APP_ID_PATTERN.test(appId);
|
|
1593
1596
|
}
|
|
@@ -1646,13 +1649,22 @@ function escapeEnvValue(value) {
|
|
|
1646
1649
|
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
1647
1650
|
return `"${escaped}"`;
|
|
1648
1651
|
}
|
|
1649
|
-
var API_PREFIX, SUPPORT_EMAIL, PLATFORM_ADMIN_EMAIL, AUDIO_CHUNK_DURATION_MS, ACTION_BATCH_INTERVAL_MS, ACTION_BATCH_MAX_SIZE, MOBILE_SCREENSHOT_INTERVAL_MS, MOBILE_ACTION_BATCH_MAX_SIZE, MOBILE_BRIDGE_PORT, MOBILE_SNAPSHOT_MAX_SIZE_BYTES, MOBILE_APP_ID_MAX_LENGTH, MOBILE_APP_ID_PATTERN, MOBILE_FOCUS_SETTLE_MS, MOBILE_POST_ACTION_SETTLE_MS, MOBILE_COLD_BOOT_SETTLE_MS, MOBILE_DEFAULT_SWIPE_DISTANCE, MOBILE_MAX_SWIPE_DISTANCE, SELECTOR_PRIORITIES, MOBILE_STEP_TYPES, BLOCKED_ENV_VARS, SPAWNED_ENV_ALLOWLIST;
|
|
1652
|
+
var API_PREFIX, SUPPORT_EMAIL, PLATFORM_ADMIN_EMAIL, NOREPLY_EMAIL, EMAIL_FROM_IDENTITIES, ADMIN_EMAIL_LIMITS, AUDIO_CHUNK_DURATION_MS, ACTION_BATCH_INTERVAL_MS, ACTION_BATCH_MAX_SIZE, MOBILE_SCREENSHOT_INTERVAL_MS, MOBILE_ACTION_BATCH_MAX_SIZE, MOBILE_BRIDGE_PORT, MOBILE_SNAPSHOT_MAX_SIZE_BYTES, MOBILE_APP_ID_MAX_LENGTH, MOBILE_APP_ID_PATTERN, MOBILE_FOCUS_SETTLE_MS, MOBILE_POST_ACTION_SETTLE_MS, MOBILE_COLD_BOOT_SETTLE_MS, MOBILE_DEFAULT_SWIPE_DISTANCE, MOBILE_MAX_SWIPE_DISTANCE, SELECTOR_PRIORITIES, MOBILE_STEP_TYPES, BLOCKED_ENV_VARS, SPAWNED_ENV_ALLOWLIST;
|
|
1650
1653
|
var init_constants = __esm({
|
|
1651
1654
|
"../shared/dist/constants.js"() {
|
|
1652
1655
|
"use strict";
|
|
1653
1656
|
API_PREFIX = "/api";
|
|
1654
1657
|
SUPPORT_EMAIL = "support@validate.qa";
|
|
1655
1658
|
PLATFORM_ADMIN_EMAIL = "admin@validate.qa";
|
|
1659
|
+
NOREPLY_EMAIL = "noreply@validate.qa";
|
|
1660
|
+
EMAIL_FROM_IDENTITIES = ["noreply", "support", "admin"];
|
|
1661
|
+
ADMIN_EMAIL_LIMITS = {
|
|
1662
|
+
recipientsPerSend: 100,
|
|
1663
|
+
recipientNameLength: 200,
|
|
1664
|
+
subjectLength: 200,
|
|
1665
|
+
bodyLength: 5e4,
|
|
1666
|
+
templateNameLength: 120
|
|
1667
|
+
};
|
|
1656
1668
|
AUDIO_CHUNK_DURATION_MS = 3e4;
|
|
1657
1669
|
ACTION_BATCH_INTERVAL_MS = 2e3;
|
|
1658
1670
|
ACTION_BATCH_MAX_SIZE = 10;
|
|
@@ -1791,6 +1803,83 @@ var init_constants = __esm({
|
|
|
1791
1803
|
}
|
|
1792
1804
|
});
|
|
1793
1805
|
|
|
1806
|
+
// ../shared/dist/project-key.js
|
|
1807
|
+
function isValidProjectKey(key) {
|
|
1808
|
+
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
1809
|
+
}
|
|
1810
|
+
function randomChar(rng) {
|
|
1811
|
+
return KEY_ALPHABET[Math.floor(rng() * KEY_ALPHABET.length)] ?? "0";
|
|
1812
|
+
}
|
|
1813
|
+
function randomKey(length, rng) {
|
|
1814
|
+
let out = "";
|
|
1815
|
+
for (let i = 0; i < length; i++)
|
|
1816
|
+
out += randomChar(rng);
|
|
1817
|
+
return out;
|
|
1818
|
+
}
|
|
1819
|
+
function alnumLower(name) {
|
|
1820
|
+
return (name ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1821
|
+
}
|
|
1822
|
+
function deriveProjectKey(name, rng = Math.random) {
|
|
1823
|
+
const base2 = alnumLower(name).slice(0, 3);
|
|
1824
|
+
return base2.length >= PROJECT_KEY_MIN_LENGTH ? base2 : randomKey(3, rng);
|
|
1825
|
+
}
|
|
1826
|
+
function generateUniqueProjectKey(name, taken, rng = Math.random) {
|
|
1827
|
+
const isFree = (k) => isValidProjectKey(k) && !taken.has(k);
|
|
1828
|
+
const primary = deriveProjectKey(name, rng);
|
|
1829
|
+
if (isFree(primary))
|
|
1830
|
+
return primary;
|
|
1831
|
+
const prefix2 = alnumLower(name).slice(0, 2);
|
|
1832
|
+
for (const suffixLen of [1, 2]) {
|
|
1833
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1834
|
+
const stem = prefix2.length >= 1 ? prefix2 : deriveProjectKey(name, rng).slice(0, 2);
|
|
1835
|
+
const candidate = stem + randomKey(suffixLen, rng);
|
|
1836
|
+
if (isFree(candidate))
|
|
1837
|
+
return candidate;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
for (let len = 4; len <= PROJECT_KEY_MAX_LENGTH; len++) {
|
|
1841
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1842
|
+
const candidate = randomKey(len, rng);
|
|
1843
|
+
if (isFree(candidate))
|
|
1844
|
+
return candidate;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
throw new Error("Unable to generate a unique project key");
|
|
1848
|
+
}
|
|
1849
|
+
var RESERVED_PROJECT_KEYS, PROJECT_KEY_MIN_LENGTH, PROJECT_KEY_MAX_LENGTH, PROJECT_KEY_PATTERN, KEY_ALPHABET;
|
|
1850
|
+
var init_project_key = __esm({
|
|
1851
|
+
"../shared/dist/project-key.js"() {
|
|
1852
|
+
"use strict";
|
|
1853
|
+
RESERVED_PROJECT_KEYS = /* @__PURE__ */ new Set([
|
|
1854
|
+
"tests",
|
|
1855
|
+
"sessions",
|
|
1856
|
+
"settings",
|
|
1857
|
+
"reports",
|
|
1858
|
+
"account",
|
|
1859
|
+
"notifications",
|
|
1860
|
+
"support",
|
|
1861
|
+
"site-map",
|
|
1862
|
+
"api-intelligence",
|
|
1863
|
+
"selector-registry",
|
|
1864
|
+
"project-knowledge",
|
|
1865
|
+
"test-data",
|
|
1866
|
+
"audit",
|
|
1867
|
+
"validate-copilot",
|
|
1868
|
+
"qa-copilot",
|
|
1869
|
+
"insights",
|
|
1870
|
+
"regression",
|
|
1871
|
+
"reviews",
|
|
1872
|
+
"admin",
|
|
1873
|
+
"login",
|
|
1874
|
+
"admin-view"
|
|
1875
|
+
]);
|
|
1876
|
+
PROJECT_KEY_MIN_LENGTH = 2;
|
|
1877
|
+
PROJECT_KEY_MAX_LENGTH = 12;
|
|
1878
|
+
PROJECT_KEY_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])$/;
|
|
1879
|
+
KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1794
1883
|
// ../shared/dist/mobile-deeplink.js
|
|
1795
1884
|
function isSafeDeepLink(value) {
|
|
1796
1885
|
if (typeof value !== "string")
|
|
@@ -4946,6 +5035,7 @@ __export(dist_exports, {
|
|
|
4946
5035
|
ACTION_BATCH_INTERVAL_MS: () => ACTION_BATCH_INTERVAL_MS,
|
|
4947
5036
|
ACTION_BATCH_MAX_SIZE: () => ACTION_BATCH_MAX_SIZE,
|
|
4948
5037
|
ADDON_DEFINITIONS: () => ADDON_DEFINITIONS,
|
|
5038
|
+
ADMIN_EMAIL_LIMITS: () => ADMIN_EMAIL_LIMITS,
|
|
4949
5039
|
AI_CREDIT_MARGIN: () => AI_CREDIT_MARGIN,
|
|
4950
5040
|
API_PREFIX: () => API_PREFIX,
|
|
4951
5041
|
AUDIO_CHUNK_DURATION_MS: () => AUDIO_CHUNK_DURATION_MS,
|
|
@@ -4959,6 +5049,7 @@ __export(dist_exports, {
|
|
|
4959
5049
|
DEFAULT_TTS_SPEED: () => DEFAULT_TTS_SPEED,
|
|
4960
5050
|
DEFAULT_TTS_VOICE_ID: () => DEFAULT_TTS_VOICE_ID,
|
|
4961
5051
|
DEFAULT_UI_VARIANT: () => DEFAULT_UI_VARIANT,
|
|
5052
|
+
EMAIL_FROM_IDENTITIES: () => EMAIL_FROM_IDENTITIES,
|
|
4962
5053
|
ENVIRONMENTAL_ERROR_PREFIXES: () => ENVIRONMENTAL_ERROR_PREFIXES,
|
|
4963
5054
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
4964
5055
|
MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
|
|
@@ -4977,9 +5068,14 @@ __export(dist_exports, {
|
|
|
4977
5068
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
4978
5069
|
MOBILE_SNAPSHOT_MAX_SIZE_BYTES: () => MOBILE_SNAPSHOT_MAX_SIZE_BYTES,
|
|
4979
5070
|
MOBILE_STEP_TYPES: () => MOBILE_STEP_TYPES,
|
|
5071
|
+
NOREPLY_EMAIL: () => NOREPLY_EMAIL,
|
|
4980
5072
|
PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
|
|
4981
5073
|
PLAN_ORDER: () => PLAN_ORDER,
|
|
4982
5074
|
PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
|
|
5075
|
+
PROJECT_KEY_MAX_LENGTH: () => PROJECT_KEY_MAX_LENGTH,
|
|
5076
|
+
PROJECT_KEY_MIN_LENGTH: () => PROJECT_KEY_MIN_LENGTH,
|
|
5077
|
+
PROJECT_KEY_PATTERN: () => PROJECT_KEY_PATTERN,
|
|
5078
|
+
RESERVED_PROJECT_KEYS: () => RESERVED_PROJECT_KEYS,
|
|
4983
5079
|
SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
|
|
4984
5080
|
SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
|
|
4985
5081
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
@@ -5013,6 +5109,7 @@ __export(dist_exports, {
|
|
|
5013
5109
|
controlSignature: () => controlSignature,
|
|
5014
5110
|
countInnerTests: () => countInnerTests,
|
|
5015
5111
|
decideHarPosted: () => decideHarPosted,
|
|
5112
|
+
deriveProjectKey: () => deriveProjectKey,
|
|
5016
5113
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5017
5114
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
5018
5115
|
effectiveTestCount: () => effectiveTestCount,
|
|
@@ -5029,6 +5126,7 @@ __export(dist_exports, {
|
|
|
5029
5126
|
formatLimit: () => formatLimit,
|
|
5030
5127
|
fromDbPricingPlan: () => fromDbPricingPlan,
|
|
5031
5128
|
generateMobileTestFromSession: () => generateMobileTestFromSession,
|
|
5129
|
+
generateUniqueProjectKey: () => generateUniqueProjectKey,
|
|
5032
5130
|
getDefaultPlanForTier: () => getDefaultPlanForTier,
|
|
5033
5131
|
getDeploymentTierForPlan: () => getDeploymentTierForPlan,
|
|
5034
5132
|
getPlanChangeType: () => getPlanChangeType,
|
|
@@ -5047,6 +5145,7 @@ __export(dist_exports, {
|
|
|
5047
5145
|
isCreditPackAmountCents: () => isCreditPackAmountCents,
|
|
5048
5146
|
isDbPricingPlan: () => isDbPricingPlan,
|
|
5049
5147
|
isEditableMobileClassName: () => isEditableMobileClassName,
|
|
5148
|
+
isEmailFromIdentity: () => isEmailFromIdentity,
|
|
5050
5149
|
isEnvironmentalError: () => isEnvironmentalError,
|
|
5051
5150
|
isMobilePlatform: () => isMobilePlatform,
|
|
5052
5151
|
isPricingPlan: () => isPricingPlan,
|
|
@@ -5058,6 +5157,7 @@ __export(dist_exports, {
|
|
|
5058
5157
|
isTrialExpired: () => isTrialExpired,
|
|
5059
5158
|
isUIVariant: () => isUIVariant,
|
|
5060
5159
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
5160
|
+
isValidProjectKey: () => isValidProjectKey,
|
|
5061
5161
|
isValidSttProvider: () => isValidSttProvider,
|
|
5062
5162
|
isValidTtsModel: () => isValidTtsModel,
|
|
5063
5163
|
isValidTtsVoiceId: () => isValidTtsVoiceId,
|
|
@@ -5103,6 +5203,7 @@ var init_dist = __esm({
|
|
|
5103
5203
|
"use strict";
|
|
5104
5204
|
init_types();
|
|
5105
5205
|
init_constants();
|
|
5206
|
+
init_project_key();
|
|
5106
5207
|
init_mobile_test_generation();
|
|
5107
5208
|
init_mobile_step_healer();
|
|
5108
5209
|
init_mobile_cross_platform();
|
|
@@ -5893,17 +5994,27 @@ var require_mobile_session_keepalive = __commonJS({
|
|
|
5893
5994
|
"use strict";
|
|
5894
5995
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5895
5996
|
exports.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5997
|
+
exports.computeKeepAliveIntervalMs = computeKeepAliveIntervalMs;
|
|
5896
5998
|
exports.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5897
|
-
|
|
5898
|
-
const
|
|
5899
|
-
|
|
5900
|
-
|
|
5999
|
+
function computeKeepAliveIntervalMs(keepaliveMsRaw, newCommandTimeoutSecondsRaw) {
|
|
6000
|
+
const explicit = keepaliveMsRaw?.trim() ? Number(keepaliveMsRaw) : NaN;
|
|
6001
|
+
if (Number.isFinite(explicit) && explicit >= 0)
|
|
6002
|
+
return Math.floor(explicit);
|
|
6003
|
+
const timeoutSeconds = Number(newCommandTimeoutSecondsRaw);
|
|
6004
|
+
const reapWindowMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) * 1e3 : 6e5;
|
|
6005
|
+
return Math.min(12e4, Math.max(5e3, Math.floor(reapWindowMs / 3)));
|
|
6006
|
+
}
|
|
6007
|
+
exports.MOBILE_KEEPALIVE_INTERVAL_MS = computeKeepAliveIntervalMs(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS, process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
5901
6008
|
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5902
6009
|
if (intervalMs <= 0)
|
|
5903
6010
|
return fn();
|
|
5904
6011
|
const timer = setInterval(() => {
|
|
5905
|
-
|
|
5906
|
-
|
|
6012
|
+
try {
|
|
6013
|
+
const probe = driver.keepAlivePing ? driver.keepAlivePing() : driver.windowSize();
|
|
6014
|
+
void probe.catch(() => {
|
|
6015
|
+
});
|
|
6016
|
+
} catch {
|
|
6017
|
+
}
|
|
5907
6018
|
}, intervalMs);
|
|
5908
6019
|
timer.unref?.();
|
|
5909
6020
|
try {
|
|
@@ -73998,6 +74109,7 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
73998
74109
|
var mobile_heal_postprocess_js_1 = require_mobile_heal_postprocess();
|
|
73999
74110
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
74000
74111
|
var selector_registry_context_js_1 = require_selector_registry_context();
|
|
74112
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
74001
74113
|
exports.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.
|
|
74002
74114
|
## You MUST try to fix the steps
|
|
74003
74115
|
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).
|
|
@@ -74889,6 +75001,19 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74889
75001
|
};
|
|
74890
75002
|
}
|
|
74891
75003
|
const maxPasses = envInt2("MOBILE_HEAL_PHASE2_MAX_PASSES") ?? DEFAULT_PHASE2_MAX_PASSES;
|
|
75004
|
+
const keepAlivePingable = {
|
|
75005
|
+
...typeof liveDriver.keepAlivePing === "function" ? { keepAlivePing: liveDriver.keepAlivePing.bind(liveDriver) } : {},
|
|
75006
|
+
// WARNING: this fallback routes through snapshot(), which sits inside
|
|
75007
|
+
// withSessionRecovery on the real driver — a failed tick could consume the
|
|
75008
|
+
// bounded recovery budget, the exact bug keepAlivePing exists to avoid.
|
|
75009
|
+
// Every current MobileLiveDriverLike provides keepAlivePing, so this only
|
|
75010
|
+
// exists for structural compatibility; implement keepAlivePing on any new
|
|
75011
|
+
// facade rather than relying on it.
|
|
75012
|
+
windowSize: async () => {
|
|
75013
|
+
await liveDriver.snapshot();
|
|
75014
|
+
return null;
|
|
75015
|
+
}
|
|
75016
|
+
};
|
|
74892
75017
|
let baseSteps = args.steps;
|
|
74893
75018
|
let priorError = args.priorError ?? null;
|
|
74894
75019
|
let bestSteps;
|
|
@@ -74923,10 +75048,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74923
75048
|
];
|
|
74924
75049
|
let content;
|
|
74925
75050
|
try {
|
|
74926
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
75051
|
+
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)(() => {
|
|
74927
75052
|
arm();
|
|
74928
75053
|
return aiClient.chat.completions.create({ model, messages, max_tokens: 8e3, temperature: 0.1 }, { signal });
|
|
74929
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true });
|
|
75054
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
74930
75055
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
74931
75056
|
} catch {
|
|
74932
75057
|
continue;
|
|
@@ -84729,6 +84854,51 @@ var require_discover_generator = __commonJS({
|
|
|
84729
84854
|
}
|
|
84730
84855
|
return s.length;
|
|
84731
84856
|
}
|
|
84857
|
+
function lineParenDeficit(line) {
|
|
84858
|
+
const stack = [];
|
|
84859
|
+
let regexSkips = 0;
|
|
84860
|
+
for (let i = 0; i < line.length; i++) {
|
|
84861
|
+
const ch = line[i];
|
|
84862
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
84863
|
+
i = skipQuotedLiteral(line, i) - 1;
|
|
84864
|
+
continue;
|
|
84865
|
+
}
|
|
84866
|
+
if (ch === "/" && line[i + 1] === "/") {
|
|
84867
|
+
i = skipLineComment(line, i) - 1;
|
|
84868
|
+
continue;
|
|
84869
|
+
}
|
|
84870
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
84871
|
+
i = skipBlockComment(line, i) - 1;
|
|
84872
|
+
continue;
|
|
84873
|
+
}
|
|
84874
|
+
if (ch === "/" && isRegexStart(line, i)) {
|
|
84875
|
+
regexSkips++;
|
|
84876
|
+
i = skipRegexLiteral(line, i) - 1;
|
|
84877
|
+
continue;
|
|
84878
|
+
}
|
|
84879
|
+
if (ch === "(" || ch === "{" || ch === "[") {
|
|
84880
|
+
stack.push(ch);
|
|
84881
|
+
continue;
|
|
84882
|
+
}
|
|
84883
|
+
if (ch === ")" || ch === "}" || ch === "]") {
|
|
84884
|
+
if (stack[stack.length - 1] !== CLOSE_TO_OPEN[ch])
|
|
84885
|
+
return { deficit: 0, regexSkips };
|
|
84886
|
+
stack.pop();
|
|
84887
|
+
}
|
|
84888
|
+
}
|
|
84889
|
+
const deficit = stack.length > 0 && stack.every((open) => open === "(") ? stack.length : 0;
|
|
84890
|
+
return { deficit, regexSkips };
|
|
84891
|
+
}
|
|
84892
|
+
function parsesAsAsyncStatement(line) {
|
|
84893
|
+
try {
|
|
84894
|
+
new Function(`return (async () => {
|
|
84895
|
+
${line}
|
|
84896
|
+
})`);
|
|
84897
|
+
return true;
|
|
84898
|
+
} catch {
|
|
84899
|
+
return false;
|
|
84900
|
+
}
|
|
84901
|
+
}
|
|
84732
84902
|
function closeUnclosedRegexLiterals(code) {
|
|
84733
84903
|
const anchors = [
|
|
84734
84904
|
{ rx: /\b(?:waitForURL|toHaveURL)\(\//g, mode: "url" },
|
|
@@ -84767,12 +84937,33 @@ var require_discover_generator = __commonJS({
|
|
|
84767
84937
|
const term = findRegexTerminator(code, slash, TERMINATORS[mode]);
|
|
84768
84938
|
out.push(code.slice(cursor, slash));
|
|
84769
84939
|
const raw = code.slice(slash + 1, term).replace(/\s+$/, "");
|
|
84940
|
+
let closedLiteral;
|
|
84770
84941
|
if (mode === "url") {
|
|
84771
84942
|
const escaped = raw.replace(/(?<!\\)\//g, "\\/");
|
|
84772
84943
|
const finalBody = escaped.startsWith("\\/") ? escaped : `\\/${escaped}`;
|
|
84773
|
-
|
|
84944
|
+
closedLiteral = `/${finalBody}/`;
|
|
84774
84945
|
} else {
|
|
84775
|
-
|
|
84946
|
+
closedLiteral = `/${raw}/`;
|
|
84947
|
+
}
|
|
84948
|
+
out.push(closedLiteral);
|
|
84949
|
+
if (code[term] === ")") {
|
|
84950
|
+
const lineStart = code.lastIndexOf("\n", slash) + 1;
|
|
84951
|
+
const nextNewline = code.indexOf("\n", term);
|
|
84952
|
+
const lineEnd = nextNewline === -1 ? code.length : nextNewline;
|
|
84953
|
+
const tail = code.slice(term, lineEnd);
|
|
84954
|
+
const physicalLine = code.slice(lineStart, lineEnd);
|
|
84955
|
+
const lineIsPlain = !physicalLine.includes("`") && !physicalLine.includes("/*") && !physicalLine.includes("*/");
|
|
84956
|
+
const soleAnchorOnLine = !hits.some((h) => h.slash !== slash && h.slash >= lineStart && h.slash < lineEnd);
|
|
84957
|
+
if (lineIsPlain && soleAnchorOnLine && /;\s*$/.test(tail)) {
|
|
84958
|
+
const repairedLine = code.slice(lineStart, slash) + "/x/" + tail;
|
|
84959
|
+
const { deficit, regexSkips } = lineParenDeficit(repairedLine);
|
|
84960
|
+
if (deficit > 0 && deficit <= 3 && regexSkips === 1) {
|
|
84961
|
+
const candidateLine = code.slice(lineStart, slash) + closedLiteral + ")".repeat(deficit) + tail;
|
|
84962
|
+
if (parsesAsAsyncStatement(candidateLine)) {
|
|
84963
|
+
out.push(")".repeat(deficit));
|
|
84964
|
+
}
|
|
84965
|
+
}
|
|
84966
|
+
}
|
|
84776
84967
|
}
|
|
84777
84968
|
cursor = term;
|
|
84778
84969
|
}
|
|
@@ -337150,7 +337341,7 @@ var require_package4 = __commonJS({
|
|
|
337150
337341
|
"package.json"(exports, module) {
|
|
337151
337342
|
module.exports = {
|
|
337152
337343
|
name: "@validate.qa/runner",
|
|
337153
|
-
version: "1.0.
|
|
337344
|
+
version: "1.0.42",
|
|
337154
337345
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
337155
337346
|
bin: {
|
|
337156
337347
|
"validate-runner": "dist/cli.js"
|
|
@@ -337159,7 +337350,7 @@ var require_package4 = __commonJS({
|
|
|
337159
337350
|
build: "tsup",
|
|
337160
337351
|
dev: "tsx src/cli.ts",
|
|
337161
337352
|
test: "node --import tsx --test src/version-check.test.ts",
|
|
337162
|
-
"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",
|
|
337353
|
+
"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",
|
|
337163
337354
|
prepublishOnly: "npm run build"
|
|
337164
337355
|
},
|
|
337165
337356
|
files: [
|
|
@@ -343310,6 +343501,17 @@ var MobileBridgeDriver = class {
|
|
|
343310
343501
|
return size;
|
|
343311
343502
|
});
|
|
343312
343503
|
}
|
|
343504
|
+
/**
|
|
343505
|
+
* Recovery-free liveness probe for keep-alive ticks (mobile-session-keepalive).
|
|
343506
|
+
* Deliberately NOT wrapped in withSessionRecovery: keep-alive ticks are
|
|
343507
|
+
* fire-and-forget, so a failing probe must never consume the bounded recovery
|
|
343508
|
+
* budget or relaunch the app from the background — a genuinely dead session
|
|
343509
|
+
* surfaces (with its precise error) on the caller's next REAL command, which
|
|
343510
|
+
* still runs the full recovery path.
|
|
343511
|
+
*/
|
|
343512
|
+
async keepAlivePing() {
|
|
343513
|
+
await this.assertSessionReachable();
|
|
343514
|
+
}
|
|
343313
343515
|
// ── Action result wrapper ──────────────────────────────
|
|
343314
343516
|
/**
|
|
343315
343517
|
* Run an action body, let the UI tree settle, then capture the post-action
|
|
@@ -345867,6 +346069,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345867
346069
|
setExecutorBusy(true);
|
|
345868
346070
|
let session = null;
|
|
345869
346071
|
let capture = null;
|
|
346072
|
+
let discoveryTeardownStarted = false;
|
|
345870
346073
|
try {
|
|
345871
346074
|
const discoverySessionTarget = {
|
|
345872
346075
|
appId: mobileTarget.appId,
|
|
@@ -345918,6 +346121,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345918
346121
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
345919
346122
|
maxSessionRecoveries: 1,
|
|
345920
346123
|
recoverSession: async (cause) => {
|
|
346124
|
+
if (discoveryTeardownStarted) {
|
|
346125
|
+
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
346126
|
+
}
|
|
345921
346127
|
const lost = session;
|
|
345922
346128
|
mobileOnLog(
|
|
345923
346129
|
`[appium] discovery session lost (${cause.message}); recreating once on the leased device`
|
|
@@ -345931,6 +346137,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345931
346137
|
}
|
|
345932
346138
|
}
|
|
345933
346139
|
const replacement = await openDiscoverySession();
|
|
346140
|
+
if (discoveryTeardownStarted) {
|
|
346141
|
+
try {
|
|
346142
|
+
await replacement.stop();
|
|
346143
|
+
} catch {
|
|
346144
|
+
}
|
|
346145
|
+
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
346146
|
+
}
|
|
345934
346147
|
session = replacement;
|
|
345935
346148
|
attachMirrorSession(replacement.sessionId, platform3, replacement.deviceId);
|
|
345936
346149
|
liveBrowserHandle.patch({
|
|
@@ -345970,6 +346183,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345970
346183
|
onPlanReady
|
|
345971
346184
|
});
|
|
345972
346185
|
} finally {
|
|
346186
|
+
discoveryTeardownStarted = true;
|
|
345973
346187
|
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
345974
346188
|
const timeboxed = async (step) => Promise.race([
|
|
345975
346189
|
step,
|
|
@@ -346005,8 +346219,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346005
346219
|
}
|
|
346006
346220
|
if (session) {
|
|
346007
346221
|
detachMirrorSession(session.deviceId);
|
|
346222
|
+
const closing = session;
|
|
346008
346223
|
try {
|
|
346009
|
-
await timeboxed(
|
|
346224
|
+
const stopped = await timeboxed(closing.stop());
|
|
346225
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
346226
|
+
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`);
|
|
346227
|
+
}
|
|
346010
346228
|
} catch {
|
|
346011
346229
|
}
|
|
346012
346230
|
}
|
|
@@ -346444,7 +346662,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346444
346662
|
relaunch: async () => {
|
|
346445
346663
|
await currentDriver().relaunch();
|
|
346446
346664
|
},
|
|
346447
|
-
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState()
|
|
346665
|
+
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState(),
|
|
346666
|
+
// Recovery-free liveness probe so the loop's keep-alive can span
|
|
346667
|
+
// long AI waits without touching the driver's recovery machinery
|
|
346668
|
+
// (see mobile-session-keepalive.ts).
|
|
346669
|
+
keepAlivePing: async () => {
|
|
346670
|
+
await currentDriver().keepAlivePing();
|
|
346671
|
+
}
|
|
346448
346672
|
};
|
|
346449
346673
|
const runNativeWithSessionHandoff = async (verifyArgs) => {
|
|
346450
346674
|
await closeLiveSession();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@validate.qa/runner",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.42",
|
|
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"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"build": "tsup",
|
|
10
10
|
"dev": "tsx src/cli.ts",
|
|
11
11
|
"test": "node --import tsx --test src/version-check.test.ts",
|
|
12
|
-
"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",
|
|
12
|
+
"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",
|
|
13
13
|
"prepublishOnly": "npm run build"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|