@yoqa/runner 0.3.19 → 0.3.20
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/index.js +138 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16801,7 +16801,7 @@ function getCatalogDb() {
|
|
|
16801
16801
|
// package.json
|
|
16802
16802
|
var package_default = {
|
|
16803
16803
|
name: "@yoqa/runner",
|
|
16804
|
-
version: "0.3.
|
|
16804
|
+
version: "0.3.20",
|
|
16805
16805
|
description: "Yoqa local runner \u2014 Appium + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
|
|
16806
16806
|
license: "UNLICENSED",
|
|
16807
16807
|
type: "module",
|
|
@@ -17353,10 +17353,6 @@ var MJPEG_SETTINGS_BASE = {
|
|
|
17353
17353
|
mjpegScalingFactor: 50
|
|
17354
17354
|
};
|
|
17355
17355
|
var openByDeviceId = new Map;
|
|
17356
|
-
var exclusiveReleaseListener = null;
|
|
17357
|
-
function onDeviceSessionExclusiveRelease(listener) {
|
|
17358
|
-
exclusiveReleaseListener = listener;
|
|
17359
|
-
}
|
|
17360
17356
|
var DEAD_SESSION_RE = /session does not exist|invalid session id|no such session|terminated or not started|session is either terminated/i;
|
|
17361
17357
|
|
|
17362
17358
|
class DeadSessionError extends Error {
|
|
@@ -17441,7 +17437,6 @@ async function releaseExistingSession(deviceId) {
|
|
|
17441
17437
|
if (!existing)
|
|
17442
17438
|
return;
|
|
17443
17439
|
openByDeviceId.delete(deviceId);
|
|
17444
|
-
exclusiveReleaseListener?.(deviceId);
|
|
17445
17440
|
try {
|
|
17446
17441
|
await existing.quit();
|
|
17447
17442
|
} catch (error) {
|
|
@@ -17975,21 +17970,32 @@ async function createDeviceSession(options) {
|
|
|
17975
17970
|
|
|
17976
17971
|
// src/domains/devices/active-session.ts
|
|
17977
17972
|
var active = null;
|
|
17973
|
+
|
|
17974
|
+
class SessionBusyError extends Error {
|
|
17975
|
+
constructor(message) {
|
|
17976
|
+
super(message);
|
|
17977
|
+
this.name = "SessionBusyError";
|
|
17978
|
+
}
|
|
17979
|
+
}
|
|
17978
17980
|
var isMissingAppiumSessionError = isDeadSessionError;
|
|
17981
|
+
function toInfo(current) {
|
|
17982
|
+
return {
|
|
17983
|
+
deviceId: current.deviceId,
|
|
17984
|
+
platform: current.platform,
|
|
17985
|
+
connectedAt: current.connectedAt,
|
|
17986
|
+
mjpegPort: current.mjpegPort,
|
|
17987
|
+
streamReady: current.streamReady,
|
|
17988
|
+
streamUrl: current.streamUrl,
|
|
17989
|
+
heldByRun: current.heldByRunId != null
|
|
17990
|
+
};
|
|
17991
|
+
}
|
|
17979
17992
|
function getActiveSession() {
|
|
17980
17993
|
return active;
|
|
17981
17994
|
}
|
|
17982
17995
|
function getActiveSessionInfo() {
|
|
17983
17996
|
if (!active)
|
|
17984
17997
|
return null;
|
|
17985
|
-
return
|
|
17986
|
-
deviceId: active.deviceId,
|
|
17987
|
-
platform: active.platform,
|
|
17988
|
-
connectedAt: active.connectedAt,
|
|
17989
|
-
mjpegPort: active.mjpegPort,
|
|
17990
|
-
streamReady: active.streamReady,
|
|
17991
|
-
streamUrl: active.streamUrl
|
|
17992
|
-
};
|
|
17998
|
+
return toInfo(active);
|
|
17993
17999
|
}
|
|
17994
18000
|
function requireActiveSession() {
|
|
17995
18001
|
if (!active) {
|
|
@@ -17997,6 +18003,9 @@ function requireActiveSession() {
|
|
|
17997
18003
|
}
|
|
17998
18004
|
return active;
|
|
17999
18005
|
}
|
|
18006
|
+
function isActiveSessionHeldByRun() {
|
|
18007
|
+
return active?.heldByRunId != null;
|
|
18008
|
+
}
|
|
18000
18009
|
function abandonActiveSession() {
|
|
18001
18010
|
if (!active)
|
|
18002
18011
|
return null;
|
|
@@ -18006,20 +18015,12 @@ function abandonActiveSession() {
|
|
|
18006
18015
|
console.warn(`[yoqa-runner] abandoned dead Appium session for ${info?.platform} ${info?.deviceId}`);
|
|
18007
18016
|
return info;
|
|
18008
18017
|
}
|
|
18009
|
-
|
|
18010
|
-
if (active?.deviceId === deviceId) {
|
|
18011
|
-
abandonActiveSession();
|
|
18012
|
-
}
|
|
18013
|
-
});
|
|
18014
|
-
async function connectDevice(options) {
|
|
18015
|
-
if (active) {
|
|
18016
|
-
await disconnectDevice();
|
|
18017
|
-
}
|
|
18018
|
+
async function createAndRegister(options) {
|
|
18018
18019
|
const session = await createDeviceSession({
|
|
18019
18020
|
platform: options.platform,
|
|
18020
18021
|
deviceId: options.deviceId,
|
|
18021
|
-
appCaps: [],
|
|
18022
|
-
caseCaps: [],
|
|
18022
|
+
appCaps: options.appCaps ?? [],
|
|
18023
|
+
caseCaps: options.caseCaps ?? [],
|
|
18023
18024
|
bundleId: options.bundleId,
|
|
18024
18025
|
appPackage: options.appPackage,
|
|
18025
18026
|
onSessionDead: () => {
|
|
@@ -18033,8 +18034,19 @@ async function connectDevice(options) {
|
|
|
18033
18034
|
mjpegPort: session.mjpegPort,
|
|
18034
18035
|
streamReady: session.streamReady,
|
|
18035
18036
|
streamUrl: "/stream.mjpeg",
|
|
18036
|
-
session
|
|
18037
|
+
session,
|
|
18038
|
+
heldByRunId: options.heldByRunId
|
|
18037
18039
|
};
|
|
18040
|
+
return session;
|
|
18041
|
+
}
|
|
18042
|
+
async function connectDevice(options) {
|
|
18043
|
+
if (isActiveSessionHeldByRun()) {
|
|
18044
|
+
throw new SessionBusyError("A run is using the device session. Cancel the run before connecting another device.");
|
|
18045
|
+
}
|
|
18046
|
+
if (active) {
|
|
18047
|
+
await disconnectDevice();
|
|
18048
|
+
}
|
|
18049
|
+
await createAndRegister({ ...options, heldByRunId: null });
|
|
18038
18050
|
const info = getActiveSessionInfo();
|
|
18039
18051
|
if (!info) {
|
|
18040
18052
|
throw new Error("Failed to read active session after connect");
|
|
@@ -18044,6 +18056,9 @@ async function connectDevice(options) {
|
|
|
18044
18056
|
async function disconnectDevice() {
|
|
18045
18057
|
if (!active)
|
|
18046
18058
|
return null;
|
|
18059
|
+
if (isActiveSessionHeldByRun()) {
|
|
18060
|
+
throw new SessionBusyError("A run is using the device session. Cancel the run first.");
|
|
18061
|
+
}
|
|
18047
18062
|
const info = getActiveSessionInfo();
|
|
18048
18063
|
const session = active.session;
|
|
18049
18064
|
active = null;
|
|
@@ -18053,6 +18068,58 @@ async function disconnectDevice() {
|
|
|
18053
18068
|
} catch {}
|
|
18054
18069
|
return info;
|
|
18055
18070
|
}
|
|
18071
|
+
async function acquireSessionForRun(options) {
|
|
18072
|
+
const current = active;
|
|
18073
|
+
if (current?.heldByRunId && current.heldByRunId !== options.runId) {
|
|
18074
|
+
const session2 = await createDeviceSession({
|
|
18075
|
+
platform: options.platform,
|
|
18076
|
+
deviceId: options.deviceId,
|
|
18077
|
+
appCaps: options.appCaps ?? [],
|
|
18078
|
+
caseCaps: options.caseCaps ?? [],
|
|
18079
|
+
bundleId: options.bundleId,
|
|
18080
|
+
appPackage: options.appPackage,
|
|
18081
|
+
onSessionDead: () => {
|
|
18082
|
+
return;
|
|
18083
|
+
}
|
|
18084
|
+
});
|
|
18085
|
+
return { session: session2, shared: false };
|
|
18086
|
+
}
|
|
18087
|
+
if (current && current.deviceId === options.deviceId) {
|
|
18088
|
+
const healthy = await current.session.getWindowSize().then(() => true).catch(() => false);
|
|
18089
|
+
if (healthy) {
|
|
18090
|
+
current.heldByRunId = options.runId;
|
|
18091
|
+
return { session: current.session, shared: true };
|
|
18092
|
+
}
|
|
18093
|
+
console.warn(`[yoqa-runner] active session for ${options.deviceId} is dead \u2014 creating a fresh one for the run`);
|
|
18094
|
+
await disconnectDevice().catch(() => {
|
|
18095
|
+
return;
|
|
18096
|
+
});
|
|
18097
|
+
}
|
|
18098
|
+
if (current) {
|
|
18099
|
+
await disconnectDevice();
|
|
18100
|
+
}
|
|
18101
|
+
const session = await createAndRegister({
|
|
18102
|
+
deviceId: options.deviceId,
|
|
18103
|
+
platform: options.platform,
|
|
18104
|
+
appCaps: options.appCaps,
|
|
18105
|
+
caseCaps: options.caseCaps,
|
|
18106
|
+
bundleId: options.bundleId,
|
|
18107
|
+
appPackage: options.appPackage,
|
|
18108
|
+
heldByRunId: options.runId
|
|
18109
|
+
});
|
|
18110
|
+
return { session, shared: true };
|
|
18111
|
+
}
|
|
18112
|
+
async function releaseSessionFromRun(runId, session, shared) {
|
|
18113
|
+
if (!shared) {
|
|
18114
|
+
await session.quit().catch(() => {
|
|
18115
|
+
return;
|
|
18116
|
+
});
|
|
18117
|
+
return;
|
|
18118
|
+
}
|
|
18119
|
+
if (active && active.heldByRunId === runId) {
|
|
18120
|
+
active.heldByRunId = null;
|
|
18121
|
+
}
|
|
18122
|
+
}
|
|
18056
18123
|
|
|
18057
18124
|
// src/domains/servers/application.ts
|
|
18058
18125
|
function appiumLabel(info) {
|
|
@@ -24351,7 +24418,8 @@ var activeDeviceResponseSchema = exports_external.object({
|
|
|
24351
24418
|
connectedAt: exports_external.number().int().nonnegative(),
|
|
24352
24419
|
mjpegPort: exports_external.number().int().positive().optional(),
|
|
24353
24420
|
streamReady: exports_external.boolean().optional(),
|
|
24354
|
-
streamUrl: exports_external.string().min(1).optional()
|
|
24421
|
+
streamUrl: exports_external.string().min(1).optional(),
|
|
24422
|
+
heldByRun: exports_external.boolean().optional()
|
|
24355
24423
|
});
|
|
24356
24424
|
var controlPointerMessageSchema = exports_external.object({
|
|
24357
24425
|
type: exports_external.literal("pointer"),
|
|
@@ -98329,6 +98397,7 @@ async function executeRun(runId) {
|
|
|
98329
98397
|
return;
|
|
98330
98398
|
registerControl(runId);
|
|
98331
98399
|
let session = null;
|
|
98400
|
+
let sharedSession = true;
|
|
98332
98401
|
try {
|
|
98333
98402
|
if (isAborted2(runId)) {
|
|
98334
98403
|
await persistCancelled(runId);
|
|
@@ -98369,14 +98438,17 @@ async function executeRun(runId) {
|
|
|
98369
98438
|
}
|
|
98370
98439
|
}
|
|
98371
98440
|
const firstCase = run.tests[0] ? await getCase(run.tests[0].caseId) : null;
|
|
98372
|
-
|
|
98373
|
-
|
|
98441
|
+
const acquired = await acquireSessionForRun({
|
|
98442
|
+
runId,
|
|
98374
98443
|
deviceId: run.deviceId,
|
|
98444
|
+
platform: run.platform,
|
|
98375
98445
|
appCaps: app.capabilities,
|
|
98376
98446
|
caseCaps: firstCase?.capabilities ?? [],
|
|
98377
98447
|
bundleId: app.iosBundleId || undefined,
|
|
98378
98448
|
appPackage: app.androidApplicationId || undefined
|
|
98379
98449
|
});
|
|
98450
|
+
session = acquired.session;
|
|
98451
|
+
sharedSession = acquired.shared;
|
|
98380
98452
|
if (isAborted2(runId)) {
|
|
98381
98453
|
await persistCancelled(runId);
|
|
98382
98454
|
return;
|
|
@@ -98451,14 +98523,15 @@ async function executeRun(runId) {
|
|
|
98451
98523
|
}
|
|
98452
98524
|
} finally {
|
|
98453
98525
|
if (session) {
|
|
98454
|
-
await
|
|
98455
|
-
return;
|
|
98456
|
-
});
|
|
98526
|
+
await releaseSessionFromRun(runId, session, sharedSession);
|
|
98457
98527
|
}
|
|
98458
98528
|
clearControl(runId);
|
|
98459
98529
|
}
|
|
98460
98530
|
}
|
|
98461
98531
|
async function createRun(input) {
|
|
98532
|
+
if (runControls.size > 0) {
|
|
98533
|
+
throw new RunValidationError("A run is already in progress. Cancel it before starting another.");
|
|
98534
|
+
}
|
|
98462
98535
|
const uniqueCaseIds = [...new Set(input.caseIds)];
|
|
98463
98536
|
if (uniqueCaseIds.length === 0) {
|
|
98464
98537
|
throw new RunValidationError("At least one case id is required");
|
|
@@ -98816,6 +98889,12 @@ import { mkdir as mkdir4, rename as rename2 } from "fs/promises";
|
|
|
98816
98889
|
import { dirname as dirname4 } from "path";
|
|
98817
98890
|
function sessionErrorResponse(error40) {
|
|
98818
98891
|
const message = error40 instanceof Error ? error40.message : String(error40);
|
|
98892
|
+
if (error40 instanceof SessionBusyError) {
|
|
98893
|
+
return {
|
|
98894
|
+
status: 409,
|
|
98895
|
+
body: { error: error40.message }
|
|
98896
|
+
};
|
|
98897
|
+
}
|
|
98819
98898
|
if (isMissingAppiumSessionError(error40)) {
|
|
98820
98899
|
abandonActiveSession();
|
|
98821
98900
|
return {
|
|
@@ -98845,6 +98924,9 @@ function createSessionRoutes() {
|
|
|
98845
98924
|
const info = await connectDevice(parsed.data);
|
|
98846
98925
|
return c.json(activeDeviceResponseSchema.parse(info));
|
|
98847
98926
|
} catch (error40) {
|
|
98927
|
+
const mapped = sessionErrorResponse(error40);
|
|
98928
|
+
if (mapped)
|
|
98929
|
+
return c.json(mapped.body, mapped.status);
|
|
98848
98930
|
const message = error40 instanceof Error ? error40.message : String(error40);
|
|
98849
98931
|
return c.json({ error: "Failed to connect device", detail: message }, 500);
|
|
98850
98932
|
}
|
|
@@ -98857,11 +98939,19 @@ function createSessionRoutes() {
|
|
|
98857
98939
|
return c.json(activeDeviceResponseSchema.parse(info));
|
|
98858
98940
|
});
|
|
98859
98941
|
app.post("/devices/disconnect", async (c) => {
|
|
98860
|
-
|
|
98861
|
-
|
|
98862
|
-
|
|
98942
|
+
try {
|
|
98943
|
+
const info = await disconnectDevice();
|
|
98944
|
+
if (!info) {
|
|
98945
|
+
return c.json({ error: "No active device session" }, 404);
|
|
98946
|
+
}
|
|
98947
|
+
return c.json(activeDeviceResponseSchema.parse(info));
|
|
98948
|
+
} catch (error40) {
|
|
98949
|
+
const mapped = sessionErrorResponse(error40);
|
|
98950
|
+
if (mapped)
|
|
98951
|
+
return c.json(mapped.body, mapped.status);
|
|
98952
|
+
const message = error40 instanceof Error ? error40.message : String(error40);
|
|
98953
|
+
return c.json({ error: "Failed to disconnect device", detail: message }, 500);
|
|
98863
98954
|
}
|
|
98864
|
-
return c.json(activeDeviceResponseSchema.parse(info));
|
|
98865
98955
|
});
|
|
98866
98956
|
app.get("/screen", async (c) => {
|
|
98867
98957
|
try {
|
|
@@ -98982,6 +99072,11 @@ function createSessionRoutes() {
|
|
|
98982
99072
|
}
|
|
98983
99073
|
try {
|
|
98984
99074
|
const { session } = requireActiveSession();
|
|
99075
|
+
if (isActiveSessionHeldByRun()) {
|
|
99076
|
+
return c.json({
|
|
99077
|
+
error: "A run is using this device session. Cancel the run to interact manually."
|
|
99078
|
+
}, 409);
|
|
99079
|
+
}
|
|
98985
99080
|
const result = await performAction(session, parsed.data);
|
|
98986
99081
|
return c.json(actionResponseSchema.parse(result));
|
|
98987
99082
|
} catch (error40) {
|
|
@@ -99110,6 +99205,13 @@ var controlWebSocket = {
|
|
|
99110
99205
|
return;
|
|
99111
99206
|
}
|
|
99112
99207
|
const msg = result.data;
|
|
99208
|
+
if (isActiveSessionHeldByRun()) {
|
|
99209
|
+
ws.send(JSON.stringify({
|
|
99210
|
+
ok: false,
|
|
99211
|
+
error: "A run is using this device session. Cancel the run to interact manually."
|
|
99212
|
+
}));
|
|
99213
|
+
return;
|
|
99214
|
+
}
|
|
99113
99215
|
try {
|
|
99114
99216
|
await active2.session.pointerEvent(msg.phase, msg.x, msg.y, msg.seq);
|
|
99115
99217
|
if (msg.phase === "end" || msg.phase === "begin") {
|
package/package.json
CHANGED