@validate.qa/runner 1.0.71 → 1.0.72
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 +160 -44
- package/dist/cli.mjs +160 -44
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11491,7 +11491,7 @@ var require_thread_stream = __commonJS({
|
|
|
11491
11491
|
"use strict";
|
|
11492
11492
|
var { version } = require_package();
|
|
11493
11493
|
var { EventEmitter: EventEmitter2 } = require("events");
|
|
11494
|
-
var { Worker } = require("worker_threads");
|
|
11494
|
+
var { Worker: Worker2 } = require("worker_threads");
|
|
11495
11495
|
var { join: join8 } = require("path");
|
|
11496
11496
|
var { pathToFileURL } = require("url");
|
|
11497
11497
|
var { wait } = require_wait();
|
|
@@ -11544,7 +11544,7 @@ var require_thread_stream = __commonJS({
|
|
|
11544
11544
|
const { filename, workerData } = opts;
|
|
11545
11545
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
11546
11546
|
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
11547
|
-
const worker = new
|
|
11547
|
+
const worker = new Worker2(toExecute, {
|
|
11548
11548
|
...opts.workerOpts,
|
|
11549
11549
|
name: opts.workerOpts?.name || "thread-stream",
|
|
11550
11550
|
trackUnmanagedFds: false,
|
|
@@ -74644,6 +74644,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
74644
74644
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
74645
74645
|
exports2.computeMobileSurveyBreadthContinuation = computeMobileSurveyBreadthContinuation;
|
|
74646
74646
|
exports2.computeMobileBreadthFinishRedirectDecision = computeMobileBreadthFinishRedirectDecision;
|
|
74647
|
+
exports2.hybridScreenFingerprint = hybridScreenFingerprint;
|
|
74647
74648
|
exports2.runMobileExplorePhase = runMobileExplorePhase;
|
|
74648
74649
|
var crypto_1 = require("crypto");
|
|
74649
74650
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
@@ -75097,11 +75098,12 @@ var require_mobile_explorer = __commonJS({
|
|
|
75097
75098
|
const validSurfaceDigest = /^[0-9a-f]{16,64}$/.test(normalizedSurfaceDigest) ? normalizedSurfaceDigest : void 0;
|
|
75098
75099
|
const legacyElementDigests = [...new Set(elements.map((element) => element.pageIdentityDigest?.trim().toLowerCase() ?? "").filter((digest) => /^[0-9a-f]{16,64}$/.test(digest)))];
|
|
75099
75100
|
const pageIdentityDigest = validSurfaceDigest ?? (legacyElementDigests.length === 1 ? legacyElementDigests[0] : void 0);
|
|
75101
|
+
const hasStableControlIdentity = elements.some((element) => Boolean(safeHybridIdentity(element.id, credentials) || safeHybridIdentity(element.name, credentials)));
|
|
75100
75102
|
const descriptors = elements.map((element) => {
|
|
75101
75103
|
const editable = element.editable === true || /^(?:input|textarea|select)$/i.test(element.tag) || /(?:textbox|searchbox|combobox)/i.test(element.role);
|
|
75102
75104
|
const id = safeHybridIdentity(element.id, credentials)?.toLowerCase() ?? "";
|
|
75103
75105
|
const name = safeHybridIdentity(element.name, credentials)?.toLowerCase() ?? "";
|
|
75104
|
-
const semanticText = editable || id || name ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75106
|
+
const semanticText = editable || id || name || hasStableControlIdentity ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75105
75107
|
return [
|
|
75106
75108
|
element.tag.trim().toLowerCase(),
|
|
75107
75109
|
element.role.trim().toLowerCase(),
|
|
@@ -83740,6 +83742,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83740
83742
|
return actions.filter((_, index) => !toDrop.has(index));
|
|
83741
83743
|
}
|
|
83742
83744
|
var MAX_DETOUR_LENGTH = 6;
|
|
83745
|
+
function isDetourClosingAction(action) {
|
|
83746
|
+
if (action.type === "BACK" || action.type === "HOME")
|
|
83747
|
+
return true;
|
|
83748
|
+
return isNavigationChromeTap(action);
|
|
83749
|
+
}
|
|
83750
|
+
function isEvidenceAction(action) {
|
|
83751
|
+
return action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT" || action.type === "WAIT_FOR_ELEMENT";
|
|
83752
|
+
}
|
|
83743
83753
|
function elideNoOpDetours(actions) {
|
|
83744
83754
|
if (actions.length < 2)
|
|
83745
83755
|
return actions;
|
|
@@ -83747,12 +83757,16 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83747
83757
|
for (let j = 0; j < actions.length; j++) {
|
|
83748
83758
|
if (toDrop.has(j))
|
|
83749
83759
|
continue;
|
|
83750
|
-
if (actions[j]
|
|
83760
|
+
if (!isDetourClosingAction(actions[j]))
|
|
83751
83761
|
continue;
|
|
83752
83762
|
const keyAfter = actions[j].metadata.structuralKey;
|
|
83753
83763
|
if (!keyAfter)
|
|
83754
83764
|
continue;
|
|
83765
|
+
const next = actions[j + 1];
|
|
83766
|
+
if (next && isEvidenceAction(next))
|
|
83767
|
+
continue;
|
|
83755
83768
|
let bestStart = null;
|
|
83769
|
+
let leftScreen = false;
|
|
83756
83770
|
const earliestStart = Math.max(0, j - MAX_DETOUR_LENGTH + 1);
|
|
83757
83771
|
for (let i = j; i >= earliestStart; i--) {
|
|
83758
83772
|
if (i < j) {
|
|
@@ -83760,11 +83774,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83760
83774
|
if (inside.type === "ASSERT_VISIBLE" || inside.type === "ASSERT_TEXT" || isDurableMutation(inside)) {
|
|
83761
83775
|
break;
|
|
83762
83776
|
}
|
|
83777
|
+
const insideKey = inside.metadata.structuralKey;
|
|
83778
|
+
if (insideKey && insideKey !== keyAfter)
|
|
83779
|
+
leftScreen = true;
|
|
83763
83780
|
}
|
|
83764
83781
|
if (i === 0)
|
|
83765
83782
|
continue;
|
|
83766
83783
|
const keyBefore = actions[i - 1].metadata.structuralKey;
|
|
83767
|
-
if (i < j && keyBefore && keyBefore === keyAfter) {
|
|
83784
|
+
if (i < j && leftScreen && keyBefore && keyBefore === keyAfter) {
|
|
83768
83785
|
bestStart = i;
|
|
83769
83786
|
}
|
|
83770
83787
|
}
|
|
@@ -352990,7 +353007,7 @@ var require_package4 = __commonJS({
|
|
|
352990
353007
|
"package.json"(exports2, module2) {
|
|
352991
353008
|
module2.exports = {
|
|
352992
353009
|
name: "@validate.qa/runner",
|
|
352993
|
-
version: "1.0.
|
|
353010
|
+
version: "1.0.72",
|
|
352994
353011
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
352995
353012
|
bin: {
|
|
352996
353013
|
"validate-runner": "dist/cli.js"
|
|
@@ -354802,7 +354819,11 @@ async function pressHome(sessionPath, platform3) {
|
|
|
354802
354819
|
}
|
|
354803
354820
|
|
|
354804
354821
|
// src/services/device-quarantine.ts
|
|
354805
|
-
var
|
|
354822
|
+
var QUARANTINE_MAX_MS = (() => {
|
|
354823
|
+
const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
|
|
354824
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
|
|
354825
|
+
})();
|
|
354826
|
+
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
354806
354827
|
var DeviceQuarantinedError = class extends Error {
|
|
354807
354828
|
constructor(deviceId) {
|
|
354808
354829
|
super(
|
|
@@ -354811,17 +354832,27 @@ var DeviceQuarantinedError = class extends Error {
|
|
|
354811
354832
|
this.name = "DeviceQuarantinedError";
|
|
354812
354833
|
}
|
|
354813
354834
|
};
|
|
354814
|
-
function quarantineDevice(deviceId) {
|
|
354815
|
-
|
|
354835
|
+
function quarantineDevice(deviceId, now = Date.now()) {
|
|
354836
|
+
quarantinedAt.set(deviceId, now);
|
|
354816
354837
|
}
|
|
354817
354838
|
function unquarantineDevice(deviceId) {
|
|
354818
|
-
|
|
354839
|
+
quarantinedAt.delete(deviceId);
|
|
354819
354840
|
}
|
|
354820
|
-
function isDeviceQuarantined(deviceId) {
|
|
354821
|
-
|
|
354841
|
+
function isDeviceQuarantined(deviceId, now = Date.now()) {
|
|
354842
|
+
if (typeof deviceId !== "string") return false;
|
|
354843
|
+
const since = quarantinedAt.get(deviceId);
|
|
354844
|
+
if (since === void 0) return false;
|
|
354845
|
+
if (now - since >= QUARANTINE_MAX_MS) {
|
|
354846
|
+
quarantinedAt.delete(deviceId);
|
|
354847
|
+
console.warn(
|
|
354848
|
+
`[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).`
|
|
354849
|
+
);
|
|
354850
|
+
return false;
|
|
354851
|
+
}
|
|
354852
|
+
return true;
|
|
354822
354853
|
}
|
|
354823
|
-
function assertDeviceNotQuarantined(deviceId) {
|
|
354824
|
-
if (isDeviceQuarantined(deviceId)) {
|
|
354854
|
+
function assertDeviceNotQuarantined(deviceId, now = Date.now()) {
|
|
354855
|
+
if (isDeviceQuarantined(deviceId, now)) {
|
|
354825
354856
|
throw new DeviceQuarantinedError(deviceId);
|
|
354826
354857
|
}
|
|
354827
354858
|
}
|
|
@@ -358158,6 +358189,86 @@ function validateRunnerMobileTarget(value) {
|
|
|
358158
358189
|
};
|
|
358159
358190
|
}
|
|
358160
358191
|
|
|
358192
|
+
// src/services/heartbeat-keepalive.ts
|
|
358193
|
+
var import_node_worker_threads = require("worker_threads");
|
|
358194
|
+
var KEEPALIVE_INTERVAL_MS = (() => {
|
|
358195
|
+
const raw = Number(process.env.VALIDATEQA_HEARTBEAT_KEEPALIVE_MS);
|
|
358196
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 2e4;
|
|
358197
|
+
})();
|
|
358198
|
+
function isHeartbeatKeepaliveEnabled(env = process.env) {
|
|
358199
|
+
return (env.VALIDATEQA_HEARTBEAT_KEEPALIVE ?? "").trim().toLowerCase() !== "off";
|
|
358200
|
+
}
|
|
358201
|
+
var WORKER_SOURCE = `
|
|
358202
|
+
const { parentPort } = require('node:worker_threads');
|
|
358203
|
+
let url = null;
|
|
358204
|
+
let headers = null;
|
|
358205
|
+
let body = null;
|
|
358206
|
+
let intervalMs = 20000;
|
|
358207
|
+
let timer = null;
|
|
358208
|
+
|
|
358209
|
+
async function post() {
|
|
358210
|
+
if (!url || !headers || !body) return; // inert until the first real heartbeat
|
|
358211
|
+
try {
|
|
358212
|
+
await fetch(url, {
|
|
358213
|
+
method: 'POST',
|
|
358214
|
+
headers,
|
|
358215
|
+
body,
|
|
358216
|
+
signal: AbortSignal.timeout(10000),
|
|
358217
|
+
});
|
|
358218
|
+
} catch {
|
|
358219
|
+
// Best-effort by design: the next tick retries; the main loop's own
|
|
358220
|
+
// heartbeat error handling owns logging/upgrade signals.
|
|
358221
|
+
}
|
|
358222
|
+
}
|
|
358223
|
+
|
|
358224
|
+
parentPort.on('message', (msg) => {
|
|
358225
|
+
if (!msg || typeof msg !== 'object') return;
|
|
358226
|
+
if (msg.type === 'update') {
|
|
358227
|
+
url = msg.url;
|
|
358228
|
+
headers = msg.headers;
|
|
358229
|
+
body = msg.body;
|
|
358230
|
+
intervalMs = msg.intervalMs || intervalMs;
|
|
358231
|
+
if (!timer) {
|
|
358232
|
+
timer = setInterval(post, intervalMs);
|
|
358233
|
+
if (timer.unref) timer.unref();
|
|
358234
|
+
}
|
|
358235
|
+
} else if (msg.type === 'stop') {
|
|
358236
|
+
if (timer) clearInterval(timer);
|
|
358237
|
+
timer = null;
|
|
358238
|
+
parentPort.close();
|
|
358239
|
+
}
|
|
358240
|
+
});
|
|
358241
|
+
`;
|
|
358242
|
+
var NOOP_KEEPALIVE = { update: () => {
|
|
358243
|
+
}, stop: () => {
|
|
358244
|
+
} };
|
|
358245
|
+
function startHeartbeatKeepalive(options = {}) {
|
|
358246
|
+
if (!isHeartbeatKeepaliveEnabled(options.env)) return NOOP_KEEPALIVE;
|
|
358247
|
+
const intervalMs = options.intervalMs ?? KEEPALIVE_INTERVAL_MS;
|
|
358248
|
+
let worker;
|
|
358249
|
+
try {
|
|
358250
|
+
worker = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true });
|
|
358251
|
+
worker.unref();
|
|
358252
|
+
worker.on("error", () => {
|
|
358253
|
+
worker = null;
|
|
358254
|
+
});
|
|
358255
|
+
} catch {
|
|
358256
|
+
return NOOP_KEEPALIVE;
|
|
358257
|
+
}
|
|
358258
|
+
return {
|
|
358259
|
+
update(url, headers, body) {
|
|
358260
|
+
worker?.postMessage({ type: "update", url, headers, body, intervalMs });
|
|
358261
|
+
},
|
|
358262
|
+
stop() {
|
|
358263
|
+
try {
|
|
358264
|
+
worker?.postMessage({ type: "stop" });
|
|
358265
|
+
} catch {
|
|
358266
|
+
}
|
|
358267
|
+
worker = null;
|
|
358268
|
+
}
|
|
358269
|
+
};
|
|
358270
|
+
}
|
|
358271
|
+
|
|
358161
358272
|
// src/mobile-device-queue.ts
|
|
358162
358273
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
358163
358274
|
if (!ctx) return false;
|
|
@@ -363762,6 +363873,7 @@ async function startRunner(opts) {
|
|
|
363762
363873
|
}
|
|
363763
363874
|
}
|
|
363764
363875
|
};
|
|
363876
|
+
const heartbeatKeepalive = startHeartbeatKeepalive();
|
|
363765
363877
|
const sendHeartbeat = async () => {
|
|
363766
363878
|
try {
|
|
363767
363879
|
const devices = enableMobile ? getCachedDeviceSnapshot() : [];
|
|
@@ -363776,41 +363888,44 @@ async function startRunner(opts) {
|
|
|
363776
363888
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
363777
363889
|
lastInventoryKinds.clear();
|
|
363778
363890
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
363779
|
-
const
|
|
363891
|
+
const heartbeatUrl = `${serverUrl}/api/runner/heartbeat`;
|
|
363892
|
+
const heartbeatBody = JSON.stringify({
|
|
363893
|
+
runnerName: RUNNER_NAME,
|
|
363894
|
+
runnerVersion: RUNNER_VERSION,
|
|
363895
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
363896
|
+
runsCompleted,
|
|
363897
|
+
activeRuns,
|
|
363898
|
+
maxConcurrent: MAX_CONCURRENT,
|
|
363899
|
+
uptimeMs: Date.now() - startedAt,
|
|
363900
|
+
capabilities,
|
|
363901
|
+
devices,
|
|
363902
|
+
mobileBridge: bridge ? {
|
|
363903
|
+
online: bridge.running,
|
|
363904
|
+
port: bridge.port,
|
|
363905
|
+
tunnelUrl: bridge.tunnelUrl,
|
|
363906
|
+
tunnelToken: bridge.tunnelToken
|
|
363907
|
+
} : void 0,
|
|
363908
|
+
activeByType: Object.fromEntries(activeByType),
|
|
363909
|
+
activeByProject: Object.fromEntries(activeByProject),
|
|
363910
|
+
typeLimits: TYPE_LIMITS,
|
|
363911
|
+
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
363912
|
+
resourceSnapshot: resourceStatus.snapshot,
|
|
363913
|
+
resourceGovernor: resourceStatus.governor,
|
|
363914
|
+
processInventory,
|
|
363915
|
+
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
363916
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
363917
|
+
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
363918
|
+
// Cached & cheap — see getCachedDoctor above.
|
|
363919
|
+
doctor: getCachedDoctor()
|
|
363920
|
+
});
|
|
363921
|
+
const response = await fetch(heartbeatUrl, {
|
|
363780
363922
|
method: "POST",
|
|
363781
363923
|
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
|
-
}),
|
|
363924
|
+
body: heartbeatBody,
|
|
363811
363925
|
signal: AbortSignal.timeout(1e4)
|
|
363812
363926
|
});
|
|
363813
363927
|
if (response.ok) {
|
|
363928
|
+
heartbeatKeepalive.update(heartbeatUrl, headers, heartbeatBody);
|
|
363814
363929
|
const body = await response.json().catch(() => null);
|
|
363815
363930
|
if (body && Array.isArray(body.commands) && body.commands.length > 0) {
|
|
363816
363931
|
void handleControlCommands(body.commands);
|
|
@@ -363881,6 +363996,7 @@ async function startRunner(opts) {
|
|
|
363881
363996
|
process.exit(1);
|
|
363882
363997
|
}, SHUTDOWN_TIMEOUT_MS).unref();
|
|
363883
363998
|
clearInterval(heartbeat);
|
|
363999
|
+
heartbeatKeepalive.stop();
|
|
363884
364000
|
if (liveBrowserHeartbeatTimer) {
|
|
363885
364001
|
clearTimeout(liveBrowserHeartbeatTimer);
|
|
363886
364002
|
liveBrowserHeartbeatTimer = null;
|
package/dist/cli.mjs
CHANGED
|
@@ -11496,7 +11496,7 @@ var require_thread_stream = __commonJS({
|
|
|
11496
11496
|
"use strict";
|
|
11497
11497
|
var { version } = require_package();
|
|
11498
11498
|
var { EventEmitter: EventEmitter2 } = __require("events");
|
|
11499
|
-
var { Worker } = __require("worker_threads");
|
|
11499
|
+
var { Worker: Worker2 } = __require("worker_threads");
|
|
11500
11500
|
var { join: join8 } = __require("path");
|
|
11501
11501
|
var { pathToFileURL } = __require("url");
|
|
11502
11502
|
var { wait } = require_wait();
|
|
@@ -11549,7 +11549,7 @@ var require_thread_stream = __commonJS({
|
|
|
11549
11549
|
const { filename, workerData } = opts;
|
|
11550
11550
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
11551
11551
|
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
11552
|
-
const worker = new
|
|
11552
|
+
const worker = new Worker2(toExecute, {
|
|
11553
11553
|
...opts.workerOpts,
|
|
11554
11554
|
name: opts.workerOpts?.name || "thread-stream",
|
|
11555
11555
|
trackUnmanagedFds: false,
|
|
@@ -74649,6 +74649,7 @@ var require_mobile_explorer = __commonJS({
|
|
|
74649
74649
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74650
74650
|
exports.computeMobileSurveyBreadthContinuation = computeMobileSurveyBreadthContinuation;
|
|
74651
74651
|
exports.computeMobileBreadthFinishRedirectDecision = computeMobileBreadthFinishRedirectDecision;
|
|
74652
|
+
exports.hybridScreenFingerprint = hybridScreenFingerprint;
|
|
74652
74653
|
exports.runMobileExplorePhase = runMobileExplorePhase;
|
|
74653
74654
|
var crypto_1 = __require("crypto");
|
|
74654
74655
|
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
@@ -75102,11 +75103,12 @@ var require_mobile_explorer = __commonJS({
|
|
|
75102
75103
|
const validSurfaceDigest = /^[0-9a-f]{16,64}$/.test(normalizedSurfaceDigest) ? normalizedSurfaceDigest : void 0;
|
|
75103
75104
|
const legacyElementDigests = [...new Set(elements.map((element) => element.pageIdentityDigest?.trim().toLowerCase() ?? "").filter((digest) => /^[0-9a-f]{16,64}$/.test(digest)))];
|
|
75104
75105
|
const pageIdentityDigest = validSurfaceDigest ?? (legacyElementDigests.length === 1 ? legacyElementDigests[0] : void 0);
|
|
75106
|
+
const hasStableControlIdentity = elements.some((element) => Boolean(safeHybridIdentity(element.id, credentials) || safeHybridIdentity(element.name, credentials)));
|
|
75105
75107
|
const descriptors = elements.map((element) => {
|
|
75106
75108
|
const editable = element.editable === true || /^(?:input|textarea|select)$/i.test(element.tag) || /(?:textbox|searchbox|combobox)/i.test(element.role);
|
|
75107
75109
|
const id = safeHybridIdentity(element.id, credentials)?.toLowerCase() ?? "";
|
|
75108
75110
|
const name = safeHybridIdentity(element.name, credentials)?.toLowerCase() ?? "";
|
|
75109
|
-
const semanticText = editable || id || name ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75111
|
+
const semanticText = editable || id || name || hasStableControlIdentity ? "" : stableHybridSemanticText(element.text, credentials);
|
|
75110
75112
|
return [
|
|
75111
75113
|
element.tag.trim().toLowerCase(),
|
|
75112
75114
|
element.role.trim().toLowerCase(),
|
|
@@ -83745,6 +83747,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83745
83747
|
return actions.filter((_, index) => !toDrop.has(index));
|
|
83746
83748
|
}
|
|
83747
83749
|
var MAX_DETOUR_LENGTH = 6;
|
|
83750
|
+
function isDetourClosingAction(action) {
|
|
83751
|
+
if (action.type === "BACK" || action.type === "HOME")
|
|
83752
|
+
return true;
|
|
83753
|
+
return isNavigationChromeTap(action);
|
|
83754
|
+
}
|
|
83755
|
+
function isEvidenceAction(action) {
|
|
83756
|
+
return action.type === "ASSERT_VISIBLE" || action.type === "ASSERT_TEXT" || action.type === "WAIT_FOR_ELEMENT";
|
|
83757
|
+
}
|
|
83748
83758
|
function elideNoOpDetours(actions) {
|
|
83749
83759
|
if (actions.length < 2)
|
|
83750
83760
|
return actions;
|
|
@@ -83752,12 +83762,16 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83752
83762
|
for (let j = 0; j < actions.length; j++) {
|
|
83753
83763
|
if (toDrop.has(j))
|
|
83754
83764
|
continue;
|
|
83755
|
-
if (actions[j]
|
|
83765
|
+
if (!isDetourClosingAction(actions[j]))
|
|
83756
83766
|
continue;
|
|
83757
83767
|
const keyAfter = actions[j].metadata.structuralKey;
|
|
83758
83768
|
if (!keyAfter)
|
|
83759
83769
|
continue;
|
|
83770
|
+
const next = actions[j + 1];
|
|
83771
|
+
if (next && isEvidenceAction(next))
|
|
83772
|
+
continue;
|
|
83760
83773
|
let bestStart = null;
|
|
83774
|
+
let leftScreen = false;
|
|
83761
83775
|
const earliestStart = Math.max(0, j - MAX_DETOUR_LENGTH + 1);
|
|
83762
83776
|
for (let i = j; i >= earliestStart; i--) {
|
|
83763
83777
|
if (i < j) {
|
|
@@ -83765,11 +83779,14 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83765
83779
|
if (inside.type === "ASSERT_VISIBLE" || inside.type === "ASSERT_TEXT" || isDurableMutation(inside)) {
|
|
83766
83780
|
break;
|
|
83767
83781
|
}
|
|
83782
|
+
const insideKey = inside.metadata.structuralKey;
|
|
83783
|
+
if (insideKey && insideKey !== keyAfter)
|
|
83784
|
+
leftScreen = true;
|
|
83768
83785
|
}
|
|
83769
83786
|
if (i === 0)
|
|
83770
83787
|
continue;
|
|
83771
83788
|
const keyBefore = actions[i - 1].metadata.structuralKey;
|
|
83772
|
-
if (i < j && keyBefore && keyBefore === keyAfter) {
|
|
83789
|
+
if (i < j && leftScreen && keyBefore && keyBefore === keyAfter) {
|
|
83773
83790
|
bestStart = i;
|
|
83774
83791
|
}
|
|
83775
83792
|
}
|
|
@@ -352995,7 +353012,7 @@ var require_package4 = __commonJS({
|
|
|
352995
353012
|
"package.json"(exports, module) {
|
|
352996
353013
|
module.exports = {
|
|
352997
353014
|
name: "@validate.qa/runner",
|
|
352998
|
-
version: "1.0.
|
|
353015
|
+
version: "1.0.72",
|
|
352999
353016
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353000
353017
|
bin: {
|
|
353001
353018
|
"validate-runner": "dist/cli.js"
|
|
@@ -354807,7 +354824,11 @@ async function pressHome(sessionPath, platform3) {
|
|
|
354807
354824
|
}
|
|
354808
354825
|
|
|
354809
354826
|
// src/services/device-quarantine.ts
|
|
354810
|
-
var
|
|
354827
|
+
var QUARANTINE_MAX_MS = (() => {
|
|
354828
|
+
const raw = Number(process.env.VALIDATEQA_DEVICE_QUARANTINE_MAX_MS);
|
|
354829
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10 * 60 * 1e3;
|
|
354830
|
+
})();
|
|
354831
|
+
var quarantinedAt = /* @__PURE__ */ new Map();
|
|
354811
354832
|
var DeviceQuarantinedError = class extends Error {
|
|
354812
354833
|
constructor(deviceId) {
|
|
354813
354834
|
super(
|
|
@@ -354816,17 +354837,27 @@ var DeviceQuarantinedError = class extends Error {
|
|
|
354816
354837
|
this.name = "DeviceQuarantinedError";
|
|
354817
354838
|
}
|
|
354818
354839
|
};
|
|
354819
|
-
function quarantineDevice(deviceId) {
|
|
354820
|
-
|
|
354840
|
+
function quarantineDevice(deviceId, now = Date.now()) {
|
|
354841
|
+
quarantinedAt.set(deviceId, now);
|
|
354821
354842
|
}
|
|
354822
354843
|
function unquarantineDevice(deviceId) {
|
|
354823
|
-
|
|
354844
|
+
quarantinedAt.delete(deviceId);
|
|
354824
354845
|
}
|
|
354825
|
-
function isDeviceQuarantined(deviceId) {
|
|
354826
|
-
|
|
354846
|
+
function isDeviceQuarantined(deviceId, now = Date.now()) {
|
|
354847
|
+
if (typeof deviceId !== "string") return false;
|
|
354848
|
+
const since = quarantinedAt.get(deviceId);
|
|
354849
|
+
if (since === void 0) return false;
|
|
354850
|
+
if (now - since >= QUARANTINE_MAX_MS) {
|
|
354851
|
+
quarantinedAt.delete(deviceId);
|
|
354852
|
+
console.warn(
|
|
354853
|
+
`[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).`
|
|
354854
|
+
);
|
|
354855
|
+
return false;
|
|
354856
|
+
}
|
|
354857
|
+
return true;
|
|
354827
354858
|
}
|
|
354828
|
-
function assertDeviceNotQuarantined(deviceId) {
|
|
354829
|
-
if (isDeviceQuarantined(deviceId)) {
|
|
354859
|
+
function assertDeviceNotQuarantined(deviceId, now = Date.now()) {
|
|
354860
|
+
if (isDeviceQuarantined(deviceId, now)) {
|
|
354830
354861
|
throw new DeviceQuarantinedError(deviceId);
|
|
354831
354862
|
}
|
|
354832
354863
|
}
|
|
@@ -358166,6 +358197,86 @@ function validateRunnerMobileTarget(value) {
|
|
|
358166
358197
|
};
|
|
358167
358198
|
}
|
|
358168
358199
|
|
|
358200
|
+
// src/services/heartbeat-keepalive.ts
|
|
358201
|
+
import { Worker } from "worker_threads";
|
|
358202
|
+
var KEEPALIVE_INTERVAL_MS = (() => {
|
|
358203
|
+
const raw = Number(process.env.VALIDATEQA_HEARTBEAT_KEEPALIVE_MS);
|
|
358204
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 2e4;
|
|
358205
|
+
})();
|
|
358206
|
+
function isHeartbeatKeepaliveEnabled(env = process.env) {
|
|
358207
|
+
return (env.VALIDATEQA_HEARTBEAT_KEEPALIVE ?? "").trim().toLowerCase() !== "off";
|
|
358208
|
+
}
|
|
358209
|
+
var WORKER_SOURCE = `
|
|
358210
|
+
const { parentPort } = require('node:worker_threads');
|
|
358211
|
+
let url = null;
|
|
358212
|
+
let headers = null;
|
|
358213
|
+
let body = null;
|
|
358214
|
+
let intervalMs = 20000;
|
|
358215
|
+
let timer = null;
|
|
358216
|
+
|
|
358217
|
+
async function post() {
|
|
358218
|
+
if (!url || !headers || !body) return; // inert until the first real heartbeat
|
|
358219
|
+
try {
|
|
358220
|
+
await fetch(url, {
|
|
358221
|
+
method: 'POST',
|
|
358222
|
+
headers,
|
|
358223
|
+
body,
|
|
358224
|
+
signal: AbortSignal.timeout(10000),
|
|
358225
|
+
});
|
|
358226
|
+
} catch {
|
|
358227
|
+
// Best-effort by design: the next tick retries; the main loop's own
|
|
358228
|
+
// heartbeat error handling owns logging/upgrade signals.
|
|
358229
|
+
}
|
|
358230
|
+
}
|
|
358231
|
+
|
|
358232
|
+
parentPort.on('message', (msg) => {
|
|
358233
|
+
if (!msg || typeof msg !== 'object') return;
|
|
358234
|
+
if (msg.type === 'update') {
|
|
358235
|
+
url = msg.url;
|
|
358236
|
+
headers = msg.headers;
|
|
358237
|
+
body = msg.body;
|
|
358238
|
+
intervalMs = msg.intervalMs || intervalMs;
|
|
358239
|
+
if (!timer) {
|
|
358240
|
+
timer = setInterval(post, intervalMs);
|
|
358241
|
+
if (timer.unref) timer.unref();
|
|
358242
|
+
}
|
|
358243
|
+
} else if (msg.type === 'stop') {
|
|
358244
|
+
if (timer) clearInterval(timer);
|
|
358245
|
+
timer = null;
|
|
358246
|
+
parentPort.close();
|
|
358247
|
+
}
|
|
358248
|
+
});
|
|
358249
|
+
`;
|
|
358250
|
+
var NOOP_KEEPALIVE = { update: () => {
|
|
358251
|
+
}, stop: () => {
|
|
358252
|
+
} };
|
|
358253
|
+
function startHeartbeatKeepalive(options = {}) {
|
|
358254
|
+
if (!isHeartbeatKeepaliveEnabled(options.env)) return NOOP_KEEPALIVE;
|
|
358255
|
+
const intervalMs = options.intervalMs ?? KEEPALIVE_INTERVAL_MS;
|
|
358256
|
+
let worker;
|
|
358257
|
+
try {
|
|
358258
|
+
worker = new Worker(WORKER_SOURCE, { eval: true });
|
|
358259
|
+
worker.unref();
|
|
358260
|
+
worker.on("error", () => {
|
|
358261
|
+
worker = null;
|
|
358262
|
+
});
|
|
358263
|
+
} catch {
|
|
358264
|
+
return NOOP_KEEPALIVE;
|
|
358265
|
+
}
|
|
358266
|
+
return {
|
|
358267
|
+
update(url, headers, body) {
|
|
358268
|
+
worker?.postMessage({ type: "update", url, headers, body, intervalMs });
|
|
358269
|
+
},
|
|
358270
|
+
stop() {
|
|
358271
|
+
try {
|
|
358272
|
+
worker?.postMessage({ type: "stop" });
|
|
358273
|
+
} catch {
|
|
358274
|
+
}
|
|
358275
|
+
worker = null;
|
|
358276
|
+
}
|
|
358277
|
+
};
|
|
358278
|
+
}
|
|
358279
|
+
|
|
358169
358280
|
// src/mobile-device-queue.ts
|
|
358170
358281
|
function isNativeMobileDiscoveryContext(ctx) {
|
|
358171
358282
|
if (!ctx) return false;
|
|
@@ -363770,6 +363881,7 @@ async function startRunner(opts) {
|
|
|
363770
363881
|
}
|
|
363771
363882
|
}
|
|
363772
363883
|
};
|
|
363884
|
+
const heartbeatKeepalive = startHeartbeatKeepalive();
|
|
363773
363885
|
const sendHeartbeat = async () => {
|
|
363774
363886
|
try {
|
|
363775
363887
|
const devices = enableMobile ? getCachedDeviceSnapshot() : [];
|
|
@@ -363784,41 +363896,44 @@ async function startRunner(opts) {
|
|
|
363784
363896
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
363785
363897
|
lastInventoryKinds.clear();
|
|
363786
363898
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
363787
|
-
const
|
|
363899
|
+
const heartbeatUrl = `${serverUrl}/api/runner/heartbeat`;
|
|
363900
|
+
const heartbeatBody = JSON.stringify({
|
|
363901
|
+
runnerName: RUNNER_NAME,
|
|
363902
|
+
runnerVersion: RUNNER_VERSION,
|
|
363903
|
+
runnerBuildId: RUNNER_BUILD_ID,
|
|
363904
|
+
runsCompleted,
|
|
363905
|
+
activeRuns,
|
|
363906
|
+
maxConcurrent: MAX_CONCURRENT,
|
|
363907
|
+
uptimeMs: Date.now() - startedAt,
|
|
363908
|
+
capabilities,
|
|
363909
|
+
devices,
|
|
363910
|
+
mobileBridge: bridge ? {
|
|
363911
|
+
online: bridge.running,
|
|
363912
|
+
port: bridge.port,
|
|
363913
|
+
tunnelUrl: bridge.tunnelUrl,
|
|
363914
|
+
tunnelToken: bridge.tunnelToken
|
|
363915
|
+
} : void 0,
|
|
363916
|
+
activeByType: Object.fromEntries(activeByType),
|
|
363917
|
+
activeByProject: Object.fromEntries(activeByProject),
|
|
363918
|
+
typeLimits: TYPE_LIMITS,
|
|
363919
|
+
maxProjectConcurrent: MAX_PROJECT_CONCURRENT,
|
|
363920
|
+
resourceSnapshot: resourceStatus.snapshot,
|
|
363921
|
+
resourceGovernor: resourceStatus.governor,
|
|
363922
|
+
processInventory,
|
|
363923
|
+
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
363924
|
+
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
363925
|
+
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
363926
|
+
// Cached & cheap — see getCachedDoctor above.
|
|
363927
|
+
doctor: getCachedDoctor()
|
|
363928
|
+
});
|
|
363929
|
+
const response = await fetch(heartbeatUrl, {
|
|
363788
363930
|
method: "POST",
|
|
363789
363931
|
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
|
-
}),
|
|
363932
|
+
body: heartbeatBody,
|
|
363819
363933
|
signal: AbortSignal.timeout(1e4)
|
|
363820
363934
|
});
|
|
363821
363935
|
if (response.ok) {
|
|
363936
|
+
heartbeatKeepalive.update(heartbeatUrl, headers, heartbeatBody);
|
|
363822
363937
|
const body = await response.json().catch(() => null);
|
|
363823
363938
|
if (body && Array.isArray(body.commands) && body.commands.length > 0) {
|
|
363824
363939
|
void handleControlCommands(body.commands);
|
|
@@ -363889,6 +364004,7 @@ async function startRunner(opts) {
|
|
|
363889
364004
|
process.exit(1);
|
|
363890
364005
|
}, SHUTDOWN_TIMEOUT_MS).unref();
|
|
363891
364006
|
clearInterval(heartbeat);
|
|
364007
|
+
heartbeatKeepalive.stop();
|
|
363892
364008
|
if (liveBrowserHeartbeatTimer) {
|
|
363893
364009
|
clearTimeout(liveBrowserHeartbeatTimer);
|
|
363894
364010
|
liveBrowserHeartbeatTimer = null;
|
package/package.json
CHANGED