@validate.qa/runner 1.0.40 → 1.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +222 -14
- package/dist/cli.mjs +222 -14
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1786,6 +1786,83 @@ var init_constants = __esm({
|
|
|
1786
1786
|
}
|
|
1787
1787
|
});
|
|
1788
1788
|
|
|
1789
|
+
// ../shared/dist/project-key.js
|
|
1790
|
+
function isValidProjectKey(key) {
|
|
1791
|
+
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
1792
|
+
}
|
|
1793
|
+
function randomChar(rng) {
|
|
1794
|
+
return KEY_ALPHABET[Math.floor(rng() * KEY_ALPHABET.length)] ?? "0";
|
|
1795
|
+
}
|
|
1796
|
+
function randomKey(length, rng) {
|
|
1797
|
+
let out = "";
|
|
1798
|
+
for (let i = 0; i < length; i++)
|
|
1799
|
+
out += randomChar(rng);
|
|
1800
|
+
return out;
|
|
1801
|
+
}
|
|
1802
|
+
function alnumLower(name) {
|
|
1803
|
+
return (name ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1804
|
+
}
|
|
1805
|
+
function deriveProjectKey(name, rng = Math.random) {
|
|
1806
|
+
const base2 = alnumLower(name).slice(0, 3);
|
|
1807
|
+
return base2.length >= PROJECT_KEY_MIN_LENGTH ? base2 : randomKey(3, rng);
|
|
1808
|
+
}
|
|
1809
|
+
function generateUniqueProjectKey(name, taken, rng = Math.random) {
|
|
1810
|
+
const isFree = (k) => isValidProjectKey(k) && !taken.has(k);
|
|
1811
|
+
const primary = deriveProjectKey(name, rng);
|
|
1812
|
+
if (isFree(primary))
|
|
1813
|
+
return primary;
|
|
1814
|
+
const prefix2 = alnumLower(name).slice(0, 2);
|
|
1815
|
+
for (const suffixLen of [1, 2]) {
|
|
1816
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1817
|
+
const stem = prefix2.length >= 1 ? prefix2 : deriveProjectKey(name, rng).slice(0, 2);
|
|
1818
|
+
const candidate = stem + randomKey(suffixLen, rng);
|
|
1819
|
+
if (isFree(candidate))
|
|
1820
|
+
return candidate;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
for (let len = 4; len <= PROJECT_KEY_MAX_LENGTH; len++) {
|
|
1824
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1825
|
+
const candidate = randomKey(len, rng);
|
|
1826
|
+
if (isFree(candidate))
|
|
1827
|
+
return candidate;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
throw new Error("Unable to generate a unique project key");
|
|
1831
|
+
}
|
|
1832
|
+
var RESERVED_PROJECT_KEYS, PROJECT_KEY_MIN_LENGTH, PROJECT_KEY_MAX_LENGTH, PROJECT_KEY_PATTERN, KEY_ALPHABET;
|
|
1833
|
+
var init_project_key = __esm({
|
|
1834
|
+
"../shared/dist/project-key.js"() {
|
|
1835
|
+
"use strict";
|
|
1836
|
+
RESERVED_PROJECT_KEYS = /* @__PURE__ */ new Set([
|
|
1837
|
+
"tests",
|
|
1838
|
+
"sessions",
|
|
1839
|
+
"settings",
|
|
1840
|
+
"reports",
|
|
1841
|
+
"account",
|
|
1842
|
+
"notifications",
|
|
1843
|
+
"support",
|
|
1844
|
+
"site-map",
|
|
1845
|
+
"api-intelligence",
|
|
1846
|
+
"selector-registry",
|
|
1847
|
+
"project-knowledge",
|
|
1848
|
+
"test-data",
|
|
1849
|
+
"audit",
|
|
1850
|
+
"validate-copilot",
|
|
1851
|
+
"qa-copilot",
|
|
1852
|
+
"insights",
|
|
1853
|
+
"regression",
|
|
1854
|
+
"reviews",
|
|
1855
|
+
"admin",
|
|
1856
|
+
"login",
|
|
1857
|
+
"admin-view"
|
|
1858
|
+
]);
|
|
1859
|
+
PROJECT_KEY_MIN_LENGTH = 2;
|
|
1860
|
+
PROJECT_KEY_MAX_LENGTH = 12;
|
|
1861
|
+
PROJECT_KEY_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])$/;
|
|
1862
|
+
KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1789
1866
|
// ../shared/dist/mobile-deeplink.js
|
|
1790
1867
|
function isSafeDeepLink(value) {
|
|
1791
1868
|
if (typeof value !== "string")
|
|
@@ -4975,6 +5052,10 @@ __export(dist_exports, {
|
|
|
4975
5052
|
PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
|
|
4976
5053
|
PLAN_ORDER: () => PLAN_ORDER,
|
|
4977
5054
|
PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
|
|
5055
|
+
PROJECT_KEY_MAX_LENGTH: () => PROJECT_KEY_MAX_LENGTH,
|
|
5056
|
+
PROJECT_KEY_MIN_LENGTH: () => PROJECT_KEY_MIN_LENGTH,
|
|
5057
|
+
PROJECT_KEY_PATTERN: () => PROJECT_KEY_PATTERN,
|
|
5058
|
+
RESERVED_PROJECT_KEYS: () => RESERVED_PROJECT_KEYS,
|
|
4978
5059
|
SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
|
|
4979
5060
|
SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
|
|
4980
5061
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
@@ -5008,6 +5089,7 @@ __export(dist_exports, {
|
|
|
5008
5089
|
controlSignature: () => controlSignature,
|
|
5009
5090
|
countInnerTests: () => countInnerTests,
|
|
5010
5091
|
decideHarPosted: () => decideHarPosted,
|
|
5092
|
+
deriveProjectKey: () => deriveProjectKey,
|
|
5011
5093
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5012
5094
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
5013
5095
|
effectiveTestCount: () => effectiveTestCount,
|
|
@@ -5024,6 +5106,7 @@ __export(dist_exports, {
|
|
|
5024
5106
|
formatLimit: () => formatLimit,
|
|
5025
5107
|
fromDbPricingPlan: () => fromDbPricingPlan,
|
|
5026
5108
|
generateMobileTestFromSession: () => generateMobileTestFromSession,
|
|
5109
|
+
generateUniqueProjectKey: () => generateUniqueProjectKey,
|
|
5027
5110
|
getDefaultPlanForTier: () => getDefaultPlanForTier,
|
|
5028
5111
|
getDeploymentTierForPlan: () => getDeploymentTierForPlan,
|
|
5029
5112
|
getPlanChangeType: () => getPlanChangeType,
|
|
@@ -5053,6 +5136,7 @@ __export(dist_exports, {
|
|
|
5053
5136
|
isTrialExpired: () => isTrialExpired,
|
|
5054
5137
|
isUIVariant: () => isUIVariant,
|
|
5055
5138
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
5139
|
+
isValidProjectKey: () => isValidProjectKey,
|
|
5056
5140
|
isValidSttProvider: () => isValidSttProvider,
|
|
5057
5141
|
isValidTtsModel: () => isValidTtsModel,
|
|
5058
5142
|
isValidTtsVoiceId: () => isValidTtsVoiceId,
|
|
@@ -5098,6 +5182,7 @@ var init_dist = __esm({
|
|
|
5098
5182
|
"use strict";
|
|
5099
5183
|
init_types();
|
|
5100
5184
|
init_constants();
|
|
5185
|
+
init_project_key();
|
|
5101
5186
|
init_mobile_test_generation();
|
|
5102
5187
|
init_mobile_step_healer();
|
|
5103
5188
|
init_mobile_cross_platform();
|
|
@@ -5888,17 +5973,27 @@ var require_mobile_session_keepalive = __commonJS({
|
|
|
5888
5973
|
"use strict";
|
|
5889
5974
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
5890
5975
|
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5976
|
+
exports2.computeKeepAliveIntervalMs = computeKeepAliveIntervalMs;
|
|
5891
5977
|
exports2.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5892
|
-
|
|
5893
|
-
const
|
|
5894
|
-
|
|
5895
|
-
|
|
5978
|
+
function computeKeepAliveIntervalMs(keepaliveMsRaw, newCommandTimeoutSecondsRaw) {
|
|
5979
|
+
const explicit = keepaliveMsRaw?.trim() ? Number(keepaliveMsRaw) : NaN;
|
|
5980
|
+
if (Number.isFinite(explicit) && explicit >= 0)
|
|
5981
|
+
return Math.floor(explicit);
|
|
5982
|
+
const timeoutSeconds = Number(newCommandTimeoutSecondsRaw);
|
|
5983
|
+
const reapWindowMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) * 1e3 : 6e5;
|
|
5984
|
+
return Math.min(12e4, Math.max(5e3, Math.floor(reapWindowMs / 3)));
|
|
5985
|
+
}
|
|
5986
|
+
exports2.MOBILE_KEEPALIVE_INTERVAL_MS = computeKeepAliveIntervalMs(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS, process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
5896
5987
|
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports2.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5897
5988
|
if (intervalMs <= 0)
|
|
5898
5989
|
return fn();
|
|
5899
5990
|
const timer = setInterval(() => {
|
|
5900
|
-
|
|
5901
|
-
|
|
5991
|
+
try {
|
|
5992
|
+
const probe = driver.keepAlivePing ? driver.keepAlivePing() : driver.windowSize();
|
|
5993
|
+
void probe.catch(() => {
|
|
5994
|
+
});
|
|
5995
|
+
} catch {
|
|
5996
|
+
}
|
|
5902
5997
|
}, intervalMs);
|
|
5903
5998
|
timer.unref?.();
|
|
5904
5999
|
try {
|
|
@@ -73993,6 +74088,7 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
73993
74088
|
var mobile_heal_postprocess_js_1 = require_mobile_heal_postprocess();
|
|
73994
74089
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
73995
74090
|
var selector_registry_context_js_1 = require_selector_registry_context();
|
|
74091
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
73996
74092
|
exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = `You are a QA engineer fixing a failing Appium (mobile UI automation) test. You receive: the test STEPS as JSON, the failure ERROR + step-level results, and the PAGE-SOURCE accessibility tree captured at the moment of failure (this tree is your 'screenshot'), plus optional known-good selector candidates from a registry.
|
|
73997
74093
|
## You MUST try to fix the steps
|
|
73998
74094
|
Reason over steps + error + tree + registry and return working healed steps. Do NOT return needs_device unless you have genuinely exhausted that reasoning (and NEVER before your final attempt).
|
|
@@ -74884,6 +74980,19 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74884
74980
|
};
|
|
74885
74981
|
}
|
|
74886
74982
|
const maxPasses = envInt2("MOBILE_HEAL_PHASE2_MAX_PASSES") ?? DEFAULT_PHASE2_MAX_PASSES;
|
|
74983
|
+
const keepAlivePingable = {
|
|
74984
|
+
...typeof liveDriver.keepAlivePing === "function" ? { keepAlivePing: liveDriver.keepAlivePing.bind(liveDriver) } : {},
|
|
74985
|
+
// WARNING: this fallback routes through snapshot(), which sits inside
|
|
74986
|
+
// withSessionRecovery on the real driver — a failed tick could consume the
|
|
74987
|
+
// bounded recovery budget, the exact bug keepAlivePing exists to avoid.
|
|
74988
|
+
// Every current MobileLiveDriverLike provides keepAlivePing, so this only
|
|
74989
|
+
// exists for structural compatibility; implement keepAlivePing on any new
|
|
74990
|
+
// facade rather than relying on it.
|
|
74991
|
+
windowSize: async () => {
|
|
74992
|
+
await liveDriver.snapshot();
|
|
74993
|
+
return null;
|
|
74994
|
+
}
|
|
74995
|
+
};
|
|
74887
74996
|
let baseSteps = args.steps;
|
|
74888
74997
|
let priorError = args.priorError ?? null;
|
|
74889
74998
|
let bestSteps;
|
|
@@ -74918,10 +75027,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74918
75027
|
];
|
|
74919
75028
|
let content;
|
|
74920
75029
|
try {
|
|
74921
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
75030
|
+
const response = await (0, mobile_session_keepalive_js_1.withMobileSessionKeepAlive)(keepAlivePingable, () => (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
74922
75031
|
arm();
|
|
74923
75032
|
return aiClient.chat.completions.create({ model, messages, max_tokens: 8e3, temperature: 0.1 }, { signal });
|
|
74924
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true });
|
|
75033
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
74925
75034
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
74926
75035
|
} catch {
|
|
74927
75036
|
continue;
|
|
@@ -84724,6 +84833,51 @@ var require_discover_generator = __commonJS({
|
|
|
84724
84833
|
}
|
|
84725
84834
|
return s.length;
|
|
84726
84835
|
}
|
|
84836
|
+
function lineParenDeficit(line) {
|
|
84837
|
+
const stack = [];
|
|
84838
|
+
let regexSkips = 0;
|
|
84839
|
+
for (let i = 0; i < line.length; i++) {
|
|
84840
|
+
const ch = line[i];
|
|
84841
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
84842
|
+
i = skipQuotedLiteral(line, i) - 1;
|
|
84843
|
+
continue;
|
|
84844
|
+
}
|
|
84845
|
+
if (ch === "/" && line[i + 1] === "/") {
|
|
84846
|
+
i = skipLineComment(line, i) - 1;
|
|
84847
|
+
continue;
|
|
84848
|
+
}
|
|
84849
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
84850
|
+
i = skipBlockComment(line, i) - 1;
|
|
84851
|
+
continue;
|
|
84852
|
+
}
|
|
84853
|
+
if (ch === "/" && isRegexStart(line, i)) {
|
|
84854
|
+
regexSkips++;
|
|
84855
|
+
i = skipRegexLiteral(line, i) - 1;
|
|
84856
|
+
continue;
|
|
84857
|
+
}
|
|
84858
|
+
if (ch === "(" || ch === "{" || ch === "[") {
|
|
84859
|
+
stack.push(ch);
|
|
84860
|
+
continue;
|
|
84861
|
+
}
|
|
84862
|
+
if (ch === ")" || ch === "}" || ch === "]") {
|
|
84863
|
+
if (stack[stack.length - 1] !== CLOSE_TO_OPEN[ch])
|
|
84864
|
+
return { deficit: 0, regexSkips };
|
|
84865
|
+
stack.pop();
|
|
84866
|
+
}
|
|
84867
|
+
}
|
|
84868
|
+
const deficit = stack.length > 0 && stack.every((open) => open === "(") ? stack.length : 0;
|
|
84869
|
+
return { deficit, regexSkips };
|
|
84870
|
+
}
|
|
84871
|
+
function parsesAsAsyncStatement(line) {
|
|
84872
|
+
try {
|
|
84873
|
+
new Function(`return (async () => {
|
|
84874
|
+
${line}
|
|
84875
|
+
})`);
|
|
84876
|
+
return true;
|
|
84877
|
+
} catch {
|
|
84878
|
+
return false;
|
|
84879
|
+
}
|
|
84880
|
+
}
|
|
84727
84881
|
function closeUnclosedRegexLiterals(code) {
|
|
84728
84882
|
const anchors = [
|
|
84729
84883
|
{ rx: /\b(?:waitForURL|toHaveURL)\(\//g, mode: "url" },
|
|
@@ -84762,12 +84916,33 @@ var require_discover_generator = __commonJS({
|
|
|
84762
84916
|
const term = findRegexTerminator(code, slash, TERMINATORS[mode]);
|
|
84763
84917
|
out.push(code.slice(cursor, slash));
|
|
84764
84918
|
const raw = code.slice(slash + 1, term).replace(/\s+$/, "");
|
|
84919
|
+
let closedLiteral;
|
|
84765
84920
|
if (mode === "url") {
|
|
84766
84921
|
const escaped = raw.replace(/(?<!\\)\//g, "\\/");
|
|
84767
84922
|
const finalBody = escaped.startsWith("\\/") ? escaped : `\\/${escaped}`;
|
|
84768
|
-
|
|
84923
|
+
closedLiteral = `/${finalBody}/`;
|
|
84769
84924
|
} else {
|
|
84770
|
-
|
|
84925
|
+
closedLiteral = `/${raw}/`;
|
|
84926
|
+
}
|
|
84927
|
+
out.push(closedLiteral);
|
|
84928
|
+
if (code[term] === ")") {
|
|
84929
|
+
const lineStart = code.lastIndexOf("\n", slash) + 1;
|
|
84930
|
+
const nextNewline = code.indexOf("\n", term);
|
|
84931
|
+
const lineEnd = nextNewline === -1 ? code.length : nextNewline;
|
|
84932
|
+
const tail = code.slice(term, lineEnd);
|
|
84933
|
+
const physicalLine = code.slice(lineStart, lineEnd);
|
|
84934
|
+
const lineIsPlain = !physicalLine.includes("`") && !physicalLine.includes("/*") && !physicalLine.includes("*/");
|
|
84935
|
+
const soleAnchorOnLine = !hits.some((h) => h.slash !== slash && h.slash >= lineStart && h.slash < lineEnd);
|
|
84936
|
+
if (lineIsPlain && soleAnchorOnLine && /;\s*$/.test(tail)) {
|
|
84937
|
+
const repairedLine = code.slice(lineStart, slash) + "/x/" + tail;
|
|
84938
|
+
const { deficit, regexSkips } = lineParenDeficit(repairedLine);
|
|
84939
|
+
if (deficit > 0 && deficit <= 3 && regexSkips === 1) {
|
|
84940
|
+
const candidateLine = code.slice(lineStart, slash) + closedLiteral + ")".repeat(deficit) + tail;
|
|
84941
|
+
if (parsesAsAsyncStatement(candidateLine)) {
|
|
84942
|
+
out.push(")".repeat(deficit));
|
|
84943
|
+
}
|
|
84944
|
+
}
|
|
84945
|
+
}
|
|
84771
84946
|
}
|
|
84772
84947
|
cursor = term;
|
|
84773
84948
|
}
|
|
@@ -337145,7 +337320,7 @@ var require_package4 = __commonJS({
|
|
|
337145
337320
|
"package.json"(exports2, module2) {
|
|
337146
337321
|
module2.exports = {
|
|
337147
337322
|
name: "@validate.qa/runner",
|
|
337148
|
-
version: "1.0.
|
|
337323
|
+
version: "1.0.41",
|
|
337149
337324
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
337150
337325
|
bin: {
|
|
337151
337326
|
"validate-runner": "dist/cli.js"
|
|
@@ -337154,7 +337329,7 @@ var require_package4 = __commonJS({
|
|
|
337154
337329
|
build: "tsup",
|
|
337155
337330
|
dev: "tsx src/cli.ts",
|
|
337156
337331
|
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",
|
|
337332
|
+
"test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
|
|
337158
337333
|
prepublishOnly: "npm run build"
|
|
337159
337334
|
},
|
|
337160
337335
|
files: [
|
|
@@ -343302,6 +343477,17 @@ var MobileBridgeDriver = class {
|
|
|
343302
343477
|
return size;
|
|
343303
343478
|
});
|
|
343304
343479
|
}
|
|
343480
|
+
/**
|
|
343481
|
+
* Recovery-free liveness probe for keep-alive ticks (mobile-session-keepalive).
|
|
343482
|
+
* Deliberately NOT wrapped in withSessionRecovery: keep-alive ticks are
|
|
343483
|
+
* fire-and-forget, so a failing probe must never consume the bounded recovery
|
|
343484
|
+
* budget or relaunch the app from the background — a genuinely dead session
|
|
343485
|
+
* surfaces (with its precise error) on the caller's next REAL command, which
|
|
343486
|
+
* still runs the full recovery path.
|
|
343487
|
+
*/
|
|
343488
|
+
async keepAlivePing() {
|
|
343489
|
+
await this.assertSessionReachable();
|
|
343490
|
+
}
|
|
343305
343491
|
// ── Action result wrapper ──────────────────────────────
|
|
343306
343492
|
/**
|
|
343307
343493
|
* Run an action body, let the UI tree settle, then capture the post-action
|
|
@@ -345859,6 +346045,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345859
346045
|
setExecutorBusy(true);
|
|
345860
346046
|
let session = null;
|
|
345861
346047
|
let capture = null;
|
|
346048
|
+
let discoveryTeardownStarted = false;
|
|
345862
346049
|
try {
|
|
345863
346050
|
const discoverySessionTarget = {
|
|
345864
346051
|
appId: mobileTarget.appId,
|
|
@@ -345910,6 +346097,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345910
346097
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
345911
346098
|
maxSessionRecoveries: 1,
|
|
345912
346099
|
recoverSession: async (cause) => {
|
|
346100
|
+
if (discoveryTeardownStarted) {
|
|
346101
|
+
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
346102
|
+
}
|
|
345913
346103
|
const lost = session;
|
|
345914
346104
|
mobileOnLog(
|
|
345915
346105
|
`[appium] discovery session lost (${cause.message}); recreating once on the leased device`
|
|
@@ -345923,6 +346113,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345923
346113
|
}
|
|
345924
346114
|
}
|
|
345925
346115
|
const replacement = await openDiscoverySession();
|
|
346116
|
+
if (discoveryTeardownStarted) {
|
|
346117
|
+
try {
|
|
346118
|
+
await replacement.stop();
|
|
346119
|
+
} catch {
|
|
346120
|
+
}
|
|
346121
|
+
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
346122
|
+
}
|
|
345926
346123
|
session = replacement;
|
|
345927
346124
|
attachMirrorSession(replacement.sessionId, platform3, replacement.deviceId);
|
|
345928
346125
|
liveBrowserHandle.patch({
|
|
@@ -345962,6 +346159,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345962
346159
|
onPlanReady
|
|
345963
346160
|
});
|
|
345964
346161
|
} finally {
|
|
346162
|
+
discoveryTeardownStarted = true;
|
|
345965
346163
|
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
345966
346164
|
const timeboxed = async (step) => Promise.race([
|
|
345967
346165
|
step,
|
|
@@ -345997,8 +346195,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345997
346195
|
}
|
|
345998
346196
|
if (session) {
|
|
345999
346197
|
detachMirrorSession(session.deviceId);
|
|
346198
|
+
const closing = session;
|
|
346000
346199
|
try {
|
|
346001
|
-
await timeboxed(
|
|
346200
|
+
const stopped = await timeboxed(closing.stop());
|
|
346201
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
346202
|
+
mobileOnLog(`[appium] session ${closing.sessionId} stop timed out after 45s \u2014 releasing device ${closing.deviceId} anyway; Appium may hold the session until newCommandTimeout reaps it`);
|
|
346203
|
+
}
|
|
346002
346204
|
} catch {
|
|
346003
346205
|
}
|
|
346004
346206
|
}
|
|
@@ -346436,7 +346638,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346436
346638
|
relaunch: async () => {
|
|
346437
346639
|
await currentDriver().relaunch();
|
|
346438
346640
|
},
|
|
346439
|
-
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState()
|
|
346641
|
+
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState(),
|
|
346642
|
+
// Recovery-free liveness probe so the loop's keep-alive can span
|
|
346643
|
+
// long AI waits without touching the driver's recovery machinery
|
|
346644
|
+
// (see mobile-session-keepalive.ts).
|
|
346645
|
+
keepAlivePing: async () => {
|
|
346646
|
+
await currentDriver().keepAlivePing();
|
|
346647
|
+
}
|
|
346440
346648
|
};
|
|
346441
346649
|
const runNativeWithSessionHandoff = async (verifyArgs) => {
|
|
346442
346650
|
await closeLiveSession();
|
package/dist/cli.mjs
CHANGED
|
@@ -1791,6 +1791,83 @@ var init_constants = __esm({
|
|
|
1791
1791
|
}
|
|
1792
1792
|
});
|
|
1793
1793
|
|
|
1794
|
+
// ../shared/dist/project-key.js
|
|
1795
|
+
function isValidProjectKey(key) {
|
|
1796
|
+
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
1797
|
+
}
|
|
1798
|
+
function randomChar(rng) {
|
|
1799
|
+
return KEY_ALPHABET[Math.floor(rng() * KEY_ALPHABET.length)] ?? "0";
|
|
1800
|
+
}
|
|
1801
|
+
function randomKey(length, rng) {
|
|
1802
|
+
let out = "";
|
|
1803
|
+
for (let i = 0; i < length; i++)
|
|
1804
|
+
out += randomChar(rng);
|
|
1805
|
+
return out;
|
|
1806
|
+
}
|
|
1807
|
+
function alnumLower(name) {
|
|
1808
|
+
return (name ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1809
|
+
}
|
|
1810
|
+
function deriveProjectKey(name, rng = Math.random) {
|
|
1811
|
+
const base2 = alnumLower(name).slice(0, 3);
|
|
1812
|
+
return base2.length >= PROJECT_KEY_MIN_LENGTH ? base2 : randomKey(3, rng);
|
|
1813
|
+
}
|
|
1814
|
+
function generateUniqueProjectKey(name, taken, rng = Math.random) {
|
|
1815
|
+
const isFree = (k) => isValidProjectKey(k) && !taken.has(k);
|
|
1816
|
+
const primary = deriveProjectKey(name, rng);
|
|
1817
|
+
if (isFree(primary))
|
|
1818
|
+
return primary;
|
|
1819
|
+
const prefix2 = alnumLower(name).slice(0, 2);
|
|
1820
|
+
for (const suffixLen of [1, 2]) {
|
|
1821
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1822
|
+
const stem = prefix2.length >= 1 ? prefix2 : deriveProjectKey(name, rng).slice(0, 2);
|
|
1823
|
+
const candidate = stem + randomKey(suffixLen, rng);
|
|
1824
|
+
if (isFree(candidate))
|
|
1825
|
+
return candidate;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
for (let len = 4; len <= PROJECT_KEY_MAX_LENGTH; len++) {
|
|
1829
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1830
|
+
const candidate = randomKey(len, rng);
|
|
1831
|
+
if (isFree(candidate))
|
|
1832
|
+
return candidate;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
throw new Error("Unable to generate a unique project key");
|
|
1836
|
+
}
|
|
1837
|
+
var RESERVED_PROJECT_KEYS, PROJECT_KEY_MIN_LENGTH, PROJECT_KEY_MAX_LENGTH, PROJECT_KEY_PATTERN, KEY_ALPHABET;
|
|
1838
|
+
var init_project_key = __esm({
|
|
1839
|
+
"../shared/dist/project-key.js"() {
|
|
1840
|
+
"use strict";
|
|
1841
|
+
RESERVED_PROJECT_KEYS = /* @__PURE__ */ new Set([
|
|
1842
|
+
"tests",
|
|
1843
|
+
"sessions",
|
|
1844
|
+
"settings",
|
|
1845
|
+
"reports",
|
|
1846
|
+
"account",
|
|
1847
|
+
"notifications",
|
|
1848
|
+
"support",
|
|
1849
|
+
"site-map",
|
|
1850
|
+
"api-intelligence",
|
|
1851
|
+
"selector-registry",
|
|
1852
|
+
"project-knowledge",
|
|
1853
|
+
"test-data",
|
|
1854
|
+
"audit",
|
|
1855
|
+
"validate-copilot",
|
|
1856
|
+
"qa-copilot",
|
|
1857
|
+
"insights",
|
|
1858
|
+
"regression",
|
|
1859
|
+
"reviews",
|
|
1860
|
+
"admin",
|
|
1861
|
+
"login",
|
|
1862
|
+
"admin-view"
|
|
1863
|
+
]);
|
|
1864
|
+
PROJECT_KEY_MIN_LENGTH = 2;
|
|
1865
|
+
PROJECT_KEY_MAX_LENGTH = 12;
|
|
1866
|
+
PROJECT_KEY_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])$/;
|
|
1867
|
+
KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1794
1871
|
// ../shared/dist/mobile-deeplink.js
|
|
1795
1872
|
function isSafeDeepLink(value) {
|
|
1796
1873
|
if (typeof value !== "string")
|
|
@@ -4980,6 +5057,10 @@ __export(dist_exports, {
|
|
|
4980
5057
|
PLAN_DEFINITIONS: () => PLAN_DEFINITIONS,
|
|
4981
5058
|
PLAN_ORDER: () => PLAN_ORDER,
|
|
4982
5059
|
PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
|
|
5060
|
+
PROJECT_KEY_MAX_LENGTH: () => PROJECT_KEY_MAX_LENGTH,
|
|
5061
|
+
PROJECT_KEY_MIN_LENGTH: () => PROJECT_KEY_MIN_LENGTH,
|
|
5062
|
+
PROJECT_KEY_PATTERN: () => PROJECT_KEY_PATTERN,
|
|
5063
|
+
RESERVED_PROJECT_KEYS: () => RESERVED_PROJECT_KEYS,
|
|
4983
5064
|
SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
|
|
4984
5065
|
SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
|
|
4985
5066
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
@@ -5013,6 +5094,7 @@ __export(dist_exports, {
|
|
|
5013
5094
|
controlSignature: () => controlSignature,
|
|
5014
5095
|
countInnerTests: () => countInnerTests,
|
|
5015
5096
|
decideHarPosted: () => decideHarPosted,
|
|
5097
|
+
deriveProjectKey: () => deriveProjectKey,
|
|
5016
5098
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5017
5099
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
5018
5100
|
effectiveTestCount: () => effectiveTestCount,
|
|
@@ -5029,6 +5111,7 @@ __export(dist_exports, {
|
|
|
5029
5111
|
formatLimit: () => formatLimit,
|
|
5030
5112
|
fromDbPricingPlan: () => fromDbPricingPlan,
|
|
5031
5113
|
generateMobileTestFromSession: () => generateMobileTestFromSession,
|
|
5114
|
+
generateUniqueProjectKey: () => generateUniqueProjectKey,
|
|
5032
5115
|
getDefaultPlanForTier: () => getDefaultPlanForTier,
|
|
5033
5116
|
getDeploymentTierForPlan: () => getDeploymentTierForPlan,
|
|
5034
5117
|
getPlanChangeType: () => getPlanChangeType,
|
|
@@ -5058,6 +5141,7 @@ __export(dist_exports, {
|
|
|
5058
5141
|
isTrialExpired: () => isTrialExpired,
|
|
5059
5142
|
isUIVariant: () => isUIVariant,
|
|
5060
5143
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
5144
|
+
isValidProjectKey: () => isValidProjectKey,
|
|
5061
5145
|
isValidSttProvider: () => isValidSttProvider,
|
|
5062
5146
|
isValidTtsModel: () => isValidTtsModel,
|
|
5063
5147
|
isValidTtsVoiceId: () => isValidTtsVoiceId,
|
|
@@ -5103,6 +5187,7 @@ var init_dist = __esm({
|
|
|
5103
5187
|
"use strict";
|
|
5104
5188
|
init_types();
|
|
5105
5189
|
init_constants();
|
|
5190
|
+
init_project_key();
|
|
5106
5191
|
init_mobile_test_generation();
|
|
5107
5192
|
init_mobile_step_healer();
|
|
5108
5193
|
init_mobile_cross_platform();
|
|
@@ -5893,17 +5978,27 @@ var require_mobile_session_keepalive = __commonJS({
|
|
|
5893
5978
|
"use strict";
|
|
5894
5979
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5895
5980
|
exports.MOBILE_KEEPALIVE_INTERVAL_MS = void 0;
|
|
5981
|
+
exports.computeKeepAliveIntervalMs = computeKeepAliveIntervalMs;
|
|
5896
5982
|
exports.withMobileSessionKeepAlive = withMobileSessionKeepAlive;
|
|
5897
|
-
|
|
5898
|
-
const
|
|
5899
|
-
|
|
5900
|
-
|
|
5983
|
+
function computeKeepAliveIntervalMs(keepaliveMsRaw, newCommandTimeoutSecondsRaw) {
|
|
5984
|
+
const explicit = keepaliveMsRaw?.trim() ? Number(keepaliveMsRaw) : NaN;
|
|
5985
|
+
if (Number.isFinite(explicit) && explicit >= 0)
|
|
5986
|
+
return Math.floor(explicit);
|
|
5987
|
+
const timeoutSeconds = Number(newCommandTimeoutSecondsRaw);
|
|
5988
|
+
const reapWindowMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? Math.floor(timeoutSeconds) * 1e3 : 6e5;
|
|
5989
|
+
return Math.min(12e4, Math.max(5e3, Math.floor(reapWindowMs / 3)));
|
|
5990
|
+
}
|
|
5991
|
+
exports.MOBILE_KEEPALIVE_INTERVAL_MS = computeKeepAliveIntervalMs(process.env.VALIDATEQA_MOBILE_KEEPALIVE_MS, process.env.VALIDATEQA_MOBILE_NEW_COMMAND_TIMEOUT);
|
|
5901
5992
|
async function withMobileSessionKeepAlive(driver, fn, intervalMs = exports.MOBILE_KEEPALIVE_INTERVAL_MS) {
|
|
5902
5993
|
if (intervalMs <= 0)
|
|
5903
5994
|
return fn();
|
|
5904
5995
|
const timer = setInterval(() => {
|
|
5905
|
-
|
|
5906
|
-
|
|
5996
|
+
try {
|
|
5997
|
+
const probe = driver.keepAlivePing ? driver.keepAlivePing() : driver.windowSize();
|
|
5998
|
+
void probe.catch(() => {
|
|
5999
|
+
});
|
|
6000
|
+
} catch {
|
|
6001
|
+
}
|
|
5907
6002
|
}, intervalMs);
|
|
5908
6003
|
timer.unref?.();
|
|
5909
6004
|
try {
|
|
@@ -73998,6 +74093,7 @@ var require_mobile_heal_loop = __commonJS({
|
|
|
73998
74093
|
var mobile_heal_postprocess_js_1 = require_mobile_heal_postprocess();
|
|
73999
74094
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
74000
74095
|
var selector_registry_context_js_1 = require_selector_registry_context();
|
|
74096
|
+
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
74001
74097
|
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
74098
|
## You MUST try to fix the steps
|
|
74003
74099
|
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 +74985,19 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74889
74985
|
};
|
|
74890
74986
|
}
|
|
74891
74987
|
const maxPasses = envInt2("MOBILE_HEAL_PHASE2_MAX_PASSES") ?? DEFAULT_PHASE2_MAX_PASSES;
|
|
74988
|
+
const keepAlivePingable = {
|
|
74989
|
+
...typeof liveDriver.keepAlivePing === "function" ? { keepAlivePing: liveDriver.keepAlivePing.bind(liveDriver) } : {},
|
|
74990
|
+
// WARNING: this fallback routes through snapshot(), which sits inside
|
|
74991
|
+
// withSessionRecovery on the real driver — a failed tick could consume the
|
|
74992
|
+
// bounded recovery budget, the exact bug keepAlivePing exists to avoid.
|
|
74993
|
+
// Every current MobileLiveDriverLike provides keepAlivePing, so this only
|
|
74994
|
+
// exists for structural compatibility; implement keepAlivePing on any new
|
|
74995
|
+
// facade rather than relying on it.
|
|
74996
|
+
windowSize: async () => {
|
|
74997
|
+
await liveDriver.snapshot();
|
|
74998
|
+
return null;
|
|
74999
|
+
}
|
|
75000
|
+
};
|
|
74892
75001
|
let baseSteps = args.steps;
|
|
74893
75002
|
let priorError = args.priorError ?? null;
|
|
74894
75003
|
let bestSteps;
|
|
@@ -74923,10 +75032,10 @@ For a fix, return the COMPLETE steps array (not a diff). Use "observe" only to r
|
|
|
74923
75032
|
];
|
|
74924
75033
|
let content;
|
|
74925
75034
|
try {
|
|
74926
|
-
const response = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
75035
|
+
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
75036
|
arm();
|
|
74928
75037
|
return aiClient.chat.completions.create({ model, messages, max_tokens: 8e3, temperature: 0.1 }, { signal });
|
|
74929
|
-
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true });
|
|
75038
|
+
}), { label: `mobile-live-heal-pass-${pass}`, deferTimeoutUntilArmed: true }));
|
|
74930
75039
|
content = response?.choices?.[0]?.message?.content ?? "";
|
|
74931
75040
|
} catch {
|
|
74932
75041
|
continue;
|
|
@@ -84729,6 +84838,51 @@ var require_discover_generator = __commonJS({
|
|
|
84729
84838
|
}
|
|
84730
84839
|
return s.length;
|
|
84731
84840
|
}
|
|
84841
|
+
function lineParenDeficit(line) {
|
|
84842
|
+
const stack = [];
|
|
84843
|
+
let regexSkips = 0;
|
|
84844
|
+
for (let i = 0; i < line.length; i++) {
|
|
84845
|
+
const ch = line[i];
|
|
84846
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
84847
|
+
i = skipQuotedLiteral(line, i) - 1;
|
|
84848
|
+
continue;
|
|
84849
|
+
}
|
|
84850
|
+
if (ch === "/" && line[i + 1] === "/") {
|
|
84851
|
+
i = skipLineComment(line, i) - 1;
|
|
84852
|
+
continue;
|
|
84853
|
+
}
|
|
84854
|
+
if (ch === "/" && line[i + 1] === "*") {
|
|
84855
|
+
i = skipBlockComment(line, i) - 1;
|
|
84856
|
+
continue;
|
|
84857
|
+
}
|
|
84858
|
+
if (ch === "/" && isRegexStart(line, i)) {
|
|
84859
|
+
regexSkips++;
|
|
84860
|
+
i = skipRegexLiteral(line, i) - 1;
|
|
84861
|
+
continue;
|
|
84862
|
+
}
|
|
84863
|
+
if (ch === "(" || ch === "{" || ch === "[") {
|
|
84864
|
+
stack.push(ch);
|
|
84865
|
+
continue;
|
|
84866
|
+
}
|
|
84867
|
+
if (ch === ")" || ch === "}" || ch === "]") {
|
|
84868
|
+
if (stack[stack.length - 1] !== CLOSE_TO_OPEN[ch])
|
|
84869
|
+
return { deficit: 0, regexSkips };
|
|
84870
|
+
stack.pop();
|
|
84871
|
+
}
|
|
84872
|
+
}
|
|
84873
|
+
const deficit = stack.length > 0 && stack.every((open) => open === "(") ? stack.length : 0;
|
|
84874
|
+
return { deficit, regexSkips };
|
|
84875
|
+
}
|
|
84876
|
+
function parsesAsAsyncStatement(line) {
|
|
84877
|
+
try {
|
|
84878
|
+
new Function(`return (async () => {
|
|
84879
|
+
${line}
|
|
84880
|
+
})`);
|
|
84881
|
+
return true;
|
|
84882
|
+
} catch {
|
|
84883
|
+
return false;
|
|
84884
|
+
}
|
|
84885
|
+
}
|
|
84732
84886
|
function closeUnclosedRegexLiterals(code) {
|
|
84733
84887
|
const anchors = [
|
|
84734
84888
|
{ rx: /\b(?:waitForURL|toHaveURL)\(\//g, mode: "url" },
|
|
@@ -84767,12 +84921,33 @@ var require_discover_generator = __commonJS({
|
|
|
84767
84921
|
const term = findRegexTerminator(code, slash, TERMINATORS[mode]);
|
|
84768
84922
|
out.push(code.slice(cursor, slash));
|
|
84769
84923
|
const raw = code.slice(slash + 1, term).replace(/\s+$/, "");
|
|
84924
|
+
let closedLiteral;
|
|
84770
84925
|
if (mode === "url") {
|
|
84771
84926
|
const escaped = raw.replace(/(?<!\\)\//g, "\\/");
|
|
84772
84927
|
const finalBody = escaped.startsWith("\\/") ? escaped : `\\/${escaped}`;
|
|
84773
|
-
|
|
84928
|
+
closedLiteral = `/${finalBody}/`;
|
|
84774
84929
|
} else {
|
|
84775
|
-
|
|
84930
|
+
closedLiteral = `/${raw}/`;
|
|
84931
|
+
}
|
|
84932
|
+
out.push(closedLiteral);
|
|
84933
|
+
if (code[term] === ")") {
|
|
84934
|
+
const lineStart = code.lastIndexOf("\n", slash) + 1;
|
|
84935
|
+
const nextNewline = code.indexOf("\n", term);
|
|
84936
|
+
const lineEnd = nextNewline === -1 ? code.length : nextNewline;
|
|
84937
|
+
const tail = code.slice(term, lineEnd);
|
|
84938
|
+
const physicalLine = code.slice(lineStart, lineEnd);
|
|
84939
|
+
const lineIsPlain = !physicalLine.includes("`") && !physicalLine.includes("/*") && !physicalLine.includes("*/");
|
|
84940
|
+
const soleAnchorOnLine = !hits.some((h) => h.slash !== slash && h.slash >= lineStart && h.slash < lineEnd);
|
|
84941
|
+
if (lineIsPlain && soleAnchorOnLine && /;\s*$/.test(tail)) {
|
|
84942
|
+
const repairedLine = code.slice(lineStart, slash) + "/x/" + tail;
|
|
84943
|
+
const { deficit, regexSkips } = lineParenDeficit(repairedLine);
|
|
84944
|
+
if (deficit > 0 && deficit <= 3 && regexSkips === 1) {
|
|
84945
|
+
const candidateLine = code.slice(lineStart, slash) + closedLiteral + ")".repeat(deficit) + tail;
|
|
84946
|
+
if (parsesAsAsyncStatement(candidateLine)) {
|
|
84947
|
+
out.push(")".repeat(deficit));
|
|
84948
|
+
}
|
|
84949
|
+
}
|
|
84950
|
+
}
|
|
84776
84951
|
}
|
|
84777
84952
|
cursor = term;
|
|
84778
84953
|
}
|
|
@@ -337150,7 +337325,7 @@ var require_package4 = __commonJS({
|
|
|
337150
337325
|
"package.json"(exports, module) {
|
|
337151
337326
|
module.exports = {
|
|
337152
337327
|
name: "@validate.qa/runner",
|
|
337153
|
-
version: "1.0.
|
|
337328
|
+
version: "1.0.41",
|
|
337154
337329
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
337155
337330
|
bin: {
|
|
337156
337331
|
"validate-runner": "dist/cli.js"
|
|
@@ -337159,7 +337334,7 @@ var require_package4 = __commonJS({
|
|
|
337159
337334
|
build: "tsup",
|
|
337160
337335
|
dev: "tsx src/cli.ts",
|
|
337161
337336
|
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",
|
|
337337
|
+
"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
337338
|
prepublishOnly: "npm run build"
|
|
337164
337339
|
},
|
|
337165
337340
|
files: [
|
|
@@ -343310,6 +343485,17 @@ var MobileBridgeDriver = class {
|
|
|
343310
343485
|
return size;
|
|
343311
343486
|
});
|
|
343312
343487
|
}
|
|
343488
|
+
/**
|
|
343489
|
+
* Recovery-free liveness probe for keep-alive ticks (mobile-session-keepalive).
|
|
343490
|
+
* Deliberately NOT wrapped in withSessionRecovery: keep-alive ticks are
|
|
343491
|
+
* fire-and-forget, so a failing probe must never consume the bounded recovery
|
|
343492
|
+
* budget or relaunch the app from the background — a genuinely dead session
|
|
343493
|
+
* surfaces (with its precise error) on the caller's next REAL command, which
|
|
343494
|
+
* still runs the full recovery path.
|
|
343495
|
+
*/
|
|
343496
|
+
async keepAlivePing() {
|
|
343497
|
+
await this.assertSessionReachable();
|
|
343498
|
+
}
|
|
343313
343499
|
// ── Action result wrapper ──────────────────────────────
|
|
343314
343500
|
/**
|
|
343315
343501
|
* Run an action body, let the UI tree settle, then capture the post-action
|
|
@@ -345867,6 +346053,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345867
346053
|
setExecutorBusy(true);
|
|
345868
346054
|
let session = null;
|
|
345869
346055
|
let capture = null;
|
|
346056
|
+
let discoveryTeardownStarted = false;
|
|
345870
346057
|
try {
|
|
345871
346058
|
const discoverySessionTarget = {
|
|
345872
346059
|
appId: mobileTarget.appId,
|
|
@@ -345918,6 +346105,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345918
346105
|
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session.deviceId),
|
|
345919
346106
|
maxSessionRecoveries: 1,
|
|
345920
346107
|
recoverSession: async (cause) => {
|
|
346108
|
+
if (discoveryTeardownStarted) {
|
|
346109
|
+
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
346110
|
+
}
|
|
345921
346111
|
const lost = session;
|
|
345922
346112
|
mobileOnLog(
|
|
345923
346113
|
`[appium] discovery session lost (${cause.message}); recreating once on the leased device`
|
|
@@ -345931,6 +346121,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345931
346121
|
}
|
|
345932
346122
|
}
|
|
345933
346123
|
const replacement = await openDiscoverySession();
|
|
346124
|
+
if (discoveryTeardownStarted) {
|
|
346125
|
+
try {
|
|
346126
|
+
await replacement.stop();
|
|
346127
|
+
} catch {
|
|
346128
|
+
}
|
|
346129
|
+
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
346130
|
+
}
|
|
345934
346131
|
session = replacement;
|
|
345935
346132
|
attachMirrorSession(replacement.sessionId, platform3, replacement.deviceId);
|
|
345936
346133
|
liveBrowserHandle.patch({
|
|
@@ -345970,6 +346167,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345970
346167
|
onPlanReady
|
|
345971
346168
|
});
|
|
345972
346169
|
} finally {
|
|
346170
|
+
discoveryTeardownStarted = true;
|
|
345973
346171
|
const TEARDOWN_STEP_TIMEOUT = /* @__PURE__ */ Symbol("vqa-teardown-timeout");
|
|
345974
346172
|
const timeboxed = async (step) => Promise.race([
|
|
345975
346173
|
step,
|
|
@@ -346005,8 +346203,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346005
346203
|
}
|
|
346006
346204
|
if (session) {
|
|
346007
346205
|
detachMirrorSession(session.deviceId);
|
|
346206
|
+
const closing = session;
|
|
346008
346207
|
try {
|
|
346009
|
-
await timeboxed(
|
|
346208
|
+
const stopped = await timeboxed(closing.stop());
|
|
346209
|
+
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
346210
|
+
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`);
|
|
346211
|
+
}
|
|
346010
346212
|
} catch {
|
|
346011
346213
|
}
|
|
346012
346214
|
}
|
|
@@ -346444,7 +346646,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346444
346646
|
relaunch: async () => {
|
|
346445
346647
|
await currentDriver().relaunch();
|
|
346446
346648
|
},
|
|
346447
|
-
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState()
|
|
346649
|
+
resetAppState: async () => target.resetPolicy === "PRESERVE_APP_DATA" ? false : currentDriver().resetAppState(),
|
|
346650
|
+
// Recovery-free liveness probe so the loop's keep-alive can span
|
|
346651
|
+
// long AI waits without touching the driver's recovery machinery
|
|
346652
|
+
// (see mobile-session-keepalive.ts).
|
|
346653
|
+
keepAlivePing: async () => {
|
|
346654
|
+
await currentDriver().keepAlivePing();
|
|
346655
|
+
}
|
|
346448
346656
|
};
|
|
346449
346657
|
const runNativeWithSessionHandoff = async (verifyArgs) => {
|
|
346450
346658
|
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.41",
|
|
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": [
|