@validate.qa/runner 1.0.71 → 1.0.73
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 +315 -88
- package/dist/cli.mjs +315 -88
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5854,6 +5854,29 @@ function normalizeTtsVoiceId(voiceId) {
|
|
|
5854
5854
|
return null;
|
|
5855
5855
|
return LEGACY_TTS_VOICE_ID_MAP[trimmed] ?? trimmed;
|
|
5856
5856
|
}
|
|
5857
|
+
function normalizeTtsModel(model) {
|
|
5858
|
+
if (!model)
|
|
5859
|
+
return null;
|
|
5860
|
+
const trimmed = model.trim();
|
|
5861
|
+
if (!trimmed)
|
|
5862
|
+
return null;
|
|
5863
|
+
return LEGACY_TTS_MODEL_MAP[trimmed] ?? trimmed;
|
|
5864
|
+
}
|
|
5865
|
+
function ttsFallbackModel(primary) {
|
|
5866
|
+
const normalized = normalizeTtsModel(primary);
|
|
5867
|
+
if (!normalized)
|
|
5868
|
+
return null;
|
|
5869
|
+
return TTS_FALLBACK_MODEL_MAP[normalized] ?? null;
|
|
5870
|
+
}
|
|
5871
|
+
function resolveTtsModelChain(model) {
|
|
5872
|
+
const normalized = normalizeTtsModel(model);
|
|
5873
|
+
const primary = normalized && normalized in TTS_FALLBACK_MODEL_MAP ? normalized : DEFAULT_TTS_MODEL;
|
|
5874
|
+
const chain = [primary];
|
|
5875
|
+
const fallback = TTS_FALLBACK_MODEL_MAP[primary];
|
|
5876
|
+
if (fallback && fallback !== primary)
|
|
5877
|
+
chain.push(fallback);
|
|
5878
|
+
return chain;
|
|
5879
|
+
}
|
|
5857
5880
|
function sttModelsForProvider(provider) {
|
|
5858
5881
|
if (provider === "openai") {
|
|
5859
5882
|
return STT_MODEL_OPTIONS.filter((m) => m.provider === "openai");
|
|
@@ -5901,16 +5924,18 @@ function isValidTtsVoiceId(value) {
|
|
|
5901
5924
|
return TTS_VOICE_OPTIONS.some((v) => v.id === normalized);
|
|
5902
5925
|
}
|
|
5903
5926
|
function isValidTtsModel(value) {
|
|
5904
|
-
|
|
5927
|
+
const normalized = normalizeTtsModel(value);
|
|
5928
|
+
if (!normalized)
|
|
5905
5929
|
return false;
|
|
5906
|
-
return TTS_MODEL_OPTIONS.some((m) => m.id ===
|
|
5930
|
+
return TTS_MODEL_OPTIONS.some((m) => m.id === normalized);
|
|
5907
5931
|
}
|
|
5908
|
-
var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
|
|
5932
|
+
var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_TURBO_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, LEGACY_TTS_MODEL_MAP, TTS_FALLBACK_MODEL_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
|
|
5909
5933
|
var init_voice_config = __esm({
|
|
5910
5934
|
"../shared/dist/voice-config.js"() {
|
|
5911
5935
|
"use strict";
|
|
5912
5936
|
VOICE_PREVIEW_SAMPLE = "Hello! I am your Validate Copilot assistant. I can help you run tests, review failures, and generate new coverage for your project.";
|
|
5913
|
-
DEFAULT_TTS_MODEL = "speech-2.
|
|
5937
|
+
DEFAULT_TTS_MODEL = "speech-2.8-hd";
|
|
5938
|
+
DEFAULT_TTS_TURBO_MODEL = "speech-2.8-turbo";
|
|
5914
5939
|
DEFAULT_TTS_VOICE_ID = "English_Graceful_Lady";
|
|
5915
5940
|
DEFAULT_TTS_SPEED = 1;
|
|
5916
5941
|
LEGACY_TTS_VOICE_ID_MAP = {
|
|
@@ -5919,6 +5944,18 @@ var init_voice_config = __esm({
|
|
|
5919
5944
|
English_Deep_Voice_Man: "English_ManWithDeepVoice",
|
|
5920
5945
|
English_CasualMan: "English_Jovialman"
|
|
5921
5946
|
};
|
|
5947
|
+
LEGACY_TTS_MODEL_MAP = {
|
|
5948
|
+
"speech-2.6-hd": "speech-2.8-hd",
|
|
5949
|
+
"speech-2.6-turbo": "speech-2.8-turbo",
|
|
5950
|
+
"speech-02-hd": "speech-2.8-hd",
|
|
5951
|
+
"speech-02-turbo": "speech-2.8-turbo",
|
|
5952
|
+
"speech-01-hd": "speech-2.8-hd",
|
|
5953
|
+
"speech-01-turbo": "speech-2.8-turbo"
|
|
5954
|
+
};
|
|
5955
|
+
TTS_FALLBACK_MODEL_MAP = {
|
|
5956
|
+
"speech-2.8-hd": "speech-2.6-hd",
|
|
5957
|
+
"speech-2.8-turbo": "speech-2.6-turbo"
|
|
5958
|
+
};
|
|
5922
5959
|
DEFAULT_STT_MODEL_BY_PROVIDER = {
|
|
5923
5960
|
groq: "whisper-large-v3-turbo",
|
|
5924
5961
|
openai: "whisper-1"
|
|
@@ -5971,49 +6008,13 @@ var init_voice_config = __esm({
|
|
|
5971
6008
|
id: "speech-2.8-hd",
|
|
5972
6009
|
label: "Speech 2.8 HD",
|
|
5973
6010
|
provider: "minimax",
|
|
5974
|
-
description: "
|
|
6011
|
+
description: "High-definition quality (default). Auto-falls back to Speech 2.6 HD."
|
|
5975
6012
|
},
|
|
5976
6013
|
{
|
|
5977
6014
|
id: "speech-2.8-turbo",
|
|
5978
6015
|
label: "Speech 2.8 Turbo",
|
|
5979
6016
|
provider: "minimax",
|
|
5980
|
-
description: "
|
|
5981
|
-
},
|
|
5982
|
-
{
|
|
5983
|
-
id: "speech-2.6-hd",
|
|
5984
|
-
label: "Speech 2.6 HD",
|
|
5985
|
-
provider: "minimax",
|
|
5986
|
-
description: "High-definition MiniMax voice model."
|
|
5987
|
-
},
|
|
5988
|
-
{
|
|
5989
|
-
id: "speech-2.6-turbo",
|
|
5990
|
-
label: "Speech 2.6 Turbo",
|
|
5991
|
-
provider: "minimax",
|
|
5992
|
-
description: "Lower-latency Speech 2.6 variant."
|
|
5993
|
-
},
|
|
5994
|
-
{
|
|
5995
|
-
id: "speech-02-hd",
|
|
5996
|
-
label: "Speech 02 HD",
|
|
5997
|
-
provider: "minimax",
|
|
5998
|
-
description: "Previous-generation HD model."
|
|
5999
|
-
},
|
|
6000
|
-
{
|
|
6001
|
-
id: "speech-02-turbo",
|
|
6002
|
-
label: "Speech 02 Turbo",
|
|
6003
|
-
provider: "minimax",
|
|
6004
|
-
description: "Lower-latency Speech 02 variant."
|
|
6005
|
-
},
|
|
6006
|
-
{
|
|
6007
|
-
id: "speech-01-hd",
|
|
6008
|
-
label: "Speech 01 HD",
|
|
6009
|
-
provider: "minimax",
|
|
6010
|
-
description: "Legacy HD model."
|
|
6011
|
-
},
|
|
6012
|
-
{
|
|
6013
|
-
id: "speech-01-turbo",
|
|
6014
|
-
label: "Speech 01 Turbo",
|
|
6015
|
-
provider: "minimax",
|
|
6016
|
-
description: "Legacy fast model."
|
|
6017
|
+
description: "Lower latency. Auto-falls back to Speech 2.6 Turbo."
|
|
6017
6018
|
}
|
|
6018
6019
|
];
|
|
6019
6020
|
TTS_VOICE_OPTIONS = [
|
|
@@ -6297,6 +6298,7 @@ __export(dist_exports, {
|
|
|
6297
6298
|
DEFAULT_STT_MODEL_BY_PROVIDER: () => DEFAULT_STT_MODEL_BY_PROVIDER,
|
|
6298
6299
|
DEFAULT_TTS_MODEL: () => DEFAULT_TTS_MODEL,
|
|
6299
6300
|
DEFAULT_TTS_SPEED: () => DEFAULT_TTS_SPEED,
|
|
6301
|
+
DEFAULT_TTS_TURBO_MODEL: () => DEFAULT_TTS_TURBO_MODEL,
|
|
6300
6302
|
DEFAULT_TTS_VOICE_ID: () => DEFAULT_TTS_VOICE_ID,
|
|
6301
6303
|
DEFAULT_UI_VARIANT: () => DEFAULT_UI_VARIANT,
|
|
6302
6304
|
EMAIL_FROM_IDENTITIES: () => EMAIL_FROM_IDENTITIES,
|
|
@@ -6306,6 +6308,7 @@ __export(dist_exports, {
|
|
|
6306
6308
|
FLOW_SERVER_TYPES: () => FLOW_SERVER_TYPES,
|
|
6307
6309
|
GROUPABLE_CALL_TYPES: () => GROUPABLE_CALL_TYPES,
|
|
6308
6310
|
GROUP_LABELS: () => GROUP_LABELS,
|
|
6311
|
+
LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
|
|
6309
6312
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
6310
6313
|
MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
|
|
6311
6314
|
MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
|
|
@@ -6339,6 +6342,7 @@ __export(dist_exports, {
|
|
|
6339
6342
|
TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
|
|
6340
6343
|
TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
|
|
6341
6344
|
TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
|
|
6345
|
+
TTS_FALLBACK_MODEL_MAP: () => TTS_FALLBACK_MODEL_MAP,
|
|
6342
6346
|
TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
|
|
6343
6347
|
TTS_VOICE_OPTIONS: () => TTS_VOICE_OPTIONS,
|
|
6344
6348
|
UI_VARIANTS: () => UI_VARIANTS,
|
|
@@ -6441,6 +6445,7 @@ __export(dist_exports, {
|
|
|
6441
6445
|
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
6442
6446
|
mobileWanderTraceEnabled: () => mobileWanderTraceEnabled,
|
|
6443
6447
|
normalizeControlName: () => normalizeControlName,
|
|
6448
|
+
normalizeTtsModel: () => normalizeTtsModel,
|
|
6444
6449
|
normalizeTtsVoiceId: () => normalizeTtsVoiceId,
|
|
6445
6450
|
otherMobilePlatform: () => otherMobilePlatform,
|
|
6446
6451
|
parenDepthDelta: () => parenDepthDelta,
|
|
@@ -6459,6 +6464,7 @@ __export(dist_exports, {
|
|
|
6459
6464
|
resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
|
|
6460
6465
|
resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
|
|
6461
6466
|
resolveSttModelForProvider: () => resolveSttModelForProvider,
|
|
6467
|
+
resolveTtsModelChain: () => resolveTtsModelChain,
|
|
6462
6468
|
safeCurrentUrl: () => safeCurrentUrl,
|
|
6463
6469
|
safeOrigin: () => safeOrigin,
|
|
6464
6470
|
sanitizeSttErrorMessage: () => sanitizeSttErrorMessage,
|
|
@@ -6481,6 +6487,7 @@ __export(dist_exports, {
|
|
|
6481
6487
|
summarizeMobileReadiness: () => summarizeMobileReadiness,
|
|
6482
6488
|
summarizeRunHealth: () => summarizeRunHealth,
|
|
6483
6489
|
toDbPricingPlan: () => toDbPricingPlan,
|
|
6490
|
+
ttsFallbackModel: () => ttsFallbackModel,
|
|
6484
6491
|
validateAssertionStrength: () => validateAssertionStrength
|
|
6485
6492
|
});
|
|
6486
6493
|
var init_dist = __esm({
|
|
@@ -11491,7 +11498,7 @@ var require_thread_stream = __commonJS({
|
|
|
11491
11498
|
"use strict";
|
|
11492
11499
|
var { version } = require_package();
|
|
11493
11500
|
var { EventEmitter: EventEmitter2 } = require("events");
|
|
11494
|
-
var { Worker } = require("worker_threads");
|
|
11501
|
+
var { Worker: Worker2 } = require("worker_threads");
|
|
11495
11502
|
var { join: join8 } = require("path");
|
|
11496
11503
|
var { pathToFileURL } = require("url");
|
|
11497
11504
|
var { wait } = require_wait();
|
|
@@ -11544,7 +11551,7 @@ var require_thread_stream = __commonJS({
|
|
|
11544
11551
|
const { filename, workerData } = opts;
|
|
11545
11552
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
11546
11553
|
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
11547
|
-
const worker = new
|
|
11554
|
+
const worker = new Worker2(toExecute, {
|
|
11548
11555
|
...opts.workerOpts,
|
|
11549
11556
|
name: opts.workerOpts?.name || "thread-stream",
|
|
11550
11557
|
trackUnmanagedFds: false,
|
|
@@ -74644,6 +74651,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
74644
74651
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
74645
74652
|
exports2.computeMobileSurveyBreadthContinuation = computeMobileSurveyBreadthContinuation;
|
|
74646
74653
|
exports2.computeMobileBreadthFinishRedirectDecision = computeMobileBreadthFinishRedirectDecision;
|
|
74654
|
+
exports2.hybridScreenFingerprint = hybridScreenFingerprint;
|
|
74647
74655
|
exports2.runMobileExplorePhase = runMobileExplorePhase;
|
|
74648
74656
|
var crypto_1 = require("crypto");
|
|
74649
74657
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
@@ -75097,11 +75105,12 @@ var require_mobile_explorer = __commonJS({
|
|
|
75097
75105
|
const validSurfaceDigest = /^[0-9a-f]{16,64}$/.test(normalizedSurfaceDigest) ? normalizedSurfaceDigest : void 0;
|
|
75098
75106
|
const legacyElementDigests = [...new Set(elements.map((element) => element.pageIdentityDigest?.trim().toLowerCase() ?? "").filter((digest) => /^[0-9a-f]{16,64}$/.test(digest)))];
|
|
75099
75107
|
const pageIdentityDigest = validSurfaceDigest ?? (legacyElementDigests.length === 1 ? legacyElementDigests[0] : void 0);
|
|
75108
|
+
const hasStableControlIdentity = elements.some((element) => Boolean(safeHybridIdentity(element.id, credentials) || safeHybridIdentity(element.name, credentials)));
|
|
75100
75109
|
const descriptors = elements.map((element) => {
|
|
75101
75110
|
const editable = element.editable === true || /^(?:input|textarea|select)$/i.test(element.tag) || /(?:textbox|searchbox|combobox)/i.test(element.role);
|
|
75102
75111
|
const id = safeHybridIdentity(element.id, credentials)?.toLowerCase() ?? "";
|
|
75103
75112
|
const name = safeHybridIdentity(element.name, credentials)?.toLowerCase() ?? "";
|
|
75104
|
-
const semanticText = editable || id || name ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75113
|
+
const semanticText = editable || id || name || hasStableControlIdentity ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75105
75114
|
return [
|
|
75106
75115
|
element.tag.trim().toLowerCase(),
|
|
75107
75116
|
element.role.trim().toLowerCase(),
|
|
@@ -83740,6 +83749,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83740
83749
|
return actions.filter((_, index) => !toDrop.has(index));
|
|
83741
83750
|
}
|
|
83742
83751
|
var MAX_DETOUR_LENGTH = 6;
|
|
83752
|
+
function isDetourClosingAction(action) {
|
|
83753
|
+
if (action.type === "BACK" || action.type === "HOME")
|
|
83754
|
+
return true;
|
|
83755
|
+
return isNavigationChromeTap(action);
|
|
83756
|
+
}
|
|
83757
|
+
function isEvidenceAction(action) {
|
|
83758
|
+
return action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT" || action.type === "WAIT_FOR_ELEMENT";
|
|
83759
|
+
}
|
|
83743
83760
|
function elideNoOpDetours(actions) {
|
|
83744
83761
|
if (actions.length < 2)
|
|
83745
83762
|
return actions;
|
|
@@ -83747,12 +83764,16 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83747
83764
|
for (let j = 0; j < actions.length; j++) {
|
|
83748
83765
|
if (toDrop.has(j))
|
|
83749
83766
|
continue;
|
|
83750
|
-
if (actions[j]
|
|
83767
|
+
if (!isDetourClosingAction(actions[j]))
|
|
83751
83768
|
continue;
|
|
83752
83769
|
const keyAfter = actions[j].metadata.structuralKey;
|
|
83753
83770
|
if (!keyAfter)
|
|
83754
83771
|
continue;
|
|
83772
|
+
const next = actions[j + 1];
|
|
83773
|
+
if (next && isEvidenceAction(next))
|
|
83774
|
+
continue;
|
|
83755
83775
|
let bestStart = null;
|
|
83776
|
+
let leftScreen = false;
|
|
83756
83777
|
const earliestStart = Math.max(0, j - MAX_DETOUR_LENGTH + 1);
|
|
83757
83778
|
for (let i = j; i >= earliestStart; i--) {
|
|
83758
83779
|
if (i < j) {
|
|
@@ -83760,11 +83781,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83760
83781
|
if (inside.type === "ASSERT_VISIBLE" || inside.type === "ASSERT_TEXT" || isDurableMutation(inside)) {
|
|
83761
83782
|
break;
|
|
83762
83783
|
}
|
|
83784
|
+
const insideKey = inside.metadata.structuralKey;
|
|
83785
|
+
if (insideKey && insideKey !== keyAfter)
|
|
83786
|
+
leftScreen = true;
|
|
83763
83787
|
}
|
|
83764
83788
|
if (i === 0)
|
|
83765
83789
|
continue;
|
|
83766
83790
|
const keyBefore = actions[i - 1].metadata.structuralKey;
|
|
83767
|
-
if (i < j && keyBefore && keyBefore === keyAfter) {
|
|
83791
|
+
if (i < j && leftScreen && keyBefore && keyBefore === keyAfter) {
|
|
83768
83792
|
bestStart = i;
|
|
83769
83793
|
}
|
|
83770
83794
|
}
|
|
@@ -352990,7 +353014,7 @@ var require_package4 = __commonJS({
|
|
|
352990
353014
|
"package.json"(exports2, module2) {
|
|
352991
353015
|
module2.exports = {
|
|
352992
353016
|
name: "@validate.qa/runner",
|
|
352993
|
-
version: "1.0.
|
|
353017
|
+
version: "1.0.73",
|
|
352994
353018
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
352995
353019
|
bin: {
|
|
352996
353020
|
"validate-runner": "dist/cli.js"
|
|
@@ -354654,6 +354678,27 @@ async function clickCssInCurrentContext(sessionPath, css) {
|
|
|
354654
354678
|
body: "{}"
|
|
354655
354679
|
});
|
|
354656
354680
|
}
|
|
354681
|
+
async function clickByVisibleTextInCurrentContext(sessionPath, text) {
|
|
354682
|
+
const script = `
|
|
354683
|
+
var t = String(arguments[0]).trim().replace(/\\s+/g, ' ');
|
|
354684
|
+
var sel = 'button, a, [role="tab"], [role="button"], [onclick], .stream-tab, .ux-interactive, input[type="submit"], input[type="button"]';
|
|
354685
|
+
var norm = function (e) { return (e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
|
|
354686
|
+
var vis = function (e) { var r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
354687
|
+
var all = Array.prototype.slice.call(document.querySelectorAll(sel));
|
|
354688
|
+
var match = all.filter(function (e) { return norm(e) === t && vis(e); });
|
|
354689
|
+
if (match.length !== 1) { return match.length; }
|
|
354690
|
+
match[0].click();
|
|
354691
|
+
return 1;
|
|
354692
|
+
`;
|
|
354693
|
+
const result = await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
354694
|
+
method: "POST",
|
|
354695
|
+
body: JSON.stringify({ script, args: [text] })
|
|
354696
|
+
});
|
|
354697
|
+
const count = readValue(result);
|
|
354698
|
+
if (count !== 1) {
|
|
354699
|
+
throw new Error(`WEBVIEW text click found ${count === 0 || count === void 0 ? "no" : String(count)} actionable match(es) for "${text}"`);
|
|
354700
|
+
}
|
|
354701
|
+
}
|
|
354657
354702
|
async function typeCssInCurrentContext(sessionPath, css, text) {
|
|
354658
354703
|
const elResult = await appiumRequest(`${sessionPath}/element`, {
|
|
354659
354704
|
method: "POST",
|
|
@@ -354802,7 +354847,11 @@ async function pressHome(sessionPath, platform3) {
|
|
|
354802
354847
|
}
|
|
354803
354848
|
|
|
354804
354849
|
// src/services/device-quarantine.ts
|
|
354805
|
-
var
|
|
354850
|
+
var QUARANTINE_MAX_MS = (() => {
|
|
354851
|
+
const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
|
|
354852
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
|
|
354853
|
+
})();
|
|
354854
|
+
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
354806
354855
|
var DeviceQuarantinedError = class extends Error {
|
|
354807
354856
|
constructor(deviceId) {
|
|
354808
354857
|
super(
|
|
@@ -354811,17 +354860,27 @@ var DeviceQuarantinedError = class extends Error {
|
|
|
354811
354860
|
this.name = "DeviceQuarantinedError";
|
|
354812
354861
|
}
|
|
354813
354862
|
};
|
|
354814
|
-
function quarantineDevice(deviceId) {
|
|
354815
|
-
|
|
354863
|
+
function quarantineDevice(deviceId, now = Date.now()) {
|
|
354864
|
+
quarantinedAt.set(deviceId, now);
|
|
354816
354865
|
}
|
|
354817
354866
|
function unquarantineDevice(deviceId) {
|
|
354818
|
-
|
|
354867
|
+
quarantinedAt.delete(deviceId);
|
|
354819
354868
|
}
|
|
354820
|
-
function isDeviceQuarantined(deviceId) {
|
|
354821
|
-
|
|
354869
|
+
function isDeviceQuarantined(deviceId, now = Date.now()) {
|
|
354870
|
+
if (typeof deviceId !== "string") return false;
|
|
354871
|
+
const since = quarantinedAt.get(deviceId);
|
|
354872
|
+
if (since === void 0) return false;
|
|
354873
|
+
if (now - since >= QUARANTINE_MAX_MS) {
|
|
354874
|
+
quarantinedAt.delete(deviceId);
|
|
354875
|
+
console.warn(
|
|
354876
|
+
`[device-quarantine] Quarantine on ${deviceId} exceeded its ${Math.round(QUARANTINE_MAX_MS / 6e4)}min ceiling without the original run settling \u2014 lifting it so the device is usable again. If this recurs, the zombie run is wedged in non-device work (check the runner log).`
|
|
354877
|
+
);
|
|
354878
|
+
return false;
|
|
354879
|
+
}
|
|
354880
|
+
return true;
|
|
354822
354881
|
}
|
|
354823
|
-
function assertDeviceNotQuarantined(deviceId) {
|
|
354824
|
-
if (isDeviceQuarantined(deviceId)) {
|
|
354882
|
+
function assertDeviceNotQuarantined(deviceId, now = Date.now()) {
|
|
354883
|
+
if (isDeviceQuarantined(deviceId, now)) {
|
|
354825
354884
|
throw new DeviceQuarantinedError(deviceId);
|
|
354826
354885
|
}
|
|
354827
354886
|
}
|
|
@@ -356998,6 +357057,48 @@ function stepWebViewContext(step) {
|
|
|
356998
357057
|
const meta = step.metadata && typeof step.metadata === "object" ? step.metadata : null;
|
|
356999
357058
|
return typeof meta?.webviewContext === "string" && meta.webviewContext.trim() ? meta.webviewContext.trim() : null;
|
|
357000
357059
|
}
|
|
357060
|
+
var DOM_ID_RE = /^[A-Za-z_][\w-]*$/;
|
|
357061
|
+
var SYSTEM_RESOURCE_ID_RE = /^(?:android|com\.android\.|com\.google\.android\.)/i;
|
|
357062
|
+
function webViewCssFallbackFromNativeStep(step) {
|
|
357063
|
+
if (stepWebViewCss(step)) return null;
|
|
357064
|
+
const target = step.target;
|
|
357065
|
+
if (!target) return null;
|
|
357066
|
+
const bundle = target.locatorBundle;
|
|
357067
|
+
const resourceId = typeof bundle?.resourceId === "string" ? bundle.resourceId.trim() : "";
|
|
357068
|
+
const accessibilityId = typeof bundle?.accessibilityId === "string" ? bundle.accessibilityId.trim() : "";
|
|
357069
|
+
const primary = typeof target.primary === "string" ? target.primary.trim() : "";
|
|
357070
|
+
const fromResourceId = (raw) => {
|
|
357071
|
+
if (!raw) return null;
|
|
357072
|
+
if (raw.includes(":id/")) {
|
|
357073
|
+
if (SYSTEM_RESOURCE_ID_RE.test(raw)) return null;
|
|
357074
|
+
const local = raw.slice(raw.indexOf(":id/") + 4);
|
|
357075
|
+
return DOM_ID_RE.test(local) ? local : null;
|
|
357076
|
+
}
|
|
357077
|
+
return DOM_ID_RE.test(raw) ? raw : null;
|
|
357078
|
+
};
|
|
357079
|
+
const domId = fromResourceId(resourceId) ?? fromResourceId(accessibilityId) ?? (primary && !/[=~/[\].\s]/.test(primary) && DOM_ID_RE.test(primary) ? primary : null);
|
|
357080
|
+
return domId ? `#${domId}` : null;
|
|
357081
|
+
}
|
|
357082
|
+
function webViewTextFallbackFromNativeStep(step) {
|
|
357083
|
+
if (stepWebViewCss(step)) return null;
|
|
357084
|
+
if (step.action !== "tap") return null;
|
|
357085
|
+
const target = step.target;
|
|
357086
|
+
if (!target) return null;
|
|
357087
|
+
const bundle = target.locatorBundle;
|
|
357088
|
+
const text = typeof bundle?.text === "string" && bundle.text.trim() ? bundle.text.trim() : typeof bundle?.contentDesc === "string" && bundle.contentDesc.trim() ? bundle.contentDesc.trim() : typeof target.primary === "string" && target.primary.trim() && !/[=~/[\]]|:id\//.test(target.primary) ? target.primary.trim() : "";
|
|
357089
|
+
const normalized = text.replace(/\s+/g, " ");
|
|
357090
|
+
if (!normalized || normalized.length > 40 || /[\n\r]/.test(text)) return null;
|
|
357091
|
+
if (SYSTEM_PERMISSION_BUTTON_RE.test(text)) return null;
|
|
357092
|
+
return normalized;
|
|
357093
|
+
}
|
|
357094
|
+
async function hasLiveWebViewContext(sessionId) {
|
|
357095
|
+
try {
|
|
357096
|
+
const contexts = await listAppContexts(`/session/${sessionId}`);
|
|
357097
|
+
return contexts.some((c) => /^WEBVIEW/i.test(c));
|
|
357098
|
+
} catch {
|
|
357099
|
+
return false;
|
|
357100
|
+
}
|
|
357101
|
+
}
|
|
357001
357102
|
var CONTAINER_ONLY_ENTRY_ID_RE = /(?:^|[:/])id\/(webview|content|decor|decor_content_parent|action_bar_root|statusBarBackground|navigationBarBackground)$|^(?:main-content|root|app|app-root|__next|react-root|application)$/i;
|
|
357002
357103
|
function isContainerOnlyEntryTarget(target) {
|
|
357003
357104
|
const primary = target?.primary?.trim() ?? "";
|
|
@@ -357085,6 +357186,7 @@ function isWebViewHydrationTransient(err) {
|
|
|
357085
357186
|
return /no such element|NoSuchElement|element not interactable|stale element|WEBVIEW step requires|Empty page dictionary|not connectable|no WEBVIEW context|chrome not reachable/i.test(message);
|
|
357086
357187
|
}
|
|
357087
357188
|
var WEBVIEW_HYDRATION_TIMEOUT_MS = Number(process.env.VALIDATEQA_WEBVIEW_HYDRATION_TIMEOUT_MS) || 3e4;
|
|
357189
|
+
var PERMISSION_DIALOG_TIMEOUT_MS = Number(process.env.VALIDATEQA_PERMISSION_DIALOG_TIMEOUT_MS) || 3e3;
|
|
357088
357190
|
async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, body, timeoutMs) {
|
|
357089
357191
|
const deadline = Date.now() + Math.max(1e3, timeoutMs);
|
|
357090
357192
|
for (; ; ) {
|
|
@@ -357097,6 +357199,17 @@ async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, b
|
|
|
357097
357199
|
}
|
|
357098
357200
|
}
|
|
357099
357201
|
}
|
|
357202
|
+
var SYSTEM_PERMISSION_BUTTON_RE = /com\.android\.permissioncontroller:id\/permission_(allow|deny)|com\.android\.packageinstaller:id\/permission_|:id\/permission_allow_(foreground_only|one_time)_button|^(?:com\.apple\.springboard|SpringBoard)/i;
|
|
357203
|
+
function isSystemPermissionButtonTarget(target) {
|
|
357204
|
+
if (!target) return false;
|
|
357205
|
+
const bundle = target.locatorBundle;
|
|
357206
|
+
const candidates = [
|
|
357207
|
+
target.primary,
|
|
357208
|
+
typeof bundle?.resourceId === "string" ? bundle.resourceId : "",
|
|
357209
|
+
typeof bundle?.xpath === "string" ? bundle.xpath : ""
|
|
357210
|
+
];
|
|
357211
|
+
return candidates.some((c) => typeof c === "string" && SYSTEM_PERMISSION_BUTTON_RE.test(c));
|
|
357212
|
+
}
|
|
357100
357213
|
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
357101
357214
|
const webCss = stepWebViewCss(step);
|
|
357102
357215
|
if (webCss) {
|
|
@@ -357109,10 +357222,11 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
|
357109
357222
|
);
|
|
357110
357223
|
return;
|
|
357111
357224
|
}
|
|
357225
|
+
const isPermissionTap = isSystemPermissionButtonTarget(step.target);
|
|
357112
357226
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
357113
357227
|
if (step.target) {
|
|
357114
357228
|
try {
|
|
357115
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357229
|
+
const elementId = await findElement(sessionId, step.target, isPermissionTap ? Math.min(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, PERMISSION_DIALOG_TIMEOUT_MS) : readStepTimeout(step), {
|
|
357116
357230
|
platform: platform3,
|
|
357117
357231
|
recordedBounds: bounds
|
|
357118
357232
|
});
|
|
@@ -357122,7 +357236,23 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
|
357122
357236
|
});
|
|
357123
357237
|
return;
|
|
357124
357238
|
} catch (error2) {
|
|
357125
|
-
if (isTransportError(error2) ||
|
|
357239
|
+
if (isTransportError(error2) || error2 instanceof MobileAmbiguousMatchError) throw error2;
|
|
357240
|
+
if (isPermissionTap) return;
|
|
357241
|
+
const cssFallback = webViewCssFallbackFromNativeStep(step);
|
|
357242
|
+
const textFallback = cssFallback ? null : webViewTextFallbackFromNativeStep(step);
|
|
357243
|
+
if ((cssFallback || textFallback) && await hasLiveWebViewContext(sessionId)) {
|
|
357244
|
+
try {
|
|
357245
|
+
if (cssFallback) {
|
|
357246
|
+
await executeInWebViewContext(sessionId, step, cssFallback, (sessionPath, css) => clickCssInCurrentContext(sessionPath, css));
|
|
357247
|
+
} else if (textFallback) {
|
|
357248
|
+
await executeInWebViewContext(sessionId, step, textFallback, (sessionPath) => clickByVisibleTextInCurrentContext(sessionPath, textFallback));
|
|
357249
|
+
}
|
|
357250
|
+
return;
|
|
357251
|
+
} catch (webErr) {
|
|
357252
|
+
if (isTransportError(webErr)) throw webErr;
|
|
357253
|
+
}
|
|
357254
|
+
}
|
|
357255
|
+
if (!bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
357126
357256
|
}
|
|
357127
357257
|
}
|
|
357128
357258
|
if (!bounds) {
|
|
@@ -357180,6 +357310,18 @@ async function executeType(sessionId, step, context) {
|
|
|
357180
357310
|
return;
|
|
357181
357311
|
} catch (error2) {
|
|
357182
357312
|
if (isTransportError(error2)) throw error2;
|
|
357313
|
+
if (!focusEstablished && !keyboardSubmitOnly) {
|
|
357314
|
+
const cssFallback = webViewCssFallbackFromNativeStep(step);
|
|
357315
|
+
if (cssFallback && await hasLiveWebViewContext(sessionId)) {
|
|
357316
|
+
try {
|
|
357317
|
+
context.log?.(` Native locate failed; retrying as WebView ${cssFallback} (hybrid mirror fallback)`);
|
|
357318
|
+
await executeInWebViewContext(sessionId, step, cssFallback, (sessionPath2, css) => typeCssInCurrentContext(sessionPath2, css, value));
|
|
357319
|
+
return;
|
|
357320
|
+
} catch (webErr) {
|
|
357321
|
+
if (isTransportError(webErr)) throw webErr;
|
|
357322
|
+
}
|
|
357323
|
+
}
|
|
357324
|
+
}
|
|
357183
357325
|
if (context.platform === "ANDROID" && focusEstablished) {
|
|
357184
357326
|
try {
|
|
357185
357327
|
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
@@ -358158,6 +358300,86 @@ function validateRunnerMobileTarget(value) {
|
|
|
358158
358300
|
};
|
|
358159
358301
|
}
|
|
358160
358302
|
|
|
358303
|
+
// src/services/heartbeat-keepalive.ts
|
|
358304
|
+
var import_node_worker_threads = require("worker_threads");
|
|
358305
|
+
var KEEPALIVE_INTERVAL_MS = (() => {
|
|
358306
|
+
const raw = Number(process.env.VALIDATEQA_HEARTBEAT_KEEPALIVE_MS);
|
|
358307
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 2e4;
|
|
358308
|
+
})();
|
|
358309
|
+
function isHeartbeatKeepaliveEnabled(env = process.env) {
|
|
358310
|
+
return (env.VALIDATEQA_HEARTBEAT_KEEPALIVE ?? "").trim().toLowerCase() !== "off";
|
|
358311
|
+
}
|
|
358312
|
+
var WORKER_SOURCE = `
|
|
358313
|
+
const { parentPort } = require('node:worker_threads');
|
|
358314
|
+
let url = null;
|
|
358315
|
+
let headers = null;
|
|
358316
|
+
let body = null;
|
|
358317
|
+
let intervalMs = 20000;
|
|
358318
|
+
let timer = null;
|
|
358319
|
+
|
|
358320
|
+
async function post() {
|
|
358321
|
+
if (!url || !headers || !body) return; // inert until the first real heartbeat
|
|
358322
|
+
try {
|
|
358323
|
+
await fetch(url, {
|
|
358324
|
+
method: 'POST',
|
|
358325
|
+
headers,
|
|
358326
|
+
body,
|
|
358327
|
+
signal: AbortSignal.timeout(10000),
|
|
358328
|
+
});
|
|
358329
|
+
} catch {
|
|
358330
|
+
// Best-effort by design: the next tick retries; the main loop's own
|
|
358331
|
+
// heartbeat error handling owns logging/upgrade signals.
|
|
358332
|
+
}
|
|
358333
|
+
}
|
|
358334
|
+
|
|
358335
|
+
parentPort.on('message', (msg) => {
|
|
358336
|
+
if (!msg || typeof msg !== 'object') return;
|
|
358337
|
+
if (msg.type === 'update') {
|
|
358338
|
+
url = msg.url;
|
|
358339
|
+
headers = msg.headers;
|
|
358340
|
+
body = msg.body;
|
|
358341
|
+
intervalMs = msg.intervalMs || intervalMs;
|
|
358342
|
+
if (!timer) {
|
|
358343
|
+
timer = setInterval(post, intervalMs);
|
|
358344
|
+
if (timer.unref) timer.unref();
|
|
358345
|
+
}
|
|
358346
|
+
} else if (msg.type === 'stop') {
|
|
358347
|
+
if (timer) clearInterval(timer);
|
|
358348
|
+
timer = null;
|
|
358349
|
+
parentPort.close();
|
|
358350
|
+
}
|
|
358351
|
+
});
|
|
358352
|
+
`;
|
|
358353
|
+
var NOOP_KEEPALIVE = { update: () => {
|
|
358354
|
+
}, stop: () => {
|
|
358355
|
+
} };
|
|
358356
|
+
function startHeartbeatKeepalive(options = {}) {
|
|
358357
|
+
if (!isHeartbeatKeepaliveEnabled(options.env)) return NOOP_KEEPALIVE;
|
|
358358
|
+
const intervalMs = options.intervalMs ?? KEEPALIVE_INTERVAL_MS;
|
|
358359
|
+
let worker;
|
|
358360
|
+
try {
|
|
358361
|
+
worker = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true });
|
|
358362
|
+
worker.unref();
|
|
358363
|
+
worker.on("error", () => {
|
|
358364
|
+
worker = null;
|
|
358365
|
+
});
|
|
358366
|
+
} catch {
|
|
358367
|
+
return NOOP_KEEPALIVE;
|
|
358368
|
+
}
|
|
358369
|
+
return {
|
|
358370
|
+
update(url, headers, body) {
|
|
358371
|
+
worker?.postMessage({ type: "update", url, headers, body, intervalMs });
|
|
358372
|
+
},
|
|
358373
|
+
stop() {
|
|
358374
|
+
try {
|
|
358375
|
+
worker?.postMessage({ type: "stop" });
|
|
358376
|
+
} catch {
|
|
358377
|
+
}
|
|
358378
|
+
worker = null;
|
|
358379
|
+
}
|
|
358380
|
+
};
|
|
358381
|
+
}
|
|
358382
|
+
|
|
358161
358383
|
// src/mobile-device-queue.ts
|
|
358162
358384
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
358163
358385
|
if (!ctx) return false;
|
|
@@ -363762,6 +363984,7 @@ async function startRunner(opts) {
|
|
|
363762
363984
|
}
|
|
363763
363985
|
}
|
|
363764
363986
|
};
|
|
363987
|
+
const heartbeatKeepalive = startHeartbeatKeepalive();
|
|
363765
363988
|
const sendHeartbeat = async () => {
|
|
363766
363989
|
try {
|
|
363767
363990
|
const devices = enableMobile ? getCachedDeviceSnapshot() : [];
|
|
@@ -363776,41 +363999,44 @@ async function startRunner(opts) {
|
|
|
363776
363999
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
363777
364000
|
lastInventoryKinds.clear();
|
|
363778
364001
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
363779
|
-
const
|
|
364002
|
+
const heartbeatUrl = `${serverUrl}/api/runner/heartbeat`;
|
|
364003
|
+
const heartbeatBody = JSON.stringify({
|
|
364004
|
+
runnerName: RUNNER_NAME,
|
|
364005
|
+
runnerVersion: RUNNER_VERSION,
|
|
364006
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
364007
|
+
runsCompleted,
|
|
364008
|
+
activeRuns,
|
|
364009
|
+
maxConcurrent: MAX_CONCURRENT,
|
|
364010
|
+
uptimeMs: Date.now() - startedAt,
|
|
364011
|
+
capabilities,
|
|
364012
|
+
devices,
|
|
364013
|
+
mobileBridge: bridge ? {
|
|
364014
|
+
online: bridge.running,
|
|
364015
|
+
port: bridge.port,
|
|
364016
|
+
tunnelUrl: bridge.tunnelUrl,
|
|
364017
|
+
tunnelToken: bridge.tunnelToken
|
|
364018
|
+
} : void 0,
|
|
364019
|
+
activeByType: Object.fromEntries(activeByType),
|
|
364020
|
+
activeByProject: Object.fromEntries(activeByProject),
|
|
364021
|
+
typeLimits: TYPE_LIMITS,
|
|
364022
|
+
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
364023
|
+
resourceSnapshot: resourceStatus.snapshot,
|
|
364024
|
+
resourceGovernor: resourceStatus.governor,
|
|
364025
|
+
processInventory,
|
|
364026
|
+
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
364027
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
364028
|
+
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
364029
|
+
// Cached & cheap — see getCachedDoctor above.
|
|
364030
|
+
doctor: getCachedDoctor()
|
|
364031
|
+
});
|
|
364032
|
+
const response = await fetch(heartbeatUrl, {
|
|
363780
364033
|
method: "POST",
|
|
363781
364034
|
headers,
|
|
363782
|
-
body:
|
|
363783
|
-
runnerName: RUNNER_NAME,
|
|
363784
|
-
runnerVersion: RUNNER_VERSION,
|
|
363785
|
-
runnerBuildId: RUNNER_BUILD_ID,
|
|
363786
|
-
runsCompleted,
|
|
363787
|
-
activeRuns,
|
|
363788
|
-
maxConcurrent: MAX_CONCURRENT,
|
|
363789
|
-
uptimeMs: Date.now() - startedAt,
|
|
363790
|
-
capabilities,
|
|
363791
|
-
devices,
|
|
363792
|
-
mobileBridge: bridge ? {
|
|
363793
|
-
online: bridge.running,
|
|
363794
|
-
port: bridge.port,
|
|
363795
|
-
tunnelUrl: bridge.tunnelUrl,
|
|
363796
|
-
tunnelToken: bridge.tunnelToken
|
|
363797
|
-
} : void 0,
|
|
363798
|
-
activeByType: Object.fromEntries(activeByType),
|
|
363799
|
-
activeByProject: Object.fromEntries(activeByProject),
|
|
363800
|
-
typeLimits: TYPE_LIMITS,
|
|
363801
|
-
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
363802
|
-
resourceSnapshot: resourceStatus.snapshot,
|
|
363803
|
-
resourceGovernor: resourceStatus.governor,
|
|
363804
|
-
processInventory,
|
|
363805
|
-
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
363806
|
-
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
363807
|
-
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
363808
|
-
// Cached & cheap — see getCachedDoctor above.
|
|
363809
|
-
doctor: getCachedDoctor()
|
|
363810
|
-
}),
|
|
364035
|
+
body: heartbeatBody,
|
|
363811
364036
|
signal: AbortSignal.timeout(1e4)
|
|
363812
364037
|
});
|
|
363813
364038
|
if (response.ok) {
|
|
364039
|
+
heartbeatKeepalive.update(heartbeatUrl, headers, heartbeatBody);
|
|
363814
364040
|
const body = await response.json().catch(() => null);
|
|
363815
364041
|
if (body && Array.isArray(body.commands) && body.commands.length > 0) {
|
|
363816
364042
|
void handleControlCommands(body.commands);
|
|
@@ -363881,6 +364107,7 @@ async function startRunner(opts) {
|
|
|
363881
364107
|
process.exit(1);
|
|
363882
364108
|
}, SHUTDOWN_TIMEOUT_MS).unref();
|
|
363883
364109
|
clearInterval(heartbeat);
|
|
364110
|
+
heartbeatKeepalive.stop();
|
|
363884
364111
|
if (liveBrowserHeartbeatTimer) {
|
|
363885
364112
|
clearTimeout(liveBrowserHeartbeatTimer);
|
|
363886
364113
|
liveBrowserHeartbeatTimer = null;
|
package/dist/cli.mjs
CHANGED
|
@@ -5859,6 +5859,29 @@ function normalizeTtsVoiceId(voiceId) {
|
|
|
5859
5859
|
return null;
|
|
5860
5860
|
return LEGACY_TTS_VOICE_ID_MAP[trimmed] ?? trimmed;
|
|
5861
5861
|
}
|
|
5862
|
+
function normalizeTtsModel(model) {
|
|
5863
|
+
if (!model)
|
|
5864
|
+
return null;
|
|
5865
|
+
const trimmed = model.trim();
|
|
5866
|
+
if (!trimmed)
|
|
5867
|
+
return null;
|
|
5868
|
+
return LEGACY_TTS_MODEL_MAP[trimmed] ?? trimmed;
|
|
5869
|
+
}
|
|
5870
|
+
function ttsFallbackModel(primary) {
|
|
5871
|
+
const normalized = normalizeTtsModel(primary);
|
|
5872
|
+
if (!normalized)
|
|
5873
|
+
return null;
|
|
5874
|
+
return TTS_FALLBACK_MODEL_MAP[normalized] ?? null;
|
|
5875
|
+
}
|
|
5876
|
+
function resolveTtsModelChain(model) {
|
|
5877
|
+
const normalized = normalizeTtsModel(model);
|
|
5878
|
+
const primary = normalized && normalized in TTS_FALLBACK_MODEL_MAP ? normalized : DEFAULT_TTS_MODEL;
|
|
5879
|
+
const chain = [primary];
|
|
5880
|
+
const fallback = TTS_FALLBACK_MODEL_MAP[primary];
|
|
5881
|
+
if (fallback && fallback !== primary)
|
|
5882
|
+
chain.push(fallback);
|
|
5883
|
+
return chain;
|
|
5884
|
+
}
|
|
5862
5885
|
function sttModelsForProvider(provider) {
|
|
5863
5886
|
if (provider === "openai") {
|
|
5864
5887
|
return STT_MODEL_OPTIONS.filter((m) => m.provider === "openai");
|
|
@@ -5906,16 +5929,18 @@ function isValidTtsVoiceId(value) {
|
|
|
5906
5929
|
return TTS_VOICE_OPTIONS.some((v) => v.id === normalized);
|
|
5907
5930
|
}
|
|
5908
5931
|
function isValidTtsModel(value) {
|
|
5909
|
-
|
|
5932
|
+
const normalized = normalizeTtsModel(value);
|
|
5933
|
+
if (!normalized)
|
|
5910
5934
|
return false;
|
|
5911
|
-
return TTS_MODEL_OPTIONS.some((m) => m.id ===
|
|
5935
|
+
return TTS_MODEL_OPTIONS.some((m) => m.id === normalized);
|
|
5912
5936
|
}
|
|
5913
|
-
var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
|
|
5937
|
+
var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_TURBO_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, LEGACY_TTS_MODEL_MAP, TTS_FALLBACK_MODEL_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
|
|
5914
5938
|
var init_voice_config = __esm({
|
|
5915
5939
|
"../shared/dist/voice-config.js"() {
|
|
5916
5940
|
"use strict";
|
|
5917
5941
|
VOICE_PREVIEW_SAMPLE = "Hello! I am your Validate Copilot assistant. I can help you run tests, review failures, and generate new coverage for your project.";
|
|
5918
|
-
DEFAULT_TTS_MODEL = "speech-2.
|
|
5942
|
+
DEFAULT_TTS_MODEL = "speech-2.8-hd";
|
|
5943
|
+
DEFAULT_TTS_TURBO_MODEL = "speech-2.8-turbo";
|
|
5919
5944
|
DEFAULT_TTS_VOICE_ID = "English_Graceful_Lady";
|
|
5920
5945
|
DEFAULT_TTS_SPEED = 1;
|
|
5921
5946
|
LEGACY_TTS_VOICE_ID_MAP = {
|
|
@@ -5924,6 +5949,18 @@ var init_voice_config = __esm({
|
|
|
5924
5949
|
English_Deep_Voice_Man: "English_ManWithDeepVoice",
|
|
5925
5950
|
English_CasualMan: "English_Jovialman"
|
|
5926
5951
|
};
|
|
5952
|
+
LEGACY_TTS_MODEL_MAP = {
|
|
5953
|
+
"speech-2.6-hd": "speech-2.8-hd",
|
|
5954
|
+
"speech-2.6-turbo": "speech-2.8-turbo",
|
|
5955
|
+
"speech-02-hd": "speech-2.8-hd",
|
|
5956
|
+
"speech-02-turbo": "speech-2.8-turbo",
|
|
5957
|
+
"speech-01-hd": "speech-2.8-hd",
|
|
5958
|
+
"speech-01-turbo": "speech-2.8-turbo"
|
|
5959
|
+
};
|
|
5960
|
+
TTS_FALLBACK_MODEL_MAP = {
|
|
5961
|
+
"speech-2.8-hd": "speech-2.6-hd",
|
|
5962
|
+
"speech-2.8-turbo": "speech-2.6-turbo"
|
|
5963
|
+
};
|
|
5927
5964
|
DEFAULT_STT_MODEL_BY_PROVIDER = {
|
|
5928
5965
|
groq: "whisper-large-v3-turbo",
|
|
5929
5966
|
openai: "whisper-1"
|
|
@@ -5976,49 +6013,13 @@ var init_voice_config = __esm({
|
|
|
5976
6013
|
id: "speech-2.8-hd",
|
|
5977
6014
|
label: "Speech 2.8 HD",
|
|
5978
6015
|
provider: "minimax",
|
|
5979
|
-
description: "
|
|
6016
|
+
description: "High-definition quality (default). Auto-falls back to Speech 2.6 HD."
|
|
5980
6017
|
},
|
|
5981
6018
|
{
|
|
5982
6019
|
id: "speech-2.8-turbo",
|
|
5983
6020
|
label: "Speech 2.8 Turbo",
|
|
5984
6021
|
provider: "minimax",
|
|
5985
|
-
description: "
|
|
5986
|
-
},
|
|
5987
|
-
{
|
|
5988
|
-
id: "speech-2.6-hd",
|
|
5989
|
-
label: "Speech 2.6 HD",
|
|
5990
|
-
provider: "minimax",
|
|
5991
|
-
description: "High-definition MiniMax voice model."
|
|
5992
|
-
},
|
|
5993
|
-
{
|
|
5994
|
-
id: "speech-2.6-turbo",
|
|
5995
|
-
label: "Speech 2.6 Turbo",
|
|
5996
|
-
provider: "minimax",
|
|
5997
|
-
description: "Lower-latency Speech 2.6 variant."
|
|
5998
|
-
},
|
|
5999
|
-
{
|
|
6000
|
-
id: "speech-02-hd",
|
|
6001
|
-
label: "Speech 02 HD",
|
|
6002
|
-
provider: "minimax",
|
|
6003
|
-
description: "Previous-generation HD model."
|
|
6004
|
-
},
|
|
6005
|
-
{
|
|
6006
|
-
id: "speech-02-turbo",
|
|
6007
|
-
label: "Speech 02 Turbo",
|
|
6008
|
-
provider: "minimax",
|
|
6009
|
-
description: "Lower-latency Speech 02 variant."
|
|
6010
|
-
},
|
|
6011
|
-
{
|
|
6012
|
-
id: "speech-01-hd",
|
|
6013
|
-
label: "Speech 01 HD",
|
|
6014
|
-
provider: "minimax",
|
|
6015
|
-
description: "Legacy HD model."
|
|
6016
|
-
},
|
|
6017
|
-
{
|
|
6018
|
-
id: "speech-01-turbo",
|
|
6019
|
-
label: "Speech 01 Turbo",
|
|
6020
|
-
provider: "minimax",
|
|
6021
|
-
description: "Legacy fast model."
|
|
6022
|
+
description: "Lower latency. Auto-falls back to Speech 2.6 Turbo."
|
|
6022
6023
|
}
|
|
6023
6024
|
];
|
|
6024
6025
|
TTS_VOICE_OPTIONS = [
|
|
@@ -6302,6 +6303,7 @@ __export(dist_exports, {
|
|
|
6302
6303
|
DEFAULT_STT_MODEL_BY_PROVIDER: () => DEFAULT_STT_MODEL_BY_PROVIDER,
|
|
6303
6304
|
DEFAULT_TTS_MODEL: () => DEFAULT_TTS_MODEL,
|
|
6304
6305
|
DEFAULT_TTS_SPEED: () => DEFAULT_TTS_SPEED,
|
|
6306
|
+
DEFAULT_TTS_TURBO_MODEL: () => DEFAULT_TTS_TURBO_MODEL,
|
|
6305
6307
|
DEFAULT_TTS_VOICE_ID: () => DEFAULT_TTS_VOICE_ID,
|
|
6306
6308
|
DEFAULT_UI_VARIANT: () => DEFAULT_UI_VARIANT,
|
|
6307
6309
|
EMAIL_FROM_IDENTITIES: () => EMAIL_FROM_IDENTITIES,
|
|
@@ -6311,6 +6313,7 @@ __export(dist_exports, {
|
|
|
6311
6313
|
FLOW_SERVER_TYPES: () => FLOW_SERVER_TYPES,
|
|
6312
6314
|
GROUPABLE_CALL_TYPES: () => GROUPABLE_CALL_TYPES,
|
|
6313
6315
|
GROUP_LABELS: () => GROUP_LABELS,
|
|
6316
|
+
LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
|
|
6314
6317
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
6315
6318
|
MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
|
|
6316
6319
|
MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
|
|
@@ -6344,6 +6347,7 @@ __export(dist_exports, {
|
|
|
6344
6347
|
TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
|
|
6345
6348
|
TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
|
|
6346
6349
|
TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
|
|
6350
|
+
TTS_FALLBACK_MODEL_MAP: () => TTS_FALLBACK_MODEL_MAP,
|
|
6347
6351
|
TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
|
|
6348
6352
|
TTS_VOICE_OPTIONS: () => TTS_VOICE_OPTIONS,
|
|
6349
6353
|
UI_VARIANTS: () => UI_VARIANTS,
|
|
@@ -6446,6 +6450,7 @@ __export(dist_exports, {
|
|
|
6446
6450
|
mobilePlatformFromMedium: () => mobilePlatformFromMedium,
|
|
6447
6451
|
mobileWanderTraceEnabled: () => mobileWanderTraceEnabled,
|
|
6448
6452
|
normalizeControlName: () => normalizeControlName,
|
|
6453
|
+
normalizeTtsModel: () => normalizeTtsModel,
|
|
6449
6454
|
normalizeTtsVoiceId: () => normalizeTtsVoiceId,
|
|
6450
6455
|
otherMobilePlatform: () => otherMobilePlatform,
|
|
6451
6456
|
parenDepthDelta: () => parenDepthDelta,
|
|
@@ -6464,6 +6469,7 @@ __export(dist_exports, {
|
|
|
6464
6469
|
resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
|
|
6465
6470
|
resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
|
|
6466
6471
|
resolveSttModelForProvider: () => resolveSttModelForProvider,
|
|
6472
|
+
resolveTtsModelChain: () => resolveTtsModelChain,
|
|
6467
6473
|
safeCurrentUrl: () => safeCurrentUrl,
|
|
6468
6474
|
safeOrigin: () => safeOrigin,
|
|
6469
6475
|
sanitizeSttErrorMessage: () => sanitizeSttErrorMessage,
|
|
@@ -6486,6 +6492,7 @@ __export(dist_exports, {
|
|
|
6486
6492
|
summarizeMobileReadiness: () => summarizeMobileReadiness,
|
|
6487
6493
|
summarizeRunHealth: () => summarizeRunHealth,
|
|
6488
6494
|
toDbPricingPlan: () => toDbPricingPlan,
|
|
6495
|
+
ttsFallbackModel: () => ttsFallbackModel,
|
|
6489
6496
|
validateAssertionStrength: () => validateAssertionStrength
|
|
6490
6497
|
});
|
|
6491
6498
|
var init_dist = __esm({
|
|
@@ -11496,7 +11503,7 @@ var require_thread_stream = __commonJS({
|
|
|
11496
11503
|
"use strict";
|
|
11497
11504
|
var { version } = require_package();
|
|
11498
11505
|
var { EventEmitter: EventEmitter2 } = __require("events");
|
|
11499
|
-
var { Worker } = __require("worker_threads");
|
|
11506
|
+
var { Worker: Worker2 } = __require("worker_threads");
|
|
11500
11507
|
var { join: join8 } = __require("path");
|
|
11501
11508
|
var { pathToFileURL } = __require("url");
|
|
11502
11509
|
var { wait } = require_wait();
|
|
@@ -11549,7 +11556,7 @@ var require_thread_stream = __commonJS({
|
|
|
11549
11556
|
const { filename, workerData } = opts;
|
|
11550
11557
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
11551
11558
|
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
11552
|
-
const worker = new
|
|
11559
|
+
const worker = new Worker2(toExecute, {
|
|
11553
11560
|
...opts.workerOpts,
|
|
11554
11561
|
name: opts.workerOpts?.name || "thread-stream",
|
|
11555
11562
|
trackUnmanagedFds: false,
|
|
@@ -74649,6 +74656,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
74649
74656
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74650
74657
|
exports.computeMobileSurveyBreadthContinuation = computeMobileSurveyBreadthContinuation;
|
|
74651
74658
|
exports.computeMobileBreadthFinishRedirectDecision = computeMobileBreadthFinishRedirectDecision;
|
|
74659
|
+
exports.hybridScreenFingerprint = hybridScreenFingerprint;
|
|
74652
74660
|
exports.runMobileExplorePhase = runMobileExplorePhase;
|
|
74653
74661
|
var crypto_1 = __require("crypto");
|
|
74654
74662
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
@@ -75102,11 +75110,12 @@ var require_mobile_explorer = __commonJS({
|
|
|
75102
75110
|
const validSurfaceDigest = /^[0-9a-f]{16,64}$/.test(normalizedSurfaceDigest) ? normalizedSurfaceDigest : void 0;
|
|
75103
75111
|
const legacyElementDigests = [...new Set(elements.map((element) => element.pageIdentityDigest?.trim().toLowerCase() ?? "").filter((digest) => /^[0-9a-f]{16,64}$/.test(digest)))];
|
|
75104
75112
|
const pageIdentityDigest = validSurfaceDigest ?? (legacyElementDigests.length === 1 ? legacyElementDigests[0] : void 0);
|
|
75113
|
+
const hasStableControlIdentity = elements.some((element) => Boolean(safeHybridIdentity(element.id, credentials) || safeHybridIdentity(element.name, credentials)));
|
|
75105
75114
|
const descriptors = elements.map((element) => {
|
|
75106
75115
|
const editable = element.editable === true || /^(?:input|textarea|select)$/i.test(element.tag) || /(?:textbox|searchbox|combobox)/i.test(element.role);
|
|
75107
75116
|
const id = safeHybridIdentity(element.id, credentials)?.toLowerCase() ?? "";
|
|
75108
75117
|
const name = safeHybridIdentity(element.name, credentials)?.toLowerCase() ?? "";
|
|
75109
|
-
const semanticText = editable || id || name ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75118
|
+
const semanticText = editable || id || name || hasStableControlIdentity ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75110
75119
|
return [
|
|
75111
75120
|
element.tag.trim().toLowerCase(),
|
|
75112
75121
|
element.role.trim().toLowerCase(),
|
|
@@ -83745,6 +83754,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83745
83754
|
return actions.filter((_, index) => !toDrop.has(index));
|
|
83746
83755
|
}
|
|
83747
83756
|
var MAX_DETOUR_LENGTH = 6;
|
|
83757
|
+
function isDetourClosingAction(action) {
|
|
83758
|
+
if (action.type === "BACK" || action.type === "HOME")
|
|
83759
|
+
return true;
|
|
83760
|
+
return isNavigationChromeTap(action);
|
|
83761
|
+
}
|
|
83762
|
+
function isEvidenceAction(action) {
|
|
83763
|
+
return action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT" || action.type === "WAIT_FOR_ELEMENT";
|
|
83764
|
+
}
|
|
83748
83765
|
function elideNoOpDetours(actions) {
|
|
83749
83766
|
if (actions.length < 2)
|
|
83750
83767
|
return actions;
|
|
@@ -83752,12 +83769,16 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83752
83769
|
for (let j = 0; j < actions.length; j++) {
|
|
83753
83770
|
if (toDrop.has(j))
|
|
83754
83771
|
continue;
|
|
83755
|
-
if (actions[j]
|
|
83772
|
+
if (!isDetourClosingAction(actions[j]))
|
|
83756
83773
|
continue;
|
|
83757
83774
|
const keyAfter = actions[j].metadata.structuralKey;
|
|
83758
83775
|
if (!keyAfter)
|
|
83759
83776
|
continue;
|
|
83777
|
+
const next = actions[j + 1];
|
|
83778
|
+
if (next && isEvidenceAction(next))
|
|
83779
|
+
continue;
|
|
83760
83780
|
let bestStart = null;
|
|
83781
|
+
let leftScreen = false;
|
|
83761
83782
|
const earliestStart = Math.max(0, j - MAX_DETOUR_LENGTH + 1);
|
|
83762
83783
|
for (let i = j; i >= earliestStart; i--) {
|
|
83763
83784
|
if (i < j) {
|
|
@@ -83765,11 +83786,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83765
83786
|
if (inside.type === "ASSERT_VISIBLE" || inside.type === "ASSERT_TEXT" || isDurableMutation(inside)) {
|
|
83766
83787
|
break;
|
|
83767
83788
|
}
|
|
83789
|
+
const insideKey = inside.metadata.structuralKey;
|
|
83790
|
+
if (insideKey && insideKey !== keyAfter)
|
|
83791
|
+
leftScreen = true;
|
|
83768
83792
|
}
|
|
83769
83793
|
if (i === 0)
|
|
83770
83794
|
continue;
|
|
83771
83795
|
const keyBefore = actions[i - 1].metadata.structuralKey;
|
|
83772
|
-
if (i < j && keyBefore && keyBefore === keyAfter) {
|
|
83796
|
+
if (i < j && leftScreen && keyBefore && keyBefore === keyAfter) {
|
|
83773
83797
|
bestStart = i;
|
|
83774
83798
|
}
|
|
83775
83799
|
}
|
|
@@ -352995,7 +353019,7 @@ var require_package4 = __commonJS({
|
|
|
352995
353019
|
"package.json"(exports, module) {
|
|
352996
353020
|
module.exports = {
|
|
352997
353021
|
name: "@validate.qa/runner",
|
|
352998
|
-
version: "1.0.
|
|
353022
|
+
version: "1.0.73",
|
|
352999
353023
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353000
353024
|
bin: {
|
|
353001
353025
|
"validate-runner": "dist/cli.js"
|
|
@@ -354659,6 +354683,27 @@ async function clickCssInCurrentContext(sessionPath, css) {
|
|
|
354659
354683
|
body: "{}"
|
|
354660
354684
|
});
|
|
354661
354685
|
}
|
|
354686
|
+
async function clickByVisibleTextInCurrentContext(sessionPath, text) {
|
|
354687
|
+
const script = `
|
|
354688
|
+
var t = String(arguments[0]).trim().replace(/\\s+/g, ' ');
|
|
354689
|
+
var sel = 'button, a, [role="tab"], [role="button"], [onclick], .stream-tab, .ux-interactive, input[type="submit"], input[type="button"]';
|
|
354690
|
+
var norm = function (e) { return (e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
|
|
354691
|
+
var vis = function (e) { var r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
354692
|
+
var all = Array.prototype.slice.call(document.querySelectorAll(sel));
|
|
354693
|
+
var match = all.filter(function (e) { return norm(e) === t && vis(e); });
|
|
354694
|
+
if (match.length !== 1) { return match.length; }
|
|
354695
|
+
match[0].click();
|
|
354696
|
+
return 1;
|
|
354697
|
+
`;
|
|
354698
|
+
const result = await appiumRequest(`${sessionPath}/execute/sync`, {
|
|
354699
|
+
method: "POST",
|
|
354700
|
+
body: JSON.stringify({ script, args: [text] })
|
|
354701
|
+
});
|
|
354702
|
+
const count = readValue(result);
|
|
354703
|
+
if (count !== 1) {
|
|
354704
|
+
throw new Error(`WEBVIEW text click found ${count === 0 || count === void 0 ? "no" : String(count)} actionable match(es) for "${text}"`);
|
|
354705
|
+
}
|
|
354706
|
+
}
|
|
354662
354707
|
async function typeCssInCurrentContext(sessionPath, css, text) {
|
|
354663
354708
|
const elResult = await appiumRequest(`${sessionPath}/element`, {
|
|
354664
354709
|
method: "POST",
|
|
@@ -354807,7 +354852,11 @@ async function pressHome(sessionPath, platform3) {
|
|
|
354807
354852
|
}
|
|
354808
354853
|
|
|
354809
354854
|
// src/services/device-quarantine.ts
|
|
354810
|
-
var
|
|
354855
|
+
var QUARANTINE_MAX_MS = (() => {
|
|
354856
|
+
const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
|
|
354857
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
|
|
354858
|
+
})();
|
|
354859
|
+
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
354811
354860
|
var DeviceQuarantinedError = class extends Error {
|
|
354812
354861
|
constructor(deviceId) {
|
|
354813
354862
|
super(
|
|
@@ -354816,17 +354865,27 @@ var DeviceQuarantinedError = class extends Error {
|
|
|
354816
354865
|
this.name = "DeviceQuarantinedError";
|
|
354817
354866
|
}
|
|
354818
354867
|
};
|
|
354819
|
-
function quarantineDevice(deviceId) {
|
|
354820
|
-
|
|
354868
|
+
function quarantineDevice(deviceId, now = Date.now()) {
|
|
354869
|
+
quarantinedAt.set(deviceId, now);
|
|
354821
354870
|
}
|
|
354822
354871
|
function unquarantineDevice(deviceId) {
|
|
354823
|
-
|
|
354872
|
+
quarantinedAt.delete(deviceId);
|
|
354824
354873
|
}
|
|
354825
|
-
function isDeviceQuarantined(deviceId) {
|
|
354826
|
-
|
|
354874
|
+
function isDeviceQuarantined(deviceId, now = Date.now()) {
|
|
354875
|
+
if (typeof deviceId !== "string") return false;
|
|
354876
|
+
const since = quarantinedAt.get(deviceId);
|
|
354877
|
+
if (since === void 0) return false;
|
|
354878
|
+
if (now - since >= QUARANTINE_MAX_MS) {
|
|
354879
|
+
quarantinedAt.delete(deviceId);
|
|
354880
|
+
console.warn(
|
|
354881
|
+
`[device-quarantine] Quarantine on ${deviceId} exceeded its ${Math.round(QUARANTINE_MAX_MS / 6e4)}min ceiling without the original run settling \u2014 lifting it so the device is usable again. If this recurs, the zombie run is wedged in non-device work (check the runner log).`
|
|
354882
|
+
);
|
|
354883
|
+
return false;
|
|
354884
|
+
}
|
|
354885
|
+
return true;
|
|
354827
354886
|
}
|
|
354828
|
-
function assertDeviceNotQuarantined(deviceId) {
|
|
354829
|
-
if (isDeviceQuarantined(deviceId)) {
|
|
354887
|
+
function assertDeviceNotQuarantined(deviceId, now = Date.now()) {
|
|
354888
|
+
if (isDeviceQuarantined(deviceId, now)) {
|
|
354830
354889
|
throw new DeviceQuarantinedError(deviceId);
|
|
354831
354890
|
}
|
|
354832
354891
|
}
|
|
@@ -357006,6 +357065,48 @@ function stepWebViewContext(step) {
|
|
|
357006
357065
|
const meta = step.metadata && typeof step.metadata === "object" ? step.metadata : null;
|
|
357007
357066
|
return typeof meta?.webviewContext === "string" && meta.webviewContext.trim() ? meta.webviewContext.trim() : null;
|
|
357008
357067
|
}
|
|
357068
|
+
var DOM_ID_RE = /^[A-Za-z_][\w-]*$/;
|
|
357069
|
+
var SYSTEM_RESOURCE_ID_RE = /^(?:android|com\.android\.|com\.google\.android\.)/i;
|
|
357070
|
+
function webViewCssFallbackFromNativeStep(step) {
|
|
357071
|
+
if (stepWebViewCss(step)) return null;
|
|
357072
|
+
const target = step.target;
|
|
357073
|
+
if (!target) return null;
|
|
357074
|
+
const bundle = target.locatorBundle;
|
|
357075
|
+
const resourceId = typeof bundle?.resourceId === "string" ? bundle.resourceId.trim() : "";
|
|
357076
|
+
const accessibilityId = typeof bundle?.accessibilityId === "string" ? bundle.accessibilityId.trim() : "";
|
|
357077
|
+
const primary = typeof target.primary === "string" ? target.primary.trim() : "";
|
|
357078
|
+
const fromResourceId = (raw) => {
|
|
357079
|
+
if (!raw) return null;
|
|
357080
|
+
if (raw.includes(":id/")) {
|
|
357081
|
+
if (SYSTEM_RESOURCE_ID_RE.test(raw)) return null;
|
|
357082
|
+
const local = raw.slice(raw.indexOf(":id/") + 4);
|
|
357083
|
+
return DOM_ID_RE.test(local) ? local : null;
|
|
357084
|
+
}
|
|
357085
|
+
return DOM_ID_RE.test(raw) ? raw : null;
|
|
357086
|
+
};
|
|
357087
|
+
const domId = fromResourceId(resourceId) ?? fromResourceId(accessibilityId) ?? (primary && !/[=~/[\].\s]/.test(primary) && DOM_ID_RE.test(primary) ? primary : null);
|
|
357088
|
+
return domId ? `#${domId}` : null;
|
|
357089
|
+
}
|
|
357090
|
+
function webViewTextFallbackFromNativeStep(step) {
|
|
357091
|
+
if (stepWebViewCss(step)) return null;
|
|
357092
|
+
if (step.action !== "tap") return null;
|
|
357093
|
+
const target = step.target;
|
|
357094
|
+
if (!target) return null;
|
|
357095
|
+
const bundle = target.locatorBundle;
|
|
357096
|
+
const text = typeof bundle?.text === "string" && bundle.text.trim() ? bundle.text.trim() : typeof bundle?.contentDesc === "string" && bundle.contentDesc.trim() ? bundle.contentDesc.trim() : typeof target.primary === "string" && target.primary.trim() && !/[=~/[\]]|:id\//.test(target.primary) ? target.primary.trim() : "";
|
|
357097
|
+
const normalized = text.replace(/\s+/g, " ");
|
|
357098
|
+
if (!normalized || normalized.length > 40 || /[\n\r]/.test(text)) return null;
|
|
357099
|
+
if (SYSTEM_PERMISSION_BUTTON_RE.test(text)) return null;
|
|
357100
|
+
return normalized;
|
|
357101
|
+
}
|
|
357102
|
+
async function hasLiveWebViewContext(sessionId) {
|
|
357103
|
+
try {
|
|
357104
|
+
const contexts = await listAppContexts(`/session/${sessionId}`);
|
|
357105
|
+
return contexts.some((c) => /^WEBVIEW/i.test(c));
|
|
357106
|
+
} catch {
|
|
357107
|
+
return false;
|
|
357108
|
+
}
|
|
357109
|
+
}
|
|
357009
357110
|
var CONTAINER_ONLY_ENTRY_ID_RE = /(?:^|[:/])id\/(webview|content|decor|decor_content_parent|action_bar_root|statusBarBackground|navigationBarBackground)$|^(?:main-content|root|app|app-root|__next|react-root|application)$/i;
|
|
357010
357111
|
function isContainerOnlyEntryTarget(target) {
|
|
357011
357112
|
const primary = target?.primary?.trim() ?? "";
|
|
@@ -357093,6 +357194,7 @@ function isWebViewHydrationTransient(err) {
|
|
|
357093
357194
|
return /no such element|NoSuchElement|element not interactable|stale element|WEBVIEW step requires|Empty page dictionary|not connectable|no WEBVIEW context|chrome not reachable/i.test(message);
|
|
357094
357195
|
}
|
|
357095
357196
|
var WEBVIEW_HYDRATION_TIMEOUT_MS = Number(process.env.VALIDATEQA_WEBVIEW_HYDRATION_TIMEOUT_MS) || 3e4;
|
|
357197
|
+
var PERMISSION_DIALOG_TIMEOUT_MS = Number(process.env.VALIDATEQA_PERMISSION_DIALOG_TIMEOUT_MS) || 3e3;
|
|
357096
357198
|
async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, body, timeoutMs) {
|
|
357097
357199
|
const deadline = Date.now() + Math.max(1e3, timeoutMs);
|
|
357098
357200
|
for (; ; ) {
|
|
@@ -357105,6 +357207,17 @@ async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, b
|
|
|
357105
357207
|
}
|
|
357106
357208
|
}
|
|
357107
357209
|
}
|
|
357210
|
+
var SYSTEM_PERMISSION_BUTTON_RE = /com\.android\.permissioncontroller:id\/permission_(allow|deny)|com\.android\.packageinstaller:id\/permission_|:id\/permission_allow_(foreground_only|one_time)_button|^(?:com\.apple\.springboard|SpringBoard)/i;
|
|
357211
|
+
function isSystemPermissionButtonTarget(target) {
|
|
357212
|
+
if (!target) return false;
|
|
357213
|
+
const bundle = target.locatorBundle;
|
|
357214
|
+
const candidates = [
|
|
357215
|
+
target.primary,
|
|
357216
|
+
typeof bundle?.resourceId === "string" ? bundle.resourceId : "",
|
|
357217
|
+
typeof bundle?.xpath === "string" ? bundle.xpath : ""
|
|
357218
|
+
];
|
|
357219
|
+
return candidates.some((c) => typeof c === "string" && SYSTEM_PERMISSION_BUTTON_RE.test(c));
|
|
357220
|
+
}
|
|
357108
357221
|
async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
357109
357222
|
const webCss = stepWebViewCss(step);
|
|
357110
357223
|
if (webCss) {
|
|
@@ -357117,10 +357230,11 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
|
357117
357230
|
);
|
|
357118
357231
|
return;
|
|
357119
357232
|
}
|
|
357233
|
+
const isPermissionTap = isSystemPermissionButtonTarget(step.target);
|
|
357120
357234
|
const bounds = boundsForRun(step, platform3, isCrossPlatform);
|
|
357121
357235
|
if (step.target) {
|
|
357122
357236
|
try {
|
|
357123
|
-
const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
|
|
357237
|
+
const elementId = await findElement(sessionId, step.target, isPermissionTap ? Math.min(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, PERMISSION_DIALOG_TIMEOUT_MS) : readStepTimeout(step), {
|
|
357124
357238
|
platform: platform3,
|
|
357125
357239
|
recordedBounds: bounds
|
|
357126
357240
|
});
|
|
@@ -357130,7 +357244,23 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
|
|
|
357130
357244
|
});
|
|
357131
357245
|
return;
|
|
357132
357246
|
} catch (error2) {
|
|
357133
|
-
if (isTransportError(error2) ||
|
|
357247
|
+
if (isTransportError(error2) || error2 instanceof MobileAmbiguousMatchError) throw error2;
|
|
357248
|
+
if (isPermissionTap) return;
|
|
357249
|
+
const cssFallback = webViewCssFallbackFromNativeStep(step);
|
|
357250
|
+
const textFallback = cssFallback ? null : webViewTextFallbackFromNativeStep(step);
|
|
357251
|
+
if ((cssFallback || textFallback) && await hasLiveWebViewContext(sessionId)) {
|
|
357252
|
+
try {
|
|
357253
|
+
if (cssFallback) {
|
|
357254
|
+
await executeInWebViewContext(sessionId, step, cssFallback, (sessionPath, css) => clickCssInCurrentContext(sessionPath, css));
|
|
357255
|
+
} else if (textFallback) {
|
|
357256
|
+
await executeInWebViewContext(sessionId, step, textFallback, (sessionPath) => clickByVisibleTextInCurrentContext(sessionPath, textFallback));
|
|
357257
|
+
}
|
|
357258
|
+
return;
|
|
357259
|
+
} catch (webErr) {
|
|
357260
|
+
if (isTransportError(webErr)) throw webErr;
|
|
357261
|
+
}
|
|
357262
|
+
}
|
|
357263
|
+
if (!bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
|
|
357134
357264
|
}
|
|
357135
357265
|
}
|
|
357136
357266
|
if (!bounds) {
|
|
@@ -357188,6 +357318,18 @@ async function executeType(sessionId, step, context) {
|
|
|
357188
357318
|
return;
|
|
357189
357319
|
} catch (error2) {
|
|
357190
357320
|
if (isTransportError(error2)) throw error2;
|
|
357321
|
+
if (!focusEstablished && !keyboardSubmitOnly) {
|
|
357322
|
+
const cssFallback = webViewCssFallbackFromNativeStep(step);
|
|
357323
|
+
if (cssFallback && await hasLiveWebViewContext(sessionId)) {
|
|
357324
|
+
try {
|
|
357325
|
+
context.log?.(` Native locate failed; retrying as WebView ${cssFallback} (hybrid mirror fallback)`);
|
|
357326
|
+
await executeInWebViewContext(sessionId, step, cssFallback, (sessionPath2, css) => typeCssInCurrentContext(sessionPath2, css, value));
|
|
357327
|
+
return;
|
|
357328
|
+
} catch (webErr) {
|
|
357329
|
+
if (isTransportError(webErr)) throw webErr;
|
|
357330
|
+
}
|
|
357331
|
+
}
|
|
357332
|
+
}
|
|
357191
357333
|
if (context.platform === "ANDROID" && focusEstablished) {
|
|
357192
357334
|
try {
|
|
357193
357335
|
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
@@ -358166,6 +358308,86 @@ function validateRunnerMobileTarget(value) {
|
|
|
358166
358308
|
};
|
|
358167
358309
|
}
|
|
358168
358310
|
|
|
358311
|
+
// src/services/heartbeat-keepalive.ts
|
|
358312
|
+
import { Worker } from "worker_threads";
|
|
358313
|
+
var KEEPALIVE_INTERVAL_MS = (() => {
|
|
358314
|
+
const raw = Number(process.env.VALIDATEQA_HEARTBEAT_KEEPALIVE_MS);
|
|
358315
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 2e4;
|
|
358316
|
+
})();
|
|
358317
|
+
function isHeartbeatKeepaliveEnabled(env = process.env) {
|
|
358318
|
+
return (env.VALIDATEQA_HEARTBEAT_KEEPALIVE ?? "").trim().toLowerCase() !== "off";
|
|
358319
|
+
}
|
|
358320
|
+
var WORKER_SOURCE = `
|
|
358321
|
+
const { parentPort } = require('node:worker_threads');
|
|
358322
|
+
let url = null;
|
|
358323
|
+
let headers = null;
|
|
358324
|
+
let body = null;
|
|
358325
|
+
let intervalMs = 20000;
|
|
358326
|
+
let timer = null;
|
|
358327
|
+
|
|
358328
|
+
async function post() {
|
|
358329
|
+
if (!url || !headers || !body) return; // inert until the first real heartbeat
|
|
358330
|
+
try {
|
|
358331
|
+
await fetch(url, {
|
|
358332
|
+
method: 'POST',
|
|
358333
|
+
headers,
|
|
358334
|
+
body,
|
|
358335
|
+
signal: AbortSignal.timeout(10000),
|
|
358336
|
+
});
|
|
358337
|
+
} catch {
|
|
358338
|
+
// Best-effort by design: the next tick retries; the main loop's own
|
|
358339
|
+
// heartbeat error handling owns logging/upgrade signals.
|
|
358340
|
+
}
|
|
358341
|
+
}
|
|
358342
|
+
|
|
358343
|
+
parentPort.on('message', (msg) => {
|
|
358344
|
+
if (!msg || typeof msg !== 'object') return;
|
|
358345
|
+
if (msg.type === 'update') {
|
|
358346
|
+
url = msg.url;
|
|
358347
|
+
headers = msg.headers;
|
|
358348
|
+
body = msg.body;
|
|
358349
|
+
intervalMs = msg.intervalMs || intervalMs;
|
|
358350
|
+
if (!timer) {
|
|
358351
|
+
timer = setInterval(post, intervalMs);
|
|
358352
|
+
if (timer.unref) timer.unref();
|
|
358353
|
+
}
|
|
358354
|
+
} else if (msg.type === 'stop') {
|
|
358355
|
+
if (timer) clearInterval(timer);
|
|
358356
|
+
timer = null;
|
|
358357
|
+
parentPort.close();
|
|
358358
|
+
}
|
|
358359
|
+
});
|
|
358360
|
+
`;
|
|
358361
|
+
var NOOP_KEEPALIVE = { update: () => {
|
|
358362
|
+
}, stop: () => {
|
|
358363
|
+
} };
|
|
358364
|
+
function startHeartbeatKeepalive(options = {}) {
|
|
358365
|
+
if (!isHeartbeatKeepaliveEnabled(options.env)) return NOOP_KEEPALIVE;
|
|
358366
|
+
const intervalMs = options.intervalMs ?? KEEPALIVE_INTERVAL_MS;
|
|
358367
|
+
let worker;
|
|
358368
|
+
try {
|
|
358369
|
+
worker = new Worker(WORKER_SOURCE, { eval: true });
|
|
358370
|
+
worker.unref();
|
|
358371
|
+
worker.on("error", () => {
|
|
358372
|
+
worker = null;
|
|
358373
|
+
});
|
|
358374
|
+
} catch {
|
|
358375
|
+
return NOOP_KEEPALIVE;
|
|
358376
|
+
}
|
|
358377
|
+
return {
|
|
358378
|
+
update(url, headers, body) {
|
|
358379
|
+
worker?.postMessage({ type: "update", url, headers, body, intervalMs });
|
|
358380
|
+
},
|
|
358381
|
+
stop() {
|
|
358382
|
+
try {
|
|
358383
|
+
worker?.postMessage({ type: "stop" });
|
|
358384
|
+
} catch {
|
|
358385
|
+
}
|
|
358386
|
+
worker = null;
|
|
358387
|
+
}
|
|
358388
|
+
};
|
|
358389
|
+
}
|
|
358390
|
+
|
|
358169
358391
|
// src/mobile-device-queue.ts
|
|
358170
358392
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
358171
358393
|
if (!ctx) return false;
|
|
@@ -363770,6 +363992,7 @@ async function startRunner(opts) {
|
|
|
363770
363992
|
}
|
|
363771
363993
|
}
|
|
363772
363994
|
};
|
|
363995
|
+
const heartbeatKeepalive = startHeartbeatKeepalive();
|
|
363773
363996
|
const sendHeartbeat = async () => {
|
|
363774
363997
|
try {
|
|
363775
363998
|
const devices = enableMobile ? getCachedDeviceSnapshot() : [];
|
|
@@ -363784,41 +364007,44 @@ async function startRunner(opts) {
|
|
|
363784
364007
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
363785
364008
|
lastInventoryKinds.clear();
|
|
363786
364009
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
363787
|
-
const
|
|
364010
|
+
const heartbeatUrl = `${serverUrl}/api/runner/heartbeat`;
|
|
364011
|
+
const heartbeatBody = JSON.stringify({
|
|
364012
|
+
runnerName: RUNNER_NAME,
|
|
364013
|
+
runnerVersion: RUNNER_VERSION,
|
|
364014
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
364015
|
+
runsCompleted,
|
|
364016
|
+
activeRuns,
|
|
364017
|
+
maxConcurrent: MAX_CONCURRENT,
|
|
364018
|
+
uptimeMs: Date.now() - startedAt,
|
|
364019
|
+
capabilities,
|
|
364020
|
+
devices,
|
|
364021
|
+
mobileBridge: bridge ? {
|
|
364022
|
+
online: bridge.running,
|
|
364023
|
+
port: bridge.port,
|
|
364024
|
+
tunnelUrl: bridge.tunnelUrl,
|
|
364025
|
+
tunnelToken: bridge.tunnelToken
|
|
364026
|
+
} : void 0,
|
|
364027
|
+
activeByType: Object.fromEntries(activeByType),
|
|
364028
|
+
activeByProject: Object.fromEntries(activeByProject),
|
|
364029
|
+
typeLimits: TYPE_LIMITS,
|
|
364030
|
+
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
364031
|
+
resourceSnapshot: resourceStatus.snapshot,
|
|
364032
|
+
resourceGovernor: resourceStatus.governor,
|
|
364033
|
+
processInventory,
|
|
364034
|
+
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
364035
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
364036
|
+
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
364037
|
+
// Cached & cheap — see getCachedDoctor above.
|
|
364038
|
+
doctor: getCachedDoctor()
|
|
364039
|
+
});
|
|
364040
|
+
const response = await fetch(heartbeatUrl, {
|
|
363788
364041
|
method: "POST",
|
|
363789
364042
|
headers,
|
|
363790
|
-
body:
|
|
363791
|
-
runnerName: RUNNER_NAME,
|
|
363792
|
-
runnerVersion: RUNNER_VERSION,
|
|
363793
|
-
runnerBuildId: RUNNER_BUILD_ID,
|
|
363794
|
-
runsCompleted,
|
|
363795
|
-
activeRuns,
|
|
363796
|
-
maxConcurrent: MAX_CONCURRENT,
|
|
363797
|
-
uptimeMs: Date.now() - startedAt,
|
|
363798
|
-
capabilities,
|
|
363799
|
-
devices,
|
|
363800
|
-
mobileBridge: bridge ? {
|
|
363801
|
-
online: bridge.running,
|
|
363802
|
-
port: bridge.port,
|
|
363803
|
-
tunnelUrl: bridge.tunnelUrl,
|
|
363804
|
-
tunnelToken: bridge.tunnelToken
|
|
363805
|
-
} : void 0,
|
|
363806
|
-
activeByType: Object.fromEntries(activeByType),
|
|
363807
|
-
activeByProject: Object.fromEntries(activeByProject),
|
|
363808
|
-
typeLimits: TYPE_LIMITS,
|
|
363809
|
-
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
363810
|
-
resourceSnapshot: resourceStatus.snapshot,
|
|
363811
|
-
resourceGovernor: resourceStatus.governor,
|
|
363812
|
-
processInventory,
|
|
363813
|
-
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
363814
|
-
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
363815
|
-
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
363816
|
-
// Cached & cheap — see getCachedDoctor above.
|
|
363817
|
-
doctor: getCachedDoctor()
|
|
363818
|
-
}),
|
|
364043
|
+
body: heartbeatBody,
|
|
363819
364044
|
signal: AbortSignal.timeout(1e4)
|
|
363820
364045
|
});
|
|
363821
364046
|
if (response.ok) {
|
|
364047
|
+
heartbeatKeepalive.update(heartbeatUrl, headers, heartbeatBody);
|
|
363822
364048
|
const body = await response.json().catch(() => null);
|
|
363823
364049
|
if (body && Array.isArray(body.commands) && body.commands.length > 0) {
|
|
363824
364050
|
void handleControlCommands(body.commands);
|
|
@@ -363889,6 +364115,7 @@ async function startRunner(opts) {
|
|
|
363889
364115
|
process.exit(1);
|
|
363890
364116
|
}, SHUTDOWN_TIMEOUT_MS).unref();
|
|
363891
364117
|
clearInterval(heartbeat);
|
|
364118
|
+
heartbeatKeepalive.stop();
|
|
363892
364119
|
if (liveBrowserHeartbeatTimer) {
|
|
363893
364120
|
clearTimeout(liveBrowserHeartbeatTimer);
|
|
363894
364121
|
liveBrowserHeartbeatTimer = null;
|
package/package.json
CHANGED