@validate.qa/runner 1.0.79 → 1.0.80
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 +188 -17
- package/dist/cli.mjs +188 -17
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1907,7 +1907,7 @@ function isValidLiveFrame(frame) {
|
|
|
1907
1907
|
return false;
|
|
1908
1908
|
return true;
|
|
1909
1909
|
}
|
|
1910
|
-
var readEnvInt, LIVE_FRAME_PUSH_MAX_BASE64_BYTES, LIVE_THUMBNAIL_MAX_BASE64_BYTES, LIVE_FRAME_MAX_DIMENSION, LIVE_FRAME_JPEG_QUALITY, LIVE_FRAME_MIN_PUSH_INTERVAL_MS, LIVE_FRAME_UNFOCUSED_INTERVAL_MS, LIVE_FRAME_STALE_MS, LIVE_FRAME_MAX_SESSIONS_PER_PROJECT, MOBILE_MJPEG_SETTINGS, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS, MOBILE_MJPEG_MAX_FRAME_BYTES;
|
|
1910
|
+
var readEnvInt, LIVE_FRAME_PUSH_MAX_BASE64_BYTES, LIVE_THUMBNAIL_MAX_BASE64_BYTES, LIVE_FRAME_MAX_DIMENSION, LIVE_FRAME_JPEG_QUALITY, LIVE_FRAME_MIN_PUSH_INTERVAL_MS, LIVE_FRAME_UNFOCUSED_INTERVAL_MS, LIVE_FRAME_IDLE_PUSH_INTERVAL_MS, LIVE_FRAME_STALE_MS, LIVE_FRAME_MAX_SESSIONS_PER_PROJECT, MOBILE_MJPEG_SETTINGS, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS, MOBILE_MJPEG_MAX_FRAME_BYTES;
|
|
1911
1911
|
var init_live_frame = __esm({
|
|
1912
1912
|
"../shared/dist/live-frame.js"() {
|
|
1913
1913
|
"use strict";
|
|
@@ -1923,6 +1923,7 @@ var init_live_frame = __esm({
|
|
|
1923
1923
|
LIVE_FRAME_JPEG_QUALITY = readEnvInt("LIVE_FRAME_JPEG_QUALITY", 55);
|
|
1924
1924
|
LIVE_FRAME_MIN_PUSH_INTERVAL_MS = readEnvInt("LIVE_FRAME_MIN_PUSH_INTERVAL_MS", 125);
|
|
1925
1925
|
LIVE_FRAME_UNFOCUSED_INTERVAL_MS = readEnvInt("LIVE_FRAME_UNFOCUSED_INTERVAL_MS", 2e3);
|
|
1926
|
+
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS = readEnvInt("LIVE_FRAME_IDLE_PUSH_INTERVAL_MS", 5e3);
|
|
1926
1927
|
LIVE_FRAME_STALE_MS = readEnvInt("LIVE_FRAME_STALE_MS", 3e4);
|
|
1927
1928
|
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT = readEnvInt("LIVE_FRAME_MAX_SESSIONS_PER_PROJECT", 32);
|
|
1928
1929
|
MOBILE_MJPEG_SETTINGS = {
|
|
@@ -6360,6 +6361,7 @@ __export(dist_exports, {
|
|
|
6360
6361
|
GROUP_LABELS: () => GROUP_LABELS,
|
|
6361
6362
|
LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
|
|
6362
6363
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
6364
|
+
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS: () => LIVE_FRAME_IDLE_PUSH_INTERVAL_MS,
|
|
6363
6365
|
LIVE_FRAME_JPEG_QUALITY: () => LIVE_FRAME_JPEG_QUALITY,
|
|
6364
6366
|
LIVE_FRAME_MAX_DIMENSION: () => LIVE_FRAME_MAX_DIMENSION,
|
|
6365
6367
|
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT: () => LIVE_FRAME_MAX_SESSIONS_PER_PROJECT,
|
|
@@ -321051,6 +321053,7 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321051
321053
|
exports2.isLedgerActive = isLedgerActive;
|
|
321052
321054
|
exports2.isGateEnforcing = isGateEnforcing;
|
|
321053
321055
|
exports2.isSteeringActive = isSteeringActive;
|
|
321056
|
+
exports2.isPrimaryActionCoverageGapEnabled = isPrimaryActionCoverageGapEnabled;
|
|
321054
321057
|
exports2.leadingVerb = leadingVerb;
|
|
321055
321058
|
exports2.classifyPrimaryAction = classifyPrimaryAction;
|
|
321056
321059
|
exports2.containerScopeOf = containerScopeOf;
|
|
@@ -321065,6 +321068,8 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321065
321068
|
exports2.classifyActionOutcome = classifyActionOutcome;
|
|
321066
321069
|
exports2.detectPrimaryActions = detectPrimaryActions;
|
|
321067
321070
|
exports2.summarizeLedger = summarizeLedger;
|
|
321071
|
+
exports2.summarizeUnexercisedPrimaries = summarizeUnexercisedPrimaries;
|
|
321072
|
+
exports2.buildUnexercisedPrimaryCoverageGaps = buildUnexercisedPrimaryCoverageGaps;
|
|
321068
321073
|
var reveal_diff_js_1 = require_reveal_diff();
|
|
321069
321074
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
321070
321075
|
function getPrimaryActionGateMode(env = process.env) {
|
|
@@ -321084,6 +321089,10 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321084
321089
|
function isSteeringActive(mode) {
|
|
321085
321090
|
return mode === "advisory" || mode === "enforce";
|
|
321086
321091
|
}
|
|
321092
|
+
function isPrimaryActionCoverageGapEnabled(env = process.env) {
|
|
321093
|
+
const raw = (env.DISCOVERY_PRIMARY_ACTION_COVERAGE_GAP ?? "").trim().toLowerCase();
|
|
321094
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "enabled";
|
|
321095
|
+
}
|
|
321087
321096
|
exports2.PRIMARY_ACTION_VERB_RE = /^(run|analyze|analyse|generate|regenerate|process|submit|save|create|send|upload|search|export|import|calculate|recalculate|compute|convert|start|scan|translate|transcribe|render|publish|sync|deploy|enrich|summar(?:ize|ise))\b/i;
|
|
321088
321097
|
exports2.AMBIGUOUS_ACTION_VERB_RE = /^(continue|next|apply|go|proceed|confirm|update|add|edit|ok|done|finish|complete|approve|enable|activate)\b/i;
|
|
321089
321098
|
exports2.DESTRUCTIVE_ACTION_VERB_RE = /^(delete|remove|archive|reset|disconnect|deactivate|destroy|purge|revoke|discard|drop|wipe|erase|unsubscribe|cancel|clear|trash)\b/i;
|
|
@@ -321347,6 +321356,47 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321347
321356
|
}
|
|
321348
321357
|
return `actions=${c.total} primary{detected=${c.detected} attempted=${c.attempted} captured=${c.captured} blocked=${c.blocked} exempt=${c.exempt}} ambiguous=${c.ambiguous}`;
|
|
321349
321358
|
}
|
|
321359
|
+
function summarizeUnexercisedPrimaries(ledger, routeInScope, cap = 12) {
|
|
321360
|
+
const out = [];
|
|
321361
|
+
for (const e of getBlockingActionDebt(ledger, routeInScope)) {
|
|
321362
|
+
if (e.status !== "DETECTED" && e.status !== "ATTEMPTED")
|
|
321363
|
+
continue;
|
|
321364
|
+
out.push({ label: e.label, route: e.route, containerScope: e.containerScope, status: e.status });
|
|
321365
|
+
if (out.length >= cap)
|
|
321366
|
+
break;
|
|
321367
|
+
}
|
|
321368
|
+
return out;
|
|
321369
|
+
}
|
|
321370
|
+
var REGEXP_META_RE = /[.*+?^${}()|[\]\\]/g;
|
|
321371
|
+
function labelCoveredByPlan(normLabel, plannedLower) {
|
|
321372
|
+
if (!normLabel.includes(" "))
|
|
321373
|
+
return false;
|
|
321374
|
+
const escaped = normLabel.replace(REGEXP_META_RE, "\\$&");
|
|
321375
|
+
return new RegExp(`(^|\\W)${escaped}($|\\W)`).test(plannedLower);
|
|
321376
|
+
}
|
|
321377
|
+
function buildUnexercisedPrimaryCoverageGaps(unexercised, plannedScenarioText, nowIso) {
|
|
321378
|
+
const gaps = [];
|
|
321379
|
+
const seen = /* @__PURE__ */ new Set();
|
|
321380
|
+
const planned = plannedScenarioText.toLowerCase();
|
|
321381
|
+
for (const a of unexercised) {
|
|
321382
|
+
const norm = normLabelKey(a.label);
|
|
321383
|
+
const key = `${norm}|${a.route}`;
|
|
321384
|
+
if (seen.has(key))
|
|
321385
|
+
continue;
|
|
321386
|
+
seen.add(key);
|
|
321387
|
+
if (labelCoveredByPlan(norm, planned))
|
|
321388
|
+
continue;
|
|
321389
|
+
const clickedNote = a.status === "ATTEMPTED" ? "clicked, but no result state was captured" : "never clicked";
|
|
321390
|
+
gaps.push({
|
|
321391
|
+
scenarioName: `Exercise "${a.label}"`,
|
|
321392
|
+
reason: `Primary action "${a.label}" was detected on ${a.route} but never exercised to an observed result (${clickedNote}).`,
|
|
321393
|
+
suggestion: `Exercise "${a.label}" to a captured result \u2014 open its result modal/panel and capture it BEFORE closing \u2014 or schedule a deep run scoped to this action, so a real test can be authored.`,
|
|
321394
|
+
unmetPrerequisites: [],
|
|
321395
|
+
detectedAt: nowIso
|
|
321396
|
+
});
|
|
321397
|
+
}
|
|
321398
|
+
return gaps;
|
|
321399
|
+
}
|
|
321350
321400
|
}
|
|
321351
321401
|
});
|
|
321352
321402
|
|
|
@@ -322835,6 +322885,9 @@ var require_exploration_prompt_builder = __commonJS({
|
|
|
322835
322885
|
"These are the feature's core actions \u2014 what it EXISTS to do. You are on a PROVISIONED TEST ACCOUNT, so exercising them is the point of this exploration. For each: click it, wait for it to finish (an async job can take 10-60s \u2014 keep waiting and capturing, do not give up early), then capture_page the result (a new panel, row, score, status, or route change).",
|
|
322836
322886
|
`Spending in-app COMPUTE credits is expected and budgeted \u2014 running an Analyze / Generate / Run / Process ONCE to observe its output is exactly what you should do; an "N of M used" usage or AI-credit meter is NOT a reason to skip. These were auto-screened, but YOU are the final check: if you can see a specific listed action actually charges REAL money at a checkout (Buy / Upgrade / Subscribe), makes an irreversible or destructive change (Delete / Wipe / Deploy to production), or sends a message to real people (Send email / Invite), SKIP just that one \u2014 note it as blocked with the reason and move on. Don't finish until every other action below has produced an observed outcome:`
|
|
322837
322887
|
];
|
|
322888
|
+
if ((0, primary_action_ledger_js_1.isPrimaryActionCoverageGapEnabled)()) {
|
|
322889
|
+
out.push("If the result opens in a modal, dialog, or panel, call capture_page WHILE IT IS STILL OPEN \u2014 do not close or dismiss it first, or the result state is never recorded and the action does not count as covered. An action reachable only from inside this feature's own result modal is in-scope: exercise it to a captured result before closing, even if it generates content that belongs to another builder.");
|
|
322890
|
+
}
|
|
322838
322891
|
for (const e of debt.slice(0, 8)) {
|
|
322839
322892
|
const note = e.status === "ATTEMPTED" ? " \u2014 clicked but no result captured yet" : "";
|
|
322840
322893
|
out.push(` - "${e.label}" on ${e.route}${note}`);
|
|
@@ -326655,6 +326708,7 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
326655
326708
|
const primaryActionMode = (0, primary_action_ledger_js_1.getPrimaryActionGateMode)();
|
|
326656
326709
|
const primaryActionLedgerActive = (0, primary_action_ledger_js_1.isLedgerActive)(primaryActionMode) && options?.mode === "deep";
|
|
326657
326710
|
const primaryActionEnforcing = primaryActionLedgerActive && (0, primary_action_ledger_js_1.isGateEnforcing)(primaryActionMode);
|
|
326711
|
+
const primaryActionCoverageGapEnabled = primaryActionLedgerActive && (0, primary_action_ledger_js_1.isPrimaryActionCoverageGapEnabled)();
|
|
326658
326712
|
const prevRouteSignatures = /* @__PURE__ */ new Map();
|
|
326659
326713
|
const prevInteractionCountsByRoute = /* @__PURE__ */ new Map();
|
|
326660
326714
|
let previousCaptureRoute = null;
|
|
@@ -326665,6 +326719,16 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
326665
326719
|
let finishBounce = (0, survey_link_frontier_js_1.initialFinishBounceState)();
|
|
326666
326720
|
let lastLandedPageUrl = null;
|
|
326667
326721
|
const state2 = (0, exploration_state_js_1.createExplorationState)();
|
|
326722
|
+
const buildUnexercisedPrimarySummary = () => {
|
|
326723
|
+
if (!primaryActionCoverageGapEnabled)
|
|
326724
|
+
return void 0;
|
|
326725
|
+
const fa = options?.focusAreas ?? [];
|
|
326726
|
+
if (fa.length === 0)
|
|
326727
|
+
return void 0;
|
|
326728
|
+
const otherRoutes = options?.featureContext?.otherFeatures?.flatMap((f) => f.pages);
|
|
326729
|
+
const summary2 = (0, primary_action_ledger_js_1.summarizeUnexercisedPrimaries)(state2.actionLedger, (r) => (0, exploration_parser_js_1.routeIsWithinFocusAreas)(r, fa, otherRoutes), 12);
|
|
326730
|
+
return summary2.length > 0 ? summary2 : void 0;
|
|
326731
|
+
};
|
|
326668
326732
|
function ingestLinksFromText(rawText) {
|
|
326669
326733
|
if (!linkFrontierEnabled || !rawText || !rawText.includes("- /url:"))
|
|
326670
326734
|
return;
|
|
@@ -328895,6 +328959,7 @@ Exploration ended after ${iteration} iterations without calling finish_explorati
|
|
|
328895
328959
|
summary: state2.summary || summary,
|
|
328896
328960
|
collectedPages: state2.collectedPages,
|
|
328897
328961
|
collectedTransitions: state2.collectedTransitions,
|
|
328962
|
+
unexercisedPrimaryActions: buildUnexercisedPrimarySummary(),
|
|
328898
328963
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0
|
|
328899
328964
|
};
|
|
328900
328965
|
}
|
|
@@ -328989,6 +329054,7 @@ Fatal error: ${msg}`);
|
|
|
328989
329054
|
summary: `Fatal error: ${msg}`,
|
|
328990
329055
|
collectedPages: state2.collectedPages,
|
|
328991
329056
|
collectedTransitions: state2.collectedTransitions,
|
|
329057
|
+
unexercisedPrimaryActions: buildUnexercisedPrimarySummary(),
|
|
328992
329058
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
|
|
328993
329059
|
truncated: true,
|
|
328994
329060
|
truncatedReason,
|
|
@@ -337175,6 +337241,7 @@ var require_discover_agent = __commonJS({
|
|
|
337175
337241
|
var discover_planner_js_1 = require_discover_planner();
|
|
337176
337242
|
var discover_generator_js_1 = require_discover_generator();
|
|
337177
337243
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
337244
|
+
var primary_action_ledger_js_1 = require_primary_action_ledger();
|
|
337178
337245
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
337179
337246
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
337180
337247
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
@@ -338477,6 +338544,16 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
|
|
|
338477
338544
|
checkpointFallbackReason = planResult.fallbackReason;
|
|
338478
338545
|
plannerWarning = planResult.warning;
|
|
338479
338546
|
plannerCoverageGaps = planResult.coverageGaps;
|
|
338547
|
+
const unexercisedPrimaries = exploreResult.unexercisedPrimaryActions ?? [];
|
|
338548
|
+
if (unexercisedPrimaries.length > 0) {
|
|
338549
|
+
const plannedText = planResult.scenarios.map((s) => `${s.name} ${s.goal ?? ""} ${(s.targetPages ?? []).join(" ")}`).join(" ");
|
|
338550
|
+
const ledgerGaps = (0, primary_action_ledger_js_1.buildUnexercisedPrimaryCoverageGaps)(unexercisedPrimaries, plannedText, (/* @__PURE__ */ new Date()).toISOString());
|
|
338551
|
+
if (ledgerGaps.length > 0) {
|
|
338552
|
+
plannerCoverageGaps = [...plannerCoverageGaps ?? [], ...ledgerGaps];
|
|
338553
|
+
}
|
|
338554
|
+
const suppressed = unexercisedPrimaries.length - ledgerGaps.length;
|
|
338555
|
+
logs2.push(` [primary-action-ledger] ${unexercisedPrimaries.length} in-scope primary action(s) un-exercised; surfaced ${ledgerGaps.length} as coverage gap(s)${suppressed > 0 ? ` (${suppressed} already covered by the plan / deduped)` : ""}`);
|
|
338556
|
+
}
|
|
338480
338557
|
}
|
|
338481
338558
|
if (callbackOptions?.onPlanReady) {
|
|
338482
338559
|
try {
|
|
@@ -353260,7 +353337,7 @@ var require_package4 = __commonJS({
|
|
|
353260
353337
|
"package.json"(exports2, module2) {
|
|
353261
353338
|
module2.exports = {
|
|
353262
353339
|
name: "@validate.qa/runner",
|
|
353263
|
-
version: "1.0.
|
|
353340
|
+
version: "1.0.80",
|
|
353264
353341
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353265
353342
|
bin: {
|
|
353266
353343
|
"validate-runner": "dist/cli.js"
|
|
@@ -353529,6 +353606,9 @@ async function enforceRunnerUpToDate(currentVersion, opts = {}) {
|
|
|
353529
353606
|
return result;
|
|
353530
353607
|
}
|
|
353531
353608
|
|
|
353609
|
+
// src/runner.ts
|
|
353610
|
+
init_dist();
|
|
353611
|
+
|
|
353532
353612
|
// src/services/appium-executor.ts
|
|
353533
353613
|
init_dist();
|
|
353534
353614
|
|
|
@@ -358890,6 +358970,7 @@ function startMjpegStream(options) {
|
|
|
358890
358970
|
// src/services/mobile-bridge.ts
|
|
358891
358971
|
init_dist();
|
|
358892
358972
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
358973
|
+
var RELAY_DEMAND_POLL_MS = 2e3;
|
|
358893
358974
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
358894
358975
|
var MAX_TEXT_INPUT_LENGTH = 4e3;
|
|
358895
358976
|
var MAX_APP_ID_LENGTH = 255;
|
|
@@ -359401,6 +359482,18 @@ async function handleSessionStop(_req, res) {
|
|
|
359401
359482
|
sendJSON(res, 200, { ok: true });
|
|
359402
359483
|
}
|
|
359403
359484
|
var runnerChannels = /* @__PURE__ */ new Map();
|
|
359485
|
+
var relayDemandTimer = null;
|
|
359486
|
+
function setMirrorRelay(deviceId, relay) {
|
|
359487
|
+
const ch = runnerChannels.get(deviceId);
|
|
359488
|
+
if (!ch) return;
|
|
359489
|
+
ch.relay = relay;
|
|
359490
|
+
if (relay) {
|
|
359491
|
+
ensureRelayDemandSupervisor();
|
|
359492
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359493
|
+
} else {
|
|
359494
|
+
stopChannelCaptureIfIdle(ch);
|
|
359495
|
+
}
|
|
359496
|
+
}
|
|
359404
359497
|
function readDeviceIdParam(req) {
|
|
359405
359498
|
try {
|
|
359406
359499
|
const url = new URL(req.url || "", "http://localhost");
|
|
@@ -359432,7 +359525,7 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359432
359525
|
} catch {
|
|
359433
359526
|
}
|
|
359434
359527
|
ch.clients.delete(client);
|
|
359435
|
-
|
|
359528
|
+
stopChannelCaptureIfIdle(ch);
|
|
359436
359529
|
return;
|
|
359437
359530
|
}
|
|
359438
359531
|
client.write(`data: ${JSON.stringify(frame)}
|
|
@@ -359444,6 +359537,12 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359444
359537
|
function broadcastChannelFrame(ch, frame) {
|
|
359445
359538
|
ch.lastFrame = frame;
|
|
359446
359539
|
for (const client of ch.clients) writeFrameToChannelClient(ch, client, frame);
|
|
359540
|
+
if (ch.relay && frame.screenshot) {
|
|
359541
|
+
try {
|
|
359542
|
+
ch.relay.onFrame(frame.screenshot, frame.mimeType ?? "image/png");
|
|
359543
|
+
} catch {
|
|
359544
|
+
}
|
|
359545
|
+
}
|
|
359447
359546
|
}
|
|
359448
359547
|
function tryStartChannelMjpeg(ch) {
|
|
359449
359548
|
if (ch.mjpeg || ch.mjpegStarting || ch.mjpegUnavailable) return;
|
|
@@ -359481,7 +359580,7 @@ function tryStartChannelMjpeg(ch) {
|
|
|
359481
359580
|
ch.mjpeg = null;
|
|
359482
359581
|
ch.mjpegStarting = false;
|
|
359483
359582
|
ch.mjpegUnavailable = true;
|
|
359484
|
-
if (wasActive && ch
|
|
359583
|
+
if (wasActive && hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359485
359584
|
}
|
|
359486
359585
|
});
|
|
359487
359586
|
const firstFrameDeadline = setTimeout(() => {
|
|
@@ -359489,12 +359588,35 @@ function tryStartChannelMjpeg(ch) {
|
|
|
359489
359588
|
ch.mjpegStarting = false;
|
|
359490
359589
|
ch.mjpegUnavailable = true;
|
|
359491
359590
|
handle.stop();
|
|
359492
|
-
if (ch
|
|
359591
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359493
359592
|
}, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS);
|
|
359494
359593
|
firstFrameDeadline.unref?.();
|
|
359495
359594
|
}
|
|
359595
|
+
function hasChannelDemand(ch) {
|
|
359596
|
+
if (ch.clients.size > 0) return true;
|
|
359597
|
+
if (!ch.relay) return false;
|
|
359598
|
+
try {
|
|
359599
|
+
return ch.relay.wanted();
|
|
359600
|
+
} catch {
|
|
359601
|
+
return false;
|
|
359602
|
+
}
|
|
359603
|
+
}
|
|
359604
|
+
function ensureRelayDemandSupervisor() {
|
|
359605
|
+
if (relayDemandTimer) return;
|
|
359606
|
+
relayDemandTimer = setInterval(() => {
|
|
359607
|
+
for (const ch of [...runnerChannels.values()]) {
|
|
359608
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359609
|
+
else stopChannelCaptureIfIdle(ch);
|
|
359610
|
+
}
|
|
359611
|
+
if (runnerChannels.size === 0 && relayDemandTimer) {
|
|
359612
|
+
clearInterval(relayDemandTimer);
|
|
359613
|
+
relayDemandTimer = null;
|
|
359614
|
+
}
|
|
359615
|
+
}, RELAY_DEMAND_POLL_MS);
|
|
359616
|
+
relayDemandTimer.unref?.();
|
|
359617
|
+
}
|
|
359496
359618
|
async function pollRunnerChannel(ch) {
|
|
359497
|
-
if (ch
|
|
359619
|
+
if (!hasChannelDemand(ch) || ch.polling) return;
|
|
359498
359620
|
if (ch.mjpeg) return;
|
|
359499
359621
|
ch.polling = true;
|
|
359500
359622
|
try {
|
|
@@ -359525,6 +359647,7 @@ async function pollRunnerChannel(ch) {
|
|
|
359525
359647
|
}
|
|
359526
359648
|
}
|
|
359527
359649
|
function ensureChannelPolling(ch) {
|
|
359650
|
+
if (!hasChannelDemand(ch)) return;
|
|
359528
359651
|
tryStartChannelMjpeg(ch);
|
|
359529
359652
|
if (ch.mjpeg || ch.pollTimer) return;
|
|
359530
359653
|
ch.pollTimer = setInterval(() => {
|
|
@@ -359532,8 +359655,8 @@ function ensureChannelPolling(ch) {
|
|
|
359532
359655
|
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
359533
359656
|
ch.pollTimer.unref?.();
|
|
359534
359657
|
}
|
|
359535
|
-
function
|
|
359536
|
-
if (ch
|
|
359658
|
+
function stopChannelCaptureIfIdle(ch) {
|
|
359659
|
+
if (hasChannelDemand(ch)) return;
|
|
359537
359660
|
if (ch.pollTimer) {
|
|
359538
359661
|
clearInterval(ch.pollTimer);
|
|
359539
359662
|
ch.pollTimer = null;
|
|
@@ -359552,6 +359675,7 @@ function closeRunnerChannel(ch) {
|
|
|
359552
359675
|
ch.mjpeg.stop();
|
|
359553
359676
|
ch.mjpeg = null;
|
|
359554
359677
|
}
|
|
359678
|
+
ch.relay = null;
|
|
359555
359679
|
for (const client of ch.clients) {
|
|
359556
359680
|
try {
|
|
359557
359681
|
client.end();
|
|
@@ -359560,7 +359684,7 @@ function closeRunnerChannel(ch) {
|
|
|
359560
359684
|
}
|
|
359561
359685
|
ch.clients.clear();
|
|
359562
359686
|
ch.lastFrame = null;
|
|
359563
|
-
runnerChannels.delete(ch.deviceId);
|
|
359687
|
+
if (runnerChannels.get(ch.deviceId) === ch) runnerChannels.delete(ch.deviceId);
|
|
359564
359688
|
}
|
|
359565
359689
|
function closeAllRunnerChannels() {
|
|
359566
359690
|
for (const ch of [...runnerChannels.values()]) closeRunnerChannel(ch);
|
|
@@ -359582,7 +359706,7 @@ async function serveRunnerMirror(req, res, ch) {
|
|
|
359582
359706
|
}
|
|
359583
359707
|
req.on("close", () => {
|
|
359584
359708
|
ch.clients.delete(res);
|
|
359585
|
-
|
|
359709
|
+
stopChannelCaptureIfIdle(ch);
|
|
359586
359710
|
});
|
|
359587
359711
|
}
|
|
359588
359712
|
async function handleMirror(req, res) {
|
|
@@ -360140,7 +360264,8 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
360140
360264
|
mjpeg: null,
|
|
360141
360265
|
mjpegLastFrameAt: 0,
|
|
360142
360266
|
mjpegStarting: false,
|
|
360143
|
-
mjpegUnavailable: false
|
|
360267
|
+
mjpegUnavailable: false,
|
|
360268
|
+
relay: null
|
|
360144
360269
|
};
|
|
360145
360270
|
ch.sessionId = sessionId;
|
|
360146
360271
|
ch.platform = platform3;
|
|
@@ -360154,7 +360279,7 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
360154
360279
|
runnerChannels.set(deviceId, ch);
|
|
360155
360280
|
void applyMjpegSettings(`/session/${sessionId}`, MOBILE_MJPEG_SETTINGS).catch(() => {
|
|
360156
360281
|
});
|
|
360157
|
-
if (ch
|
|
360282
|
+
if (hasChannelDemand(ch)) {
|
|
360158
360283
|
ensureChannelPolling(ch);
|
|
360159
360284
|
void pollRunnerChannel(ch);
|
|
360160
360285
|
}
|
|
@@ -361126,6 +361251,13 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
361126
361251
|
}));
|
|
361127
361252
|
}
|
|
361128
361253
|
var runnerPackage = require_package4();
|
|
361254
|
+
var MIRROR_RELAY_FRESH_MS = 3e3;
|
|
361255
|
+
var liveFrameWatchedByProject = /* @__PURE__ */ new Map();
|
|
361256
|
+
var liveFrameIdleProbeAt = /* @__PURE__ */ new Map();
|
|
361257
|
+
var LIVE_FRAME_IDLE_PROBE_MAX_ENTRIES = 256;
|
|
361258
|
+
function isLiveFrameWatched(projectId) {
|
|
361259
|
+
return !projectId || liveFrameWatchedByProject.get(projectId) !== false;
|
|
361260
|
+
}
|
|
361129
361261
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
361130
361262
|
var IS_CLOUD = parseBoolFlag(process.env.RUNNER_CLOUD);
|
|
361131
361263
|
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
@@ -361903,6 +362035,22 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
361903
362035
|
testCaseId: run2.testCaseId ?? void 0
|
|
361904
362036
|
});
|
|
361905
362037
|
requestLiveBrowserHeartbeat(true);
|
|
362038
|
+
let lastMirrorRelayFrameAt = 0;
|
|
362039
|
+
const attachMirrorWithRelay = (sessionId, platform3, deviceId) => {
|
|
362040
|
+
attachMirrorSession(sessionId, platform3, deviceId);
|
|
362041
|
+
if (!deviceId) return;
|
|
362042
|
+
setMirrorRelay(deviceId, {
|
|
362043
|
+
onFrame: (base64, mimeType) => {
|
|
362044
|
+
lastMirrorRelayFrameAt = Date.now();
|
|
362045
|
+
liveBrowserHandle.updateSnapshot({
|
|
362046
|
+
thumbnailJpegBase64: base64,
|
|
362047
|
+
thumbnailCapturedAt: Date.now(),
|
|
362048
|
+
mimeType
|
|
362049
|
+
});
|
|
362050
|
+
},
|
|
362051
|
+
wanted: () => isLiveFrameWatched(run2.projectId)
|
|
362052
|
+
});
|
|
362053
|
+
};
|
|
361906
362054
|
const liveBrowserHeartbeatInterval = setInterval(() => {
|
|
361907
362055
|
liveBrowserHandle.patch({});
|
|
361908
362056
|
requestLiveBrowserHeartbeat();
|
|
@@ -362806,7 +362954,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362806
362954
|
deeplink: harvestTarget.deeplink
|
|
362807
362955
|
},
|
|
362808
362956
|
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
362809
|
-
|
|
362957
|
+
attachMirrorWithRelay(sessionId, sessionPlatform, deviceId);
|
|
362810
362958
|
const name = getCachedDeviceSnapshot().find((d) => d.id === deviceId)?.name;
|
|
362811
362959
|
liveBrowserHandle.patch({ deviceId, ...name ? { deviceName: name } : {} });
|
|
362812
362960
|
requestLiveBrowserHeartbeat(true);
|
|
@@ -363276,7 +363424,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363276
363424
|
}
|
|
363277
363425
|
);
|
|
363278
363426
|
mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
|
|
363279
|
-
|
|
363427
|
+
attachMirrorWithRelay(session.sessionId, platform3, session.deviceId);
|
|
363280
363428
|
const driver = new MobileBridgeDriver({
|
|
363281
363429
|
sessionId: session.sessionId,
|
|
363282
363430
|
platform: platform3,
|
|
@@ -363313,7 +363461,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363313
363461
|
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
363314
363462
|
}
|
|
363315
363463
|
session = replacement;
|
|
363316
|
-
|
|
363464
|
+
attachMirrorWithRelay(replacement.sessionId, platform3, replacement.deviceId);
|
|
363317
363465
|
liveBrowserHandle.patch({
|
|
363318
363466
|
deviceId: replacement.deviceId,
|
|
363319
363467
|
deviceName: replacement.deviceName
|
|
@@ -363334,6 +363482,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363334
363482
|
// UI a reliable mobile fallback when the direct bridge SSE is
|
|
363335
363483
|
// still opening or blocked by the user's network/tunnel.
|
|
363336
363484
|
onScreenshot: (base64) => {
|
|
363485
|
+
if (Date.now() - lastMirrorRelayFrameAt < MIRROR_RELAY_FRESH_MS) {
|
|
363486
|
+
liveBrowserHandle.updateSnapshot({ currentUrl: mobileTarget.appId });
|
|
363487
|
+
return;
|
|
363488
|
+
}
|
|
363337
363489
|
liveBrowserHandle.updateSnapshot({
|
|
363338
363490
|
currentUrl: mobileTarget.appId,
|
|
363339
363491
|
thumbnailJpegBase64: base64,
|
|
@@ -363806,7 +363958,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363806
363958
|
deeplink: target.deeplink
|
|
363807
363959
|
});
|
|
363808
363960
|
liveSession = opened;
|
|
363809
|
-
|
|
363961
|
+
attachMirrorWithRelay(opened.sessionId, target.platform, opened.deviceId);
|
|
363810
363962
|
bridgeDriver = new MobileBridgeDriver({
|
|
363811
363963
|
sessionId: opened.sessionId,
|
|
363812
363964
|
platform: target.platform,
|
|
@@ -363996,7 +364148,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363996
364148
|
// Stream this run to its OWN per-device dashboard tile (keyed by the
|
|
363997
364149
|
// leased device id) so N parallel Appium runs each show a live mirror.
|
|
363998
364150
|
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
363999
|
-
|
|
364151
|
+
attachMirrorWithRelay(sessionId, sessionPlatform, deviceId);
|
|
364000
364152
|
requestLiveBrowserHeartbeat(true);
|
|
364001
364153
|
},
|
|
364002
364154
|
onSessionEnding: (deviceId) => {
|
|
@@ -364588,12 +364740,31 @@ async function startRunner(opts) {
|
|
|
364588
364740
|
const publishLiveFrame = (frame) => {
|
|
364589
364741
|
if (!frame.projectId) return;
|
|
364590
364742
|
if (liveFramePushInFlight.has(frame.sessionId)) return;
|
|
364743
|
+
if (!isLiveFrameWatched(frame.projectId)) {
|
|
364744
|
+
const lastProbeAt = liveFrameIdleProbeAt.get(frame.sessionId) ?? 0;
|
|
364745
|
+
if (frame.capturedAt - lastProbeAt < LIVE_FRAME_IDLE_PUSH_INTERVAL_MS) return;
|
|
364746
|
+
if (liveFrameIdleProbeAt.size >= LIVE_FRAME_IDLE_PROBE_MAX_ENTRIES) {
|
|
364747
|
+
const expiredBefore = frame.capturedAt - LIVE_FRAME_IDLE_PUSH_INTERVAL_MS;
|
|
364748
|
+
for (const [sessionId, probedAt] of liveFrameIdleProbeAt) {
|
|
364749
|
+
if (probedAt >= expiredBefore) break;
|
|
364750
|
+
liveFrameIdleProbeAt.delete(sessionId);
|
|
364751
|
+
}
|
|
364752
|
+
}
|
|
364753
|
+
liveFrameIdleProbeAt.set(frame.sessionId, frame.capturedAt);
|
|
364754
|
+
}
|
|
364591
364755
|
liveFramePushInFlight.add(frame.sessionId);
|
|
364756
|
+
const frameProjectId = frame.projectId;
|
|
364592
364757
|
void fetch(`${serverUrl}/api/runner/live-frame`, {
|
|
364593
364758
|
method: "POST",
|
|
364594
364759
|
headers,
|
|
364595
364760
|
body: JSON.stringify(frame),
|
|
364596
364761
|
signal: AbortSignal.timeout(5e3)
|
|
364762
|
+
}).then(async (res) => {
|
|
364763
|
+
if (!res.ok) return;
|
|
364764
|
+
const ack = await res.json().catch(() => null);
|
|
364765
|
+
if (ack && typeof ack.watching === "boolean") {
|
|
364766
|
+
liveFrameWatchedByProject.set(frameProjectId, ack.watching);
|
|
364767
|
+
}
|
|
364597
364768
|
}).catch(() => {
|
|
364598
364769
|
}).finally(() => {
|
|
364599
364770
|
liveFramePushInFlight.delete(frame.sessionId);
|
package/dist/cli.mjs
CHANGED
|
@@ -1912,7 +1912,7 @@ function isValidLiveFrame(frame) {
|
|
|
1912
1912
|
return false;
|
|
1913
1913
|
return true;
|
|
1914
1914
|
}
|
|
1915
|
-
var readEnvInt, LIVE_FRAME_PUSH_MAX_BASE64_BYTES, LIVE_THUMBNAIL_MAX_BASE64_BYTES, LIVE_FRAME_MAX_DIMENSION, LIVE_FRAME_JPEG_QUALITY, LIVE_FRAME_MIN_PUSH_INTERVAL_MS, LIVE_FRAME_UNFOCUSED_INTERVAL_MS, LIVE_FRAME_STALE_MS, LIVE_FRAME_MAX_SESSIONS_PER_PROJECT, MOBILE_MJPEG_SETTINGS, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS, MOBILE_MJPEG_MAX_FRAME_BYTES;
|
|
1915
|
+
var readEnvInt, LIVE_FRAME_PUSH_MAX_BASE64_BYTES, LIVE_THUMBNAIL_MAX_BASE64_BYTES, LIVE_FRAME_MAX_DIMENSION, LIVE_FRAME_JPEG_QUALITY, LIVE_FRAME_MIN_PUSH_INTERVAL_MS, LIVE_FRAME_UNFOCUSED_INTERVAL_MS, LIVE_FRAME_IDLE_PUSH_INTERVAL_MS, LIVE_FRAME_STALE_MS, LIVE_FRAME_MAX_SESSIONS_PER_PROJECT, MOBILE_MJPEG_SETTINGS, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS, MOBILE_MJPEG_MAX_FRAME_BYTES;
|
|
1916
1916
|
var init_live_frame = __esm({
|
|
1917
1917
|
"../shared/dist/live-frame.js"() {
|
|
1918
1918
|
"use strict";
|
|
@@ -1928,6 +1928,7 @@ var init_live_frame = __esm({
|
|
|
1928
1928
|
LIVE_FRAME_JPEG_QUALITY = readEnvInt("LIVE_FRAME_JPEG_QUALITY", 55);
|
|
1929
1929
|
LIVE_FRAME_MIN_PUSH_INTERVAL_MS = readEnvInt("LIVE_FRAME_MIN_PUSH_INTERVAL_MS", 125);
|
|
1930
1930
|
LIVE_FRAME_UNFOCUSED_INTERVAL_MS = readEnvInt("LIVE_FRAME_UNFOCUSED_INTERVAL_MS", 2e3);
|
|
1931
|
+
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS = readEnvInt("LIVE_FRAME_IDLE_PUSH_INTERVAL_MS", 5e3);
|
|
1931
1932
|
LIVE_FRAME_STALE_MS = readEnvInt("LIVE_FRAME_STALE_MS", 3e4);
|
|
1932
1933
|
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT = readEnvInt("LIVE_FRAME_MAX_SESSIONS_PER_PROJECT", 32);
|
|
1933
1934
|
MOBILE_MJPEG_SETTINGS = {
|
|
@@ -6365,6 +6366,7 @@ __export(dist_exports, {
|
|
|
6365
6366
|
GROUP_LABELS: () => GROUP_LABELS,
|
|
6366
6367
|
LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
|
|
6367
6368
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
6369
|
+
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS: () => LIVE_FRAME_IDLE_PUSH_INTERVAL_MS,
|
|
6368
6370
|
LIVE_FRAME_JPEG_QUALITY: () => LIVE_FRAME_JPEG_QUALITY,
|
|
6369
6371
|
LIVE_FRAME_MAX_DIMENSION: () => LIVE_FRAME_MAX_DIMENSION,
|
|
6370
6372
|
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT: () => LIVE_FRAME_MAX_SESSIONS_PER_PROJECT,
|
|
@@ -321056,6 +321058,7 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321056
321058
|
exports.isLedgerActive = isLedgerActive;
|
|
321057
321059
|
exports.isGateEnforcing = isGateEnforcing;
|
|
321058
321060
|
exports.isSteeringActive = isSteeringActive;
|
|
321061
|
+
exports.isPrimaryActionCoverageGapEnabled = isPrimaryActionCoverageGapEnabled;
|
|
321059
321062
|
exports.leadingVerb = leadingVerb;
|
|
321060
321063
|
exports.classifyPrimaryAction = classifyPrimaryAction;
|
|
321061
321064
|
exports.containerScopeOf = containerScopeOf;
|
|
@@ -321070,6 +321073,8 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321070
321073
|
exports.classifyActionOutcome = classifyActionOutcome;
|
|
321071
321074
|
exports.detectPrimaryActions = detectPrimaryActions;
|
|
321072
321075
|
exports.summarizeLedger = summarizeLedger;
|
|
321076
|
+
exports.summarizeUnexercisedPrimaries = summarizeUnexercisedPrimaries;
|
|
321077
|
+
exports.buildUnexercisedPrimaryCoverageGaps = buildUnexercisedPrimaryCoverageGaps;
|
|
321073
321078
|
var reveal_diff_js_1 = require_reveal_diff();
|
|
321074
321079
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
321075
321080
|
function getPrimaryActionGateMode(env = process.env) {
|
|
@@ -321089,6 +321094,10 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321089
321094
|
function isSteeringActive(mode) {
|
|
321090
321095
|
return mode === "advisory" || mode === "enforce";
|
|
321091
321096
|
}
|
|
321097
|
+
function isPrimaryActionCoverageGapEnabled(env = process.env) {
|
|
321098
|
+
const raw = (env.DISCOVERY_PRIMARY_ACTION_COVERAGE_GAP ?? "").trim().toLowerCase();
|
|
321099
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "enabled";
|
|
321100
|
+
}
|
|
321092
321101
|
exports.PRIMARY_ACTION_VERB_RE = /^(run|analyze|analyse|generate|regenerate|process|submit|save|create|send|upload|search|export|import|calculate|recalculate|compute|convert|start|scan|translate|transcribe|render|publish|sync|deploy|enrich|summar(?:ize|ise))\b/i;
|
|
321093
321102
|
exports.AMBIGUOUS_ACTION_VERB_RE = /^(continue|next|apply|go|proceed|confirm|update|add|edit|ok|done|finish|complete|approve|enable|activate)\b/i;
|
|
321094
321103
|
exports.DESTRUCTIVE_ACTION_VERB_RE = /^(delete|remove|archive|reset|disconnect|deactivate|destroy|purge|revoke|discard|drop|wipe|erase|unsubscribe|cancel|clear|trash)\b/i;
|
|
@@ -321352,6 +321361,47 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321352
321361
|
}
|
|
321353
321362
|
return `actions=${c.total} primary{detected=${c.detected} attempted=${c.attempted} captured=${c.captured} blocked=${c.blocked} exempt=${c.exempt}} ambiguous=${c.ambiguous}`;
|
|
321354
321363
|
}
|
|
321364
|
+
function summarizeUnexercisedPrimaries(ledger, routeInScope, cap = 12) {
|
|
321365
|
+
const out = [];
|
|
321366
|
+
for (const e of getBlockingActionDebt(ledger, routeInScope)) {
|
|
321367
|
+
if (e.status !== "DETECTED" && e.status !== "ATTEMPTED")
|
|
321368
|
+
continue;
|
|
321369
|
+
out.push({ label: e.label, route: e.route, containerScope: e.containerScope, status: e.status });
|
|
321370
|
+
if (out.length >= cap)
|
|
321371
|
+
break;
|
|
321372
|
+
}
|
|
321373
|
+
return out;
|
|
321374
|
+
}
|
|
321375
|
+
var REGEXP_META_RE = /[.*+?^${}()|[\]\\]/g;
|
|
321376
|
+
function labelCoveredByPlan(normLabel, plannedLower) {
|
|
321377
|
+
if (!normLabel.includes(" "))
|
|
321378
|
+
return false;
|
|
321379
|
+
const escaped = normLabel.replace(REGEXP_META_RE, "\\$&");
|
|
321380
|
+
return new RegExp(`(^|\\W)${escaped}($|\\W)`).test(plannedLower);
|
|
321381
|
+
}
|
|
321382
|
+
function buildUnexercisedPrimaryCoverageGaps(unexercised, plannedScenarioText, nowIso) {
|
|
321383
|
+
const gaps = [];
|
|
321384
|
+
const seen = /* @__PURE__ */ new Set();
|
|
321385
|
+
const planned = plannedScenarioText.toLowerCase();
|
|
321386
|
+
for (const a of unexercised) {
|
|
321387
|
+
const norm = normLabelKey(a.label);
|
|
321388
|
+
const key = `${norm}|${a.route}`;
|
|
321389
|
+
if (seen.has(key))
|
|
321390
|
+
continue;
|
|
321391
|
+
seen.add(key);
|
|
321392
|
+
if (labelCoveredByPlan(norm, planned))
|
|
321393
|
+
continue;
|
|
321394
|
+
const clickedNote = a.status === "ATTEMPTED" ? "clicked, but no result state was captured" : "never clicked";
|
|
321395
|
+
gaps.push({
|
|
321396
|
+
scenarioName: `Exercise "${a.label}"`,
|
|
321397
|
+
reason: `Primary action "${a.label}" was detected on ${a.route} but never exercised to an observed result (${clickedNote}).`,
|
|
321398
|
+
suggestion: `Exercise "${a.label}" to a captured result \u2014 open its result modal/panel and capture it BEFORE closing \u2014 or schedule a deep run scoped to this action, so a real test can be authored.`,
|
|
321399
|
+
unmetPrerequisites: [],
|
|
321400
|
+
detectedAt: nowIso
|
|
321401
|
+
});
|
|
321402
|
+
}
|
|
321403
|
+
return gaps;
|
|
321404
|
+
}
|
|
321355
321405
|
}
|
|
321356
321406
|
});
|
|
321357
321407
|
|
|
@@ -322840,6 +322890,9 @@ var require_exploration_prompt_builder = __commonJS({
|
|
|
322840
322890
|
"These are the feature's core actions \u2014 what it EXISTS to do. You are on a PROVISIONED TEST ACCOUNT, so exercising them is the point of this exploration. For each: click it, wait for it to finish (an async job can take 10-60s \u2014 keep waiting and capturing, do not give up early), then capture_page the result (a new panel, row, score, status, or route change).",
|
|
322841
322891
|
`Spending in-app COMPUTE credits is expected and budgeted \u2014 running an Analyze / Generate / Run / Process ONCE to observe its output is exactly what you should do; an "N of M used" usage or AI-credit meter is NOT a reason to skip. These were auto-screened, but YOU are the final check: if you can see a specific listed action actually charges REAL money at a checkout (Buy / Upgrade / Subscribe), makes an irreversible or destructive change (Delete / Wipe / Deploy to production), or sends a message to real people (Send email / Invite), SKIP just that one \u2014 note it as blocked with the reason and move on. Don't finish until every other action below has produced an observed outcome:`
|
|
322842
322892
|
];
|
|
322893
|
+
if ((0, primary_action_ledger_js_1.isPrimaryActionCoverageGapEnabled)()) {
|
|
322894
|
+
out.push("If the result opens in a modal, dialog, or panel, call capture_page WHILE IT IS STILL OPEN \u2014 do not close or dismiss it first, or the result state is never recorded and the action does not count as covered. An action reachable only from inside this feature's own result modal is in-scope: exercise it to a captured result before closing, even if it generates content that belongs to another builder.");
|
|
322895
|
+
}
|
|
322843
322896
|
for (const e of debt.slice(0, 8)) {
|
|
322844
322897
|
const note = e.status === "ATTEMPTED" ? " \u2014 clicked but no result captured yet" : "";
|
|
322845
322898
|
out.push(` - "${e.label}" on ${e.route}${note}`);
|
|
@@ -326660,6 +326713,7 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
326660
326713
|
const primaryActionMode = (0, primary_action_ledger_js_1.getPrimaryActionGateMode)();
|
|
326661
326714
|
const primaryActionLedgerActive = (0, primary_action_ledger_js_1.isLedgerActive)(primaryActionMode) && options?.mode === "deep";
|
|
326662
326715
|
const primaryActionEnforcing = primaryActionLedgerActive && (0, primary_action_ledger_js_1.isGateEnforcing)(primaryActionMode);
|
|
326716
|
+
const primaryActionCoverageGapEnabled = primaryActionLedgerActive && (0, primary_action_ledger_js_1.isPrimaryActionCoverageGapEnabled)();
|
|
326663
326717
|
const prevRouteSignatures = /* @__PURE__ */ new Map();
|
|
326664
326718
|
const prevInteractionCountsByRoute = /* @__PURE__ */ new Map();
|
|
326665
326719
|
let previousCaptureRoute = null;
|
|
@@ -326670,6 +326724,16 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
326670
326724
|
let finishBounce = (0, survey_link_frontier_js_1.initialFinishBounceState)();
|
|
326671
326725
|
let lastLandedPageUrl = null;
|
|
326672
326726
|
const state2 = (0, exploration_state_js_1.createExplorationState)();
|
|
326727
|
+
const buildUnexercisedPrimarySummary = () => {
|
|
326728
|
+
if (!primaryActionCoverageGapEnabled)
|
|
326729
|
+
return void 0;
|
|
326730
|
+
const fa = options?.focusAreas ?? [];
|
|
326731
|
+
if (fa.length === 0)
|
|
326732
|
+
return void 0;
|
|
326733
|
+
const otherRoutes = options?.featureContext?.otherFeatures?.flatMap((f) => f.pages);
|
|
326734
|
+
const summary2 = (0, primary_action_ledger_js_1.summarizeUnexercisedPrimaries)(state2.actionLedger, (r) => (0, exploration_parser_js_1.routeIsWithinFocusAreas)(r, fa, otherRoutes), 12);
|
|
326735
|
+
return summary2.length > 0 ? summary2 : void 0;
|
|
326736
|
+
};
|
|
326673
326737
|
function ingestLinksFromText(rawText) {
|
|
326674
326738
|
if (!linkFrontierEnabled || !rawText || !rawText.includes("- /url:"))
|
|
326675
326739
|
return;
|
|
@@ -328900,6 +328964,7 @@ Exploration ended after ${iteration} iterations without calling finish_explorati
|
|
|
328900
328964
|
summary: state2.summary || summary,
|
|
328901
328965
|
collectedPages: state2.collectedPages,
|
|
328902
328966
|
collectedTransitions: state2.collectedTransitions,
|
|
328967
|
+
unexercisedPrimaryActions: buildUnexercisedPrimarySummary(),
|
|
328903
328968
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0
|
|
328904
328969
|
};
|
|
328905
328970
|
}
|
|
@@ -328994,6 +329059,7 @@ Fatal error: ${msg}`);
|
|
|
328994
329059
|
summary: `Fatal error: ${msg}`,
|
|
328995
329060
|
collectedPages: state2.collectedPages,
|
|
328996
329061
|
collectedTransitions: state2.collectedTransitions,
|
|
329062
|
+
unexercisedPrimaryActions: buildUnexercisedPrimarySummary(),
|
|
328997
329063
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
|
|
328998
329064
|
truncated: true,
|
|
328999
329065
|
truncatedReason,
|
|
@@ -337180,6 +337246,7 @@ var require_discover_agent = __commonJS({
|
|
|
337180
337246
|
var discover_planner_js_1 = require_discover_planner();
|
|
337181
337247
|
var discover_generator_js_1 = require_discover_generator();
|
|
337182
337248
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
337249
|
+
var primary_action_ledger_js_1 = require_primary_action_ledger();
|
|
337183
337250
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
337184
337251
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
337185
337252
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
@@ -338482,6 +338549,16 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
|
|
|
338482
338549
|
checkpointFallbackReason = planResult.fallbackReason;
|
|
338483
338550
|
plannerWarning = planResult.warning;
|
|
338484
338551
|
plannerCoverageGaps = planResult.coverageGaps;
|
|
338552
|
+
const unexercisedPrimaries = exploreResult.unexercisedPrimaryActions ?? [];
|
|
338553
|
+
if (unexercisedPrimaries.length > 0) {
|
|
338554
|
+
const plannedText = planResult.scenarios.map((s) => `${s.name} ${s.goal ?? ""} ${(s.targetPages ?? []).join(" ")}`).join(" ");
|
|
338555
|
+
const ledgerGaps = (0, primary_action_ledger_js_1.buildUnexercisedPrimaryCoverageGaps)(unexercisedPrimaries, plannedText, (/* @__PURE__ */ new Date()).toISOString());
|
|
338556
|
+
if (ledgerGaps.length > 0) {
|
|
338557
|
+
plannerCoverageGaps = [...plannerCoverageGaps ?? [], ...ledgerGaps];
|
|
338558
|
+
}
|
|
338559
|
+
const suppressed = unexercisedPrimaries.length - ledgerGaps.length;
|
|
338560
|
+
logs2.push(` [primary-action-ledger] ${unexercisedPrimaries.length} in-scope primary action(s) un-exercised; surfaced ${ledgerGaps.length} as coverage gap(s)${suppressed > 0 ? ` (${suppressed} already covered by the plan / deduped)` : ""}`);
|
|
338561
|
+
}
|
|
338485
338562
|
}
|
|
338486
338563
|
if (callbackOptions?.onPlanReady) {
|
|
338487
338564
|
try {
|
|
@@ -353265,7 +353342,7 @@ var require_package4 = __commonJS({
|
|
|
353265
353342
|
"package.json"(exports, module) {
|
|
353266
353343
|
module.exports = {
|
|
353267
353344
|
name: "@validate.qa/runner",
|
|
353268
|
-
version: "1.0.
|
|
353345
|
+
version: "1.0.80",
|
|
353269
353346
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353270
353347
|
bin: {
|
|
353271
353348
|
"validate-runner": "dist/cli.js"
|
|
@@ -353534,6 +353611,9 @@ async function enforceRunnerUpToDate(currentVersion, opts = {}) {
|
|
|
353534
353611
|
return result;
|
|
353535
353612
|
}
|
|
353536
353613
|
|
|
353614
|
+
// src/runner.ts
|
|
353615
|
+
init_dist();
|
|
353616
|
+
|
|
353537
353617
|
// src/services/appium-executor.ts
|
|
353538
353618
|
init_dist();
|
|
353539
353619
|
|
|
@@ -358898,6 +358978,7 @@ function startMjpegStream(options) {
|
|
|
358898
358978
|
// src/services/mobile-bridge.ts
|
|
358899
358979
|
init_dist();
|
|
358900
358980
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
358981
|
+
var RELAY_DEMAND_POLL_MS = 2e3;
|
|
358901
358982
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
358902
358983
|
var MAX_TEXT_INPUT_LENGTH = 4e3;
|
|
358903
358984
|
var MAX_APP_ID_LENGTH = 255;
|
|
@@ -359409,6 +359490,18 @@ async function handleSessionStop(_req, res) {
|
|
|
359409
359490
|
sendJSON(res, 200, { ok: true });
|
|
359410
359491
|
}
|
|
359411
359492
|
var runnerChannels = /* @__PURE__ */ new Map();
|
|
359493
|
+
var relayDemandTimer = null;
|
|
359494
|
+
function setMirrorRelay(deviceId, relay) {
|
|
359495
|
+
const ch = runnerChannels.get(deviceId);
|
|
359496
|
+
if (!ch) return;
|
|
359497
|
+
ch.relay = relay;
|
|
359498
|
+
if (relay) {
|
|
359499
|
+
ensureRelayDemandSupervisor();
|
|
359500
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359501
|
+
} else {
|
|
359502
|
+
stopChannelCaptureIfIdle(ch);
|
|
359503
|
+
}
|
|
359504
|
+
}
|
|
359412
359505
|
function readDeviceIdParam(req) {
|
|
359413
359506
|
try {
|
|
359414
359507
|
const url = new URL(req.url || "", "http://localhost");
|
|
@@ -359440,7 +359533,7 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359440
359533
|
} catch {
|
|
359441
359534
|
}
|
|
359442
359535
|
ch.clients.delete(client);
|
|
359443
|
-
|
|
359536
|
+
stopChannelCaptureIfIdle(ch);
|
|
359444
359537
|
return;
|
|
359445
359538
|
}
|
|
359446
359539
|
client.write(`data: ${JSON.stringify(frame)}
|
|
@@ -359452,6 +359545,12 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359452
359545
|
function broadcastChannelFrame(ch, frame) {
|
|
359453
359546
|
ch.lastFrame = frame;
|
|
359454
359547
|
for (const client of ch.clients) writeFrameToChannelClient(ch, client, frame);
|
|
359548
|
+
if (ch.relay && frame.screenshot) {
|
|
359549
|
+
try {
|
|
359550
|
+
ch.relay.onFrame(frame.screenshot, frame.mimeType ?? "image/png");
|
|
359551
|
+
} catch {
|
|
359552
|
+
}
|
|
359553
|
+
}
|
|
359455
359554
|
}
|
|
359456
359555
|
function tryStartChannelMjpeg(ch) {
|
|
359457
359556
|
if (ch.mjpeg || ch.mjpegStarting || ch.mjpegUnavailable) return;
|
|
@@ -359489,7 +359588,7 @@ function tryStartChannelMjpeg(ch) {
|
|
|
359489
359588
|
ch.mjpeg = null;
|
|
359490
359589
|
ch.mjpegStarting = false;
|
|
359491
359590
|
ch.mjpegUnavailable = true;
|
|
359492
|
-
if (wasActive && ch
|
|
359591
|
+
if (wasActive && hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359493
359592
|
}
|
|
359494
359593
|
});
|
|
359495
359594
|
const firstFrameDeadline = setTimeout(() => {
|
|
@@ -359497,12 +359596,35 @@ function tryStartChannelMjpeg(ch) {
|
|
|
359497
359596
|
ch.mjpegStarting = false;
|
|
359498
359597
|
ch.mjpegUnavailable = true;
|
|
359499
359598
|
handle.stop();
|
|
359500
|
-
if (ch
|
|
359599
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359501
359600
|
}, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS);
|
|
359502
359601
|
firstFrameDeadline.unref?.();
|
|
359503
359602
|
}
|
|
359603
|
+
function hasChannelDemand(ch) {
|
|
359604
|
+
if (ch.clients.size > 0) return true;
|
|
359605
|
+
if (!ch.relay) return false;
|
|
359606
|
+
try {
|
|
359607
|
+
return ch.relay.wanted();
|
|
359608
|
+
} catch {
|
|
359609
|
+
return false;
|
|
359610
|
+
}
|
|
359611
|
+
}
|
|
359612
|
+
function ensureRelayDemandSupervisor() {
|
|
359613
|
+
if (relayDemandTimer) return;
|
|
359614
|
+
relayDemandTimer = setInterval(() => {
|
|
359615
|
+
for (const ch of [...runnerChannels.values()]) {
|
|
359616
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359617
|
+
else stopChannelCaptureIfIdle(ch);
|
|
359618
|
+
}
|
|
359619
|
+
if (runnerChannels.size === 0 && relayDemandTimer) {
|
|
359620
|
+
clearInterval(relayDemandTimer);
|
|
359621
|
+
relayDemandTimer = null;
|
|
359622
|
+
}
|
|
359623
|
+
}, RELAY_DEMAND_POLL_MS);
|
|
359624
|
+
relayDemandTimer.unref?.();
|
|
359625
|
+
}
|
|
359504
359626
|
async function pollRunnerChannel(ch) {
|
|
359505
|
-
if (ch
|
|
359627
|
+
if (!hasChannelDemand(ch) || ch.polling) return;
|
|
359506
359628
|
if (ch.mjpeg) return;
|
|
359507
359629
|
ch.polling = true;
|
|
359508
359630
|
try {
|
|
@@ -359533,6 +359655,7 @@ async function pollRunnerChannel(ch) {
|
|
|
359533
359655
|
}
|
|
359534
359656
|
}
|
|
359535
359657
|
function ensureChannelPolling(ch) {
|
|
359658
|
+
if (!hasChannelDemand(ch)) return;
|
|
359536
359659
|
tryStartChannelMjpeg(ch);
|
|
359537
359660
|
if (ch.mjpeg || ch.pollTimer) return;
|
|
359538
359661
|
ch.pollTimer = setInterval(() => {
|
|
@@ -359540,8 +359663,8 @@ function ensureChannelPolling(ch) {
|
|
|
359540
359663
|
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
359541
359664
|
ch.pollTimer.unref?.();
|
|
359542
359665
|
}
|
|
359543
|
-
function
|
|
359544
|
-
if (ch
|
|
359666
|
+
function stopChannelCaptureIfIdle(ch) {
|
|
359667
|
+
if (hasChannelDemand(ch)) return;
|
|
359545
359668
|
if (ch.pollTimer) {
|
|
359546
359669
|
clearInterval(ch.pollTimer);
|
|
359547
359670
|
ch.pollTimer = null;
|
|
@@ -359560,6 +359683,7 @@ function closeRunnerChannel(ch) {
|
|
|
359560
359683
|
ch.mjpeg.stop();
|
|
359561
359684
|
ch.mjpeg = null;
|
|
359562
359685
|
}
|
|
359686
|
+
ch.relay = null;
|
|
359563
359687
|
for (const client of ch.clients) {
|
|
359564
359688
|
try {
|
|
359565
359689
|
client.end();
|
|
@@ -359568,7 +359692,7 @@ function closeRunnerChannel(ch) {
|
|
|
359568
359692
|
}
|
|
359569
359693
|
ch.clients.clear();
|
|
359570
359694
|
ch.lastFrame = null;
|
|
359571
|
-
runnerChannels.delete(ch.deviceId);
|
|
359695
|
+
if (runnerChannels.get(ch.deviceId) === ch) runnerChannels.delete(ch.deviceId);
|
|
359572
359696
|
}
|
|
359573
359697
|
function closeAllRunnerChannels() {
|
|
359574
359698
|
for (const ch of [...runnerChannels.values()]) closeRunnerChannel(ch);
|
|
@@ -359590,7 +359714,7 @@ async function serveRunnerMirror(req, res, ch) {
|
|
|
359590
359714
|
}
|
|
359591
359715
|
req.on("close", () => {
|
|
359592
359716
|
ch.clients.delete(res);
|
|
359593
|
-
|
|
359717
|
+
stopChannelCaptureIfIdle(ch);
|
|
359594
359718
|
});
|
|
359595
359719
|
}
|
|
359596
359720
|
async function handleMirror(req, res) {
|
|
@@ -360148,7 +360272,8 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
360148
360272
|
mjpeg: null,
|
|
360149
360273
|
mjpegLastFrameAt: 0,
|
|
360150
360274
|
mjpegStarting: false,
|
|
360151
|
-
mjpegUnavailable: false
|
|
360275
|
+
mjpegUnavailable: false,
|
|
360276
|
+
relay: null
|
|
360152
360277
|
};
|
|
360153
360278
|
ch.sessionId = sessionId;
|
|
360154
360279
|
ch.platform = platform3;
|
|
@@ -360162,7 +360287,7 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
360162
360287
|
runnerChannels.set(deviceId, ch);
|
|
360163
360288
|
void applyMjpegSettings(`/session/${sessionId}`, MOBILE_MJPEG_SETTINGS).catch(() => {
|
|
360164
360289
|
});
|
|
360165
|
-
if (ch
|
|
360290
|
+
if (hasChannelDemand(ch)) {
|
|
360166
360291
|
ensureChannelPolling(ch);
|
|
360167
360292
|
void pollRunnerChannel(ch);
|
|
360168
360293
|
}
|
|
@@ -361134,6 +361259,13 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
361134
361259
|
}));
|
|
361135
361260
|
}
|
|
361136
361261
|
var runnerPackage = require_package4();
|
|
361262
|
+
var MIRROR_RELAY_FRESH_MS = 3e3;
|
|
361263
|
+
var liveFrameWatchedByProject = /* @__PURE__ */ new Map();
|
|
361264
|
+
var liveFrameIdleProbeAt = /* @__PURE__ */ new Map();
|
|
361265
|
+
var LIVE_FRAME_IDLE_PROBE_MAX_ENTRIES = 256;
|
|
361266
|
+
function isLiveFrameWatched(projectId) {
|
|
361267
|
+
return !projectId || liveFrameWatchedByProject.get(projectId) !== false;
|
|
361268
|
+
}
|
|
361137
361269
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
361138
361270
|
var IS_CLOUD = parseBoolFlag(process.env.RUNNER_CLOUD);
|
|
361139
361271
|
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
@@ -361911,6 +362043,22 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
361911
362043
|
testCaseId: run2.testCaseId ?? void 0
|
|
361912
362044
|
});
|
|
361913
362045
|
requestLiveBrowserHeartbeat(true);
|
|
362046
|
+
let lastMirrorRelayFrameAt = 0;
|
|
362047
|
+
const attachMirrorWithRelay = (sessionId, platform3, deviceId) => {
|
|
362048
|
+
attachMirrorSession(sessionId, platform3, deviceId);
|
|
362049
|
+
if (!deviceId) return;
|
|
362050
|
+
setMirrorRelay(deviceId, {
|
|
362051
|
+
onFrame: (base64, mimeType) => {
|
|
362052
|
+
lastMirrorRelayFrameAt = Date.now();
|
|
362053
|
+
liveBrowserHandle.updateSnapshot({
|
|
362054
|
+
thumbnailJpegBase64: base64,
|
|
362055
|
+
thumbnailCapturedAt: Date.now(),
|
|
362056
|
+
mimeType
|
|
362057
|
+
});
|
|
362058
|
+
},
|
|
362059
|
+
wanted: () => isLiveFrameWatched(run2.projectId)
|
|
362060
|
+
});
|
|
362061
|
+
};
|
|
361914
362062
|
const liveBrowserHeartbeatInterval = setInterval(() => {
|
|
361915
362063
|
liveBrowserHandle.patch({});
|
|
361916
362064
|
requestLiveBrowserHeartbeat();
|
|
@@ -362814,7 +362962,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362814
362962
|
deeplink: harvestTarget.deeplink
|
|
362815
362963
|
},
|
|
362816
362964
|
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
362817
|
-
|
|
362965
|
+
attachMirrorWithRelay(sessionId, sessionPlatform, deviceId);
|
|
362818
362966
|
const name = getCachedDeviceSnapshot().find((d) => d.id === deviceId)?.name;
|
|
362819
362967
|
liveBrowserHandle.patch({ deviceId, ...name ? { deviceName: name } : {} });
|
|
362820
362968
|
requestLiveBrowserHeartbeat(true);
|
|
@@ -363284,7 +363432,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363284
363432
|
}
|
|
363285
363433
|
);
|
|
363286
363434
|
mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
|
|
363287
|
-
|
|
363435
|
+
attachMirrorWithRelay(session.sessionId, platform3, session.deviceId);
|
|
363288
363436
|
const driver = new MobileBridgeDriver({
|
|
363289
363437
|
sessionId: session.sessionId,
|
|
363290
363438
|
platform: platform3,
|
|
@@ -363321,7 +363469,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363321
363469
|
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
363322
363470
|
}
|
|
363323
363471
|
session = replacement;
|
|
363324
|
-
|
|
363472
|
+
attachMirrorWithRelay(replacement.sessionId, platform3, replacement.deviceId);
|
|
363325
363473
|
liveBrowserHandle.patch({
|
|
363326
363474
|
deviceId: replacement.deviceId,
|
|
363327
363475
|
deviceName: replacement.deviceName
|
|
@@ -363342,6 +363490,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363342
363490
|
// UI a reliable mobile fallback when the direct bridge SSE is
|
|
363343
363491
|
// still opening or blocked by the user's network/tunnel.
|
|
363344
363492
|
onScreenshot: (base64) => {
|
|
363493
|
+
if (Date.now() - lastMirrorRelayFrameAt < MIRROR_RELAY_FRESH_MS) {
|
|
363494
|
+
liveBrowserHandle.updateSnapshot({ currentUrl: mobileTarget.appId });
|
|
363495
|
+
return;
|
|
363496
|
+
}
|
|
363345
363497
|
liveBrowserHandle.updateSnapshot({
|
|
363346
363498
|
currentUrl: mobileTarget.appId,
|
|
363347
363499
|
thumbnailJpegBase64: base64,
|
|
@@ -363814,7 +363966,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363814
363966
|
deeplink: target.deeplink
|
|
363815
363967
|
});
|
|
363816
363968
|
liveSession = opened;
|
|
363817
|
-
|
|
363969
|
+
attachMirrorWithRelay(opened.sessionId, target.platform, opened.deviceId);
|
|
363818
363970
|
bridgeDriver = new MobileBridgeDriver({
|
|
363819
363971
|
sessionId: opened.sessionId,
|
|
363820
363972
|
platform: target.platform,
|
|
@@ -364004,7 +364156,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
364004
364156
|
// Stream this run to its OWN per-device dashboard tile (keyed by the
|
|
364005
364157
|
// leased device id) so N parallel Appium runs each show a live mirror.
|
|
364006
364158
|
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
364007
|
-
|
|
364159
|
+
attachMirrorWithRelay(sessionId, sessionPlatform, deviceId);
|
|
364008
364160
|
requestLiveBrowserHeartbeat(true);
|
|
364009
364161
|
},
|
|
364010
364162
|
onSessionEnding: (deviceId) => {
|
|
@@ -364596,12 +364748,31 @@ async function startRunner(opts) {
|
|
|
364596
364748
|
const publishLiveFrame = (frame) => {
|
|
364597
364749
|
if (!frame.projectId) return;
|
|
364598
364750
|
if (liveFramePushInFlight.has(frame.sessionId)) return;
|
|
364751
|
+
if (!isLiveFrameWatched(frame.projectId)) {
|
|
364752
|
+
const lastProbeAt = liveFrameIdleProbeAt.get(frame.sessionId) ?? 0;
|
|
364753
|
+
if (frame.capturedAt - lastProbeAt < LIVE_FRAME_IDLE_PUSH_INTERVAL_MS) return;
|
|
364754
|
+
if (liveFrameIdleProbeAt.size >= LIVE_FRAME_IDLE_PROBE_MAX_ENTRIES) {
|
|
364755
|
+
const expiredBefore = frame.capturedAt - LIVE_FRAME_IDLE_PUSH_INTERVAL_MS;
|
|
364756
|
+
for (const [sessionId, probedAt] of liveFrameIdleProbeAt) {
|
|
364757
|
+
if (probedAt >= expiredBefore) break;
|
|
364758
|
+
liveFrameIdleProbeAt.delete(sessionId);
|
|
364759
|
+
}
|
|
364760
|
+
}
|
|
364761
|
+
liveFrameIdleProbeAt.set(frame.sessionId, frame.capturedAt);
|
|
364762
|
+
}
|
|
364599
364763
|
liveFramePushInFlight.add(frame.sessionId);
|
|
364764
|
+
const frameProjectId = frame.projectId;
|
|
364600
364765
|
void fetch(`${serverUrl}/api/runner/live-frame`, {
|
|
364601
364766
|
method: "POST",
|
|
364602
364767
|
headers,
|
|
364603
364768
|
body: JSON.stringify(frame),
|
|
364604
364769
|
signal: AbortSignal.timeout(5e3)
|
|
364770
|
+
}).then(async (res) => {
|
|
364771
|
+
if (!res.ok) return;
|
|
364772
|
+
const ack = await res.json().catch(() => null);
|
|
364773
|
+
if (ack && typeof ack.watching === "boolean") {
|
|
364774
|
+
liveFrameWatchedByProject.set(frameProjectId, ack.watching);
|
|
364775
|
+
}
|
|
364605
364776
|
}).catch(() => {
|
|
364606
364777
|
}).finally(() => {
|
|
364607
364778
|
liveFramePushInFlight.delete(frame.sessionId);
|
package/package.json
CHANGED