@flowriki/agent 0.1.0-beta.52 → 0.1.0-beta.53
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.
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
nodeAt,
|
|
48
48
|
normalizeStartUrl,
|
|
49
49
|
openMobileSession,
|
|
50
|
+
openSessionOn,
|
|
50
51
|
parseFlowDocument,
|
|
51
52
|
performMobileStep,
|
|
52
53
|
platformsOf,
|
|
@@ -67,7 +68,7 @@ import {
|
|
|
67
68
|
textIndependent,
|
|
68
69
|
validateActors,
|
|
69
70
|
webCloudOf
|
|
70
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-FIR6NOLA.js";
|
|
71
72
|
|
|
72
73
|
// src/engine/machine.ts
|
|
73
74
|
import fs from "fs";
|
|
@@ -75,7 +76,7 @@ import os from "os";
|
|
|
75
76
|
import path from "path";
|
|
76
77
|
import { createRequire } from "module";
|
|
77
78
|
import { spawnSync } from "child_process";
|
|
78
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
79
|
+
var AGENT_VERSION = "0.1.0-beta.53";
|
|
79
80
|
async function prepareBrowsers() {
|
|
80
81
|
if (browsersReady()) {
|
|
81
82
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -135,7 +136,7 @@ async function checkMachine() {
|
|
|
135
136
|
}
|
|
136
137
|
} catch {
|
|
137
138
|
}
|
|
138
|
-
const { mobileDoctor: mobileDoctor2 } = await import("./dist-
|
|
139
|
+
const { mobileDoctor: mobileDoctor2 } = await import("./dist-6GKVE5CL.js");
|
|
139
140
|
return {
|
|
140
141
|
agentVersion: AGENT_VERSION,
|
|
141
142
|
node: process.versions.node,
|
|
@@ -234,7 +235,7 @@ function forgetConnection(service, env = process.env) {
|
|
|
234
235
|
}
|
|
235
236
|
var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
|
|
236
237
|
|
|
237
|
-
// ../contracts/dist/chunk-
|
|
238
|
+
// ../contracts/dist/chunk-NTBXG5EZ.js
|
|
238
239
|
import { z } from "zod";
|
|
239
240
|
var PROTOCOL_VERSION = 1;
|
|
240
241
|
var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -283,6 +284,9 @@ var CommandNameSchema = z.enum([
|
|
|
283
284
|
// Reading a live phone session: its screen as a picture, and what is under
|
|
284
285
|
// a point. Not effectful — a picture is far too big to keep as a result.
|
|
285
286
|
"mobile.screen",
|
|
287
|
+
// The screen of whatever phone session is open on a platform — a run's,
|
|
288
|
+
// a recording's — as a picture, for the live mirror. Reading.
|
|
289
|
+
"mobile.peek",
|
|
286
290
|
// What this computer has installed for web and phone testing. Reading.
|
|
287
291
|
"machine.check"
|
|
288
292
|
]);
|
|
@@ -3962,7 +3966,7 @@ async function replayCase(opts) {
|
|
|
3962
3966
|
let status2 = "passed";
|
|
3963
3967
|
let error = null;
|
|
3964
3968
|
const browserRows = occurrences.filter((o) => o.platform !== "android" && o.platform !== "ios");
|
|
3965
|
-
const
|
|
3969
|
+
const laneOf2 = new Map(sessionLanes(browserRows, opts.defaultActorId).map((plan, i) => [browserRows[i].occurrenceId, plan]));
|
|
3966
3970
|
try {
|
|
3967
3971
|
for (const [index, occurrence] of occurrences.entries()) {
|
|
3968
3972
|
if (status2 !== "passed" || opts.signal?.aborted) {
|
|
@@ -4014,7 +4018,7 @@ async function replayCase(opts) {
|
|
|
4014
4018
|
const navigates = navigatesFirst(occurrence);
|
|
4015
4019
|
const who = occurrence.actorId ?? opts.defaultActorId ?? "default";
|
|
4016
4020
|
const entry = resolveStartUrl(occurrence.baseUrl ?? baseUrl, occurrence.startPath ?? "/");
|
|
4017
|
-
const plan =
|
|
4021
|
+
const plan = laneOf2.get(occurrence.occurrenceId);
|
|
4018
4022
|
try {
|
|
4019
4023
|
if (plan.opens && startOf(occurrence) === "fresh") await actors.restart(who, entry);
|
|
4020
4024
|
else if (plan.opens) await actors.select(who, entry);
|
|
@@ -7990,6 +7994,105 @@ var BrowserBusy = class extends Error {
|
|
|
7990
7994
|
this.name = "BrowserBusy";
|
|
7991
7995
|
}
|
|
7992
7996
|
};
|
|
7997
|
+
var LANES = ["web", "android", "ios"];
|
|
7998
|
+
var OneLane = class {
|
|
7999
|
+
owner = null;
|
|
8000
|
+
phase = "idle";
|
|
8001
|
+
since = null;
|
|
8002
|
+
/** Take the lane, or report who has it. Never awaits — that is the point. */
|
|
8003
|
+
acquire(owner) {
|
|
8004
|
+
if (this.owner !== null) {
|
|
8005
|
+
return { ok: false, heldBy: this.owner, phase: this.phase };
|
|
8006
|
+
}
|
|
8007
|
+
this.owner = owner;
|
|
8008
|
+
this.phase = "starting";
|
|
8009
|
+
this.since = Date.now();
|
|
8010
|
+
return { ok: true };
|
|
8011
|
+
}
|
|
8012
|
+
/** The holder finished starting and is now doing the thing. */
|
|
8013
|
+
activate(owner) {
|
|
8014
|
+
if (this.owner === owner) this.phase = "active";
|
|
8015
|
+
}
|
|
8016
|
+
/** The holder has begun shutting down. Still not idle: a second stop must
|
|
8017
|
+
* join the first rather than starting its own. */
|
|
8018
|
+
beginRelease(owner) {
|
|
8019
|
+
if (this.owner !== owner || this.phase === "stopping") return false;
|
|
8020
|
+
this.phase = "stopping";
|
|
8021
|
+
return true;
|
|
8022
|
+
}
|
|
8023
|
+
release(owner) {
|
|
8024
|
+
if (this.owner !== owner) return;
|
|
8025
|
+
this.owner = null;
|
|
8026
|
+
this.phase = "idle";
|
|
8027
|
+
this.since = null;
|
|
8028
|
+
}
|
|
8029
|
+
forceRelease() {
|
|
8030
|
+
this.owner = null;
|
|
8031
|
+
this.phase = "idle";
|
|
8032
|
+
this.since = null;
|
|
8033
|
+
}
|
|
8034
|
+
state() {
|
|
8035
|
+
return { owner: this.owner, phase: this.phase, since: this.since };
|
|
8036
|
+
}
|
|
8037
|
+
};
|
|
8038
|
+
var BrowserLease = class {
|
|
8039
|
+
lanes = { web: new OneLane(), android: new OneLane(), ios: new OneLane() };
|
|
8040
|
+
/** The web lane's methods, as they always were: the browser coordinators
|
|
8041
|
+
* drive one platform and need not know about lanes. */
|
|
8042
|
+
acquire(owner, lane = "web") {
|
|
8043
|
+
return this.lanes[lane].acquire(owner);
|
|
8044
|
+
}
|
|
8045
|
+
activate(owner, lane = "web") {
|
|
8046
|
+
this.lanes[lane].activate(owner);
|
|
8047
|
+
}
|
|
8048
|
+
beginRelease(owner, lane = "web") {
|
|
8049
|
+
return this.lanes[lane].beginRelease(owner);
|
|
8050
|
+
}
|
|
8051
|
+
release(owner, lane = "web") {
|
|
8052
|
+
this.lanes[lane].release(owner);
|
|
8053
|
+
}
|
|
8054
|
+
state(lane = "web") {
|
|
8055
|
+
return this.lanes[lane].state();
|
|
8056
|
+
}
|
|
8057
|
+
/** One lane, bound: what a coordinator for that platform is handed. */
|
|
8058
|
+
lane(lane) {
|
|
8059
|
+
return this.lanes[lane];
|
|
8060
|
+
}
|
|
8061
|
+
/** Take several lanes at once — all or none, so a job on two platforms
|
|
8062
|
+
* never holds one while refused the other. */
|
|
8063
|
+
acquireAll(owner, lanes) {
|
|
8064
|
+
const taken = [];
|
|
8065
|
+
for (const lane of new Set(lanes)) {
|
|
8066
|
+
const claim = this.lanes[lane].acquire(owner);
|
|
8067
|
+
if (!claim.ok) {
|
|
8068
|
+
for (const had of taken) this.lanes[had].release(owner);
|
|
8069
|
+
return { ok: false, lane, heldBy: claim.heldBy, phase: claim.phase };
|
|
8070
|
+
}
|
|
8071
|
+
taken.push(lane);
|
|
8072
|
+
}
|
|
8073
|
+
return { ok: true };
|
|
8074
|
+
}
|
|
8075
|
+
activateAll(owner, lanes) {
|
|
8076
|
+
for (const lane of new Set(lanes)) this.lanes[lane].activate(owner);
|
|
8077
|
+
}
|
|
8078
|
+
releaseAll(owner, lanes) {
|
|
8079
|
+
for (const lane of new Set(lanes)) this.lanes[lane].release(owner);
|
|
8080
|
+
}
|
|
8081
|
+
/** Drop every lane whoever holds it — shutdown, and nothing else. */
|
|
8082
|
+
forceRelease() {
|
|
8083
|
+
for (const lane of LANES) this.lanes[lane].forceRelease();
|
|
8084
|
+
}
|
|
8085
|
+
/** The sentence a refused caller is shown. Names what is happening and on
|
|
8086
|
+
* what, because "busy" leaves someone hunting for a window they cannot find. */
|
|
8087
|
+
static refusal(heldBy, phase, lane = "web") {
|
|
8088
|
+
const what = heldBy === "recording" ? "a recording" : heldBy === "debug" ? "a debug session" : "a run";
|
|
8089
|
+
const where = lane === "web" ? "the browser" : lane === "android" ? "the Android phone" : "the iPhone";
|
|
8090
|
+
if (phase === "starting") return `${what} is starting on ${where} \u2014 wait for it to open, or stop it first`;
|
|
8091
|
+
if (phase === "stopping") return `${what} is still closing on ${where} \u2014 try again in a moment`;
|
|
8092
|
+
return `${what} is already using ${where} \u2014 stop it before starting another`;
|
|
8093
|
+
}
|
|
8094
|
+
};
|
|
8095
|
+
var laneOf = (platform) => platform === "android" || platform === "ios" ? platform : "web";
|
|
7993
8096
|
var COMMANDS = /* @__PURE__ */ new Set([
|
|
7994
8097
|
"play",
|
|
7995
8098
|
"pause",
|
|
@@ -8166,11 +8269,12 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8166
8269
|
const steps = (opts.steps ?? flow.steps).map((s2) => DraftStepSchema.parse(s2));
|
|
8167
8270
|
if (!opts.verifyIds.length) return { results: [], error: null, failedAtStepId: null, ok: true };
|
|
8168
8271
|
const baseUrl = store.baseUrlFor(flow);
|
|
8169
|
-
const
|
|
8272
|
+
const lane = laneOf(flow.platform);
|
|
8273
|
+
const claim = deps.lease?.acquire("run", lane);
|
|
8170
8274
|
if (claim && !claim.ok) {
|
|
8171
|
-
throw new BrowserBusy(
|
|
8275
|
+
throw new BrowserBusy(`${lane === "web" ? "the browser" : lane === "android" ? "the Android phone" : "the iPhone"} on this machine is in use (${claim.heldBy}, ${claim.phase})`);
|
|
8172
8276
|
}
|
|
8173
|
-
deps.lease?.activate("run");
|
|
8277
|
+
deps.lease?.activate("run", lane);
|
|
8174
8278
|
try {
|
|
8175
8279
|
const secrets = store.readSecrets();
|
|
8176
8280
|
if (isMobile(flow.platform)) {
|
|
@@ -8215,7 +8319,7 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8215
8319
|
resolveSecret: (ref) => secrets[ref]
|
|
8216
8320
|
});
|
|
8217
8321
|
} finally {
|
|
8218
|
-
deps.lease?.release("run");
|
|
8322
|
+
deps.lease?.release("run", lane);
|
|
8219
8323
|
}
|
|
8220
8324
|
}
|
|
8221
8325
|
function deliveredRun(delivered) {
|
|
@@ -8235,55 +8339,6 @@ function deliveredRun(delivered) {
|
|
|
8235
8339
|
resolveFile: () => void 0
|
|
8236
8340
|
};
|
|
8237
8341
|
}
|
|
8238
|
-
var BrowserLease = class {
|
|
8239
|
-
owner = null;
|
|
8240
|
-
phase = "idle";
|
|
8241
|
-
since = null;
|
|
8242
|
-
/** Take the lease, or report who has it. Never awaits — that is the point. */
|
|
8243
|
-
acquire(owner) {
|
|
8244
|
-
if (this.owner !== null) {
|
|
8245
|
-
return { ok: false, heldBy: this.owner, phase: this.phase };
|
|
8246
|
-
}
|
|
8247
|
-
this.owner = owner;
|
|
8248
|
-
this.phase = "starting";
|
|
8249
|
-
this.since = Date.now();
|
|
8250
|
-
return { ok: true };
|
|
8251
|
-
}
|
|
8252
|
-
/** The holder finished starting and is now doing the thing. */
|
|
8253
|
-
activate(owner) {
|
|
8254
|
-
if (this.owner === owner) this.phase = "active";
|
|
8255
|
-
}
|
|
8256
|
-
/** The holder has begun shutting down. Still not idle: a second stop must
|
|
8257
|
-
* join the first rather than starting its own. */
|
|
8258
|
-
beginRelease(owner) {
|
|
8259
|
-
if (this.owner !== owner || this.phase === "stopping") return false;
|
|
8260
|
-
this.phase = "stopping";
|
|
8261
|
-
return true;
|
|
8262
|
-
}
|
|
8263
|
-
release(owner) {
|
|
8264
|
-
if (this.owner !== owner) return;
|
|
8265
|
-
this.owner = null;
|
|
8266
|
-
this.phase = "idle";
|
|
8267
|
-
this.since = null;
|
|
8268
|
-
}
|
|
8269
|
-
/** Drop the lease whoever holds it — shutdown, and nothing else. */
|
|
8270
|
-
forceRelease() {
|
|
8271
|
-
this.owner = null;
|
|
8272
|
-
this.phase = "idle";
|
|
8273
|
-
this.since = null;
|
|
8274
|
-
}
|
|
8275
|
-
state() {
|
|
8276
|
-
return { owner: this.owner, phase: this.phase, since: this.since };
|
|
8277
|
-
}
|
|
8278
|
-
/** The sentence a refused caller is shown. Names what is happening, because
|
|
8279
|
-
* "busy" leaves someone hunting for a window they cannot find. */
|
|
8280
|
-
static refusal(heldBy, phase) {
|
|
8281
|
-
const what = heldBy === "recording" ? "a recording" : heldBy === "debug" ? "a debug session" : "a run";
|
|
8282
|
-
if (phase === "starting") return `${what} is starting \u2014 wait for it to open, or stop it first`;
|
|
8283
|
-
if (phase === "stopping") return `${what} is still closing \u2014 try again in a moment`;
|
|
8284
|
-
return `${what} is already using the browser \u2014 stop it before starting another`;
|
|
8285
|
-
}
|
|
8286
|
-
};
|
|
8287
8342
|
|
|
8288
8343
|
// src/engine/checkouts.ts
|
|
8289
8344
|
function resolveCheckouts(dirs) {
|
|
@@ -9240,11 +9295,12 @@ function executionHandlers(ctx) {
|
|
|
9240
9295
|
detail: `this flow needs ${missing.length} secret value(s) that are not on this machine (${missing.map((m) => m.ref || m.field).join(", ")}) \u2014 set them on the machine that runs it`
|
|
9241
9296
|
};
|
|
9242
9297
|
}
|
|
9243
|
-
const
|
|
9298
|
+
const lanes = [laneOf(flow.platform)];
|
|
9299
|
+
const claim = lease?.acquireAll("run", lanes);
|
|
9244
9300
|
if (claim && !claim.ok) {
|
|
9245
|
-
return { ok: false, reason: "busy", detail:
|
|
9301
|
+
return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, claim.lane) };
|
|
9246
9302
|
}
|
|
9247
|
-
lease?.
|
|
9303
|
+
lease?.activateAll("run", lanes);
|
|
9248
9304
|
onActivity?.({ kind: "run", projectId: project.id, detail: flow.name });
|
|
9249
9305
|
const aborter = new AbortController();
|
|
9250
9306
|
cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
|
|
@@ -9279,6 +9335,8 @@ function executionHandlers(ctx) {
|
|
|
9279
9335
|
ok: true,
|
|
9280
9336
|
result: {
|
|
9281
9337
|
runId: outcome.run?.id ?? null,
|
|
9338
|
+
// What it was called, for a service that only knew its id.
|
|
9339
|
+
flowName: flow.name,
|
|
9282
9340
|
// The WHOLE outcome, not a summary of it. A dashboard renders the
|
|
9283
9341
|
// per-step report exactly as the local app does, and a result
|
|
9284
9342
|
// carrying only counts makes that screen crash rather than degrade.
|
|
@@ -9294,7 +9352,7 @@ function executionHandlers(ctx) {
|
|
|
9294
9352
|
if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
|
|
9295
9353
|
if (runId) cancels.delete(runId);
|
|
9296
9354
|
onActivity?.(null);
|
|
9297
|
-
lease?.
|
|
9355
|
+
lease?.releaseAll("run", lanes);
|
|
9298
9356
|
}
|
|
9299
9357
|
},
|
|
9300
9358
|
/** Run a test case: several flows, one browser, in order.
|
|
@@ -9312,11 +9370,12 @@ function executionHandlers(ctx) {
|
|
|
9312
9370
|
if (testCase.occurrences.length === 0) {
|
|
9313
9371
|
return { ok: false, reason: "failed", detail: `"${testCase.name}" has no flows in it to run` };
|
|
9314
9372
|
}
|
|
9315
|
-
const
|
|
9373
|
+
const lanes = [...new Set(testCase.occurrences.map((o) => laneOf(o.platform)))];
|
|
9374
|
+
const claim = lease?.acquireAll("run", lanes);
|
|
9316
9375
|
if (claim && !claim.ok) {
|
|
9317
|
-
return { ok: false, reason: "busy", detail:
|
|
9376
|
+
return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, claim.lane) };
|
|
9318
9377
|
}
|
|
9319
|
-
lease?.
|
|
9378
|
+
lease?.activateAll("run", lanes);
|
|
9320
9379
|
onActivity?.({ kind: "run", projectId: project.id, detail: testCase.name });
|
|
9321
9380
|
const aborter = new AbortController();
|
|
9322
9381
|
cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
|
|
@@ -9355,6 +9414,8 @@ function executionHandlers(ctx) {
|
|
|
9355
9414
|
ok: true,
|
|
9356
9415
|
result: {
|
|
9357
9416
|
runId: run?.id ?? null,
|
|
9417
|
+
// What it was called, for a service that only knew its id.
|
|
9418
|
+
caseName: testCase.name,
|
|
9358
9419
|
// The whole outcome, per-flow groups included: a dashboard renders
|
|
9359
9420
|
// the report from this, and a result carrying only counts makes
|
|
9360
9421
|
// that screen crash rather than degrade.
|
|
@@ -9366,7 +9427,7 @@ function executionHandlers(ctx) {
|
|
|
9366
9427
|
if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
|
|
9367
9428
|
if (runId) cancels.delete(runId);
|
|
9368
9429
|
onActivity?.(null);
|
|
9369
|
-
lease?.
|
|
9430
|
+
lease?.releaseAll("run", lanes);
|
|
9370
9431
|
}
|
|
9371
9432
|
},
|
|
9372
9433
|
"run.cancel": async (command, project) => {
|
|
@@ -9779,7 +9840,9 @@ var runFile = (file2, args, timeoutMs) => new Promise((resolve2, reject) => {
|
|
|
9779
9840
|
resolve2({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
9780
9841
|
});
|
|
9781
9842
|
});
|
|
9782
|
-
var live =
|
|
9843
|
+
var live = /* @__PURE__ */ new Map();
|
|
9844
|
+
var liveById = (sessionId) => [...live.values()].find((l) => l.id === sessionId);
|
|
9845
|
+
var ownerOf = (l) => l.draft ? "recording" : "debug";
|
|
9783
9846
|
var SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|\bpin\b|cvv|cvc/i;
|
|
9784
9847
|
async function screenOf(l, withImage = false) {
|
|
9785
9848
|
l.session.changed();
|
|
@@ -9805,8 +9868,9 @@ async function screenOf(l, withImage = false) {
|
|
|
9805
9868
|
}
|
|
9806
9869
|
function mobileHandlers(ctx) {
|
|
9807
9870
|
const require2 = (command) => {
|
|
9808
|
-
|
|
9809
|
-
|
|
9871
|
+
const l = liveById(command.args["sessionId"]);
|
|
9872
|
+
if (!l) throw new Error("that phone session has ended \u2014 open it again");
|
|
9873
|
+
return l;
|
|
9810
9874
|
};
|
|
9811
9875
|
return {
|
|
9812
9876
|
// Everything this computer has for testing — web and phones — for the Guide.
|
|
@@ -9817,6 +9881,21 @@ function mobileHandlers(ctx) {
|
|
|
9817
9881
|
const devices = await listDevices();
|
|
9818
9882
|
return { ok: true, result: { devices, ...command.args["doctor"] === true ? { doctor: await mobileDoctor() } : {} } };
|
|
9819
9883
|
},
|
|
9884
|
+
// THE PHONE A RUN IS DRIVING, as a picture: whatever session is open on
|
|
9885
|
+
// that platform — a run's, a recording's. Not effectful, never kept, and
|
|
9886
|
+
// nothing when no phone is open: the mirror then shows that.
|
|
9887
|
+
"mobile.peek": async (command) => {
|
|
9888
|
+
const platform = command.args["platform"];
|
|
9889
|
+
if (!isMobile(platform)) return { ok: false, reason: "failed", detail: "say which phone: android or ios" };
|
|
9890
|
+
const session = live.get(platform)?.session ?? openSessionOn(platform);
|
|
9891
|
+
if (!session) return { ok: true, result: { image: null } };
|
|
9892
|
+
try {
|
|
9893
|
+
const png = await session.screenshot();
|
|
9894
|
+
return { ok: true, result: { image: png.toString("base64"), device: session.deviceName } };
|
|
9895
|
+
} catch {
|
|
9896
|
+
return { ok: true, result: { image: null } };
|
|
9897
|
+
}
|
|
9898
|
+
},
|
|
9820
9899
|
"mobile.screen": async (command) => {
|
|
9821
9900
|
try {
|
|
9822
9901
|
const l = require2(command);
|
|
@@ -9833,14 +9912,21 @@ function mobileHandlers(ctx) {
|
|
|
9833
9912
|
},
|
|
9834
9913
|
"mobile.session": async (command, project) => {
|
|
9835
9914
|
const op = String(command.args["op"] ?? "");
|
|
9915
|
+
let claimed = null;
|
|
9836
9916
|
try {
|
|
9837
9917
|
if (op === "start") {
|
|
9838
|
-
if (live) {
|
|
9839
|
-
await live.session.close().catch(() => void 0);
|
|
9840
|
-
live = null;
|
|
9841
|
-
}
|
|
9842
9918
|
const platform = command.args["platform"];
|
|
9843
9919
|
if (!isMobile(platform)) return { ok: false, reason: "failed", detail: "say which phone: android or ios" };
|
|
9920
|
+
const prior = live.get(platform);
|
|
9921
|
+
if (prior) {
|
|
9922
|
+
live.delete(prior.platform);
|
|
9923
|
+
await prior.session.close().catch(() => void 0);
|
|
9924
|
+
ctx.lease?.release(ownerOf(prior), prior.platform);
|
|
9925
|
+
}
|
|
9926
|
+
const owner = command.args["record"] === true ? "recording" : "debug";
|
|
9927
|
+
const claim = ctx.lease?.acquire(owner, platform);
|
|
9928
|
+
if (claim && !claim.ok) return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, platform) };
|
|
9929
|
+
claimed = { owner, platform };
|
|
9844
9930
|
const config = command.args["config"] ?? {};
|
|
9845
9931
|
const secrets = command.args["secrets"] ?? {};
|
|
9846
9932
|
const device = command.args["device"] ? DeviceChoiceSchema.parse(command.args["device"]) : void 0;
|
|
@@ -9875,7 +9961,8 @@ function mobileHandlers(ctx) {
|
|
|
9875
9961
|
...auth?.profileId ? { signIn: auth } : {}
|
|
9876
9962
|
};
|
|
9877
9963
|
}
|
|
9878
|
-
live
|
|
9964
|
+
live.set(l2.platform, l2);
|
|
9965
|
+
ctx.lease?.activate(owner, l2.platform);
|
|
9879
9966
|
if (/^[a-z][\w+.-]*:\/\//i.test(startPath)) await session.openLink(startPath);
|
|
9880
9967
|
const opening = [
|
|
9881
9968
|
...l2.draft?.signIn ? [RecordedStepSchema.parse({ id: "sign-in", type: "signIn", auth: l2.draft.signIn, role: "", name: "Sign in", scope: "", value: "", secret: false, legacySelector: "", proposals: [] })] : [],
|
|
@@ -9958,7 +10045,8 @@ function mobileHandlers(ctx) {
|
|
|
9958
10045
|
return { ok: true, result: { steps: l.draft.steps } };
|
|
9959
10046
|
}
|
|
9960
10047
|
if (op === "stop" || op === "discard") {
|
|
9961
|
-
live
|
|
10048
|
+
live.delete(l.platform);
|
|
10049
|
+
ctx.lease?.release(ownerOf(l), l.platform);
|
|
9962
10050
|
await l.session.close().catch(() => void 0);
|
|
9963
10051
|
if (op === "discard" || !l.draft) return { ok: true, result: { ended: true } };
|
|
9964
10052
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9982,6 +10070,7 @@ function mobileHandlers(ctx) {
|
|
|
9982
10070
|
}
|
|
9983
10071
|
return { ok: false, reason: "failed", detail: `unknown phone session operation "${op}"` };
|
|
9984
10072
|
} catch (error) {
|
|
10073
|
+
if (claimed && !live.has(claimed.platform)) ctx.lease?.release(claimed.owner, claimed.platform);
|
|
9985
10074
|
return { ok: false, reason: "failed", detail: error.message.split("\n")[0] ?? "the phone session failed" };
|
|
9986
10075
|
}
|
|
9987
10076
|
}
|
|
@@ -2568,6 +2568,10 @@ async function localCapabilities(target, udid, opts = {}) {
|
|
|
2568
2568
|
"appium:appPackage": app.id,
|
|
2569
2569
|
...app.activity ? { "appium:appActivity": app.activity } : {},
|
|
2570
2570
|
"appium:appWaitActivity": "*",
|
|
2571
|
+
// Its own ports: two phones on one Appium each need their own driver
|
|
2572
|
+
// server and picture stream, or the second session steals the first's.
|
|
2573
|
+
...opts.ports?.system ? { "appium:systemPort": opts.ports.system } : {},
|
|
2574
|
+
...opts.ports?.mjpeg ? { "appium:mjpegServerPort": opts.ports.mjpeg } : {},
|
|
2571
2575
|
"appium:autoGrantPermissions": false,
|
|
2572
2576
|
"appium:disableWindowAnimation": true,
|
|
2573
2577
|
"appium:uiautomator2ServerLaunchTimeout": 12e4,
|
|
@@ -2584,6 +2588,8 @@ async function localCapabilities(target, udid, opts = {}) {
|
|
|
2584
2588
|
"appium:bundleId": app.id,
|
|
2585
2589
|
"appium:wdaLaunchTimeout": 24e4,
|
|
2586
2590
|
"appium:wdaConnectionTimeout": 24e4,
|
|
2591
|
+
...opts.ports?.wda ? { "appium:wdaLocalPort": opts.ports.wda } : {},
|
|
2592
|
+
...opts.ports?.mjpeg ? { "appium:mjpegServerPort": opts.ports.mjpeg } : {},
|
|
2587
2593
|
...target.mobile.ios?.teamId ? { "appium:xcodeOrgId": target.mobile.ios.teamId, "appium:xcodeSigningId": "Apple Development" } : {}
|
|
2588
2594
|
};
|
|
2589
2595
|
}
|
|
@@ -3203,6 +3209,8 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
|
|
|
3203
3209
|
throw last;
|
|
3204
3210
|
}
|
|
3205
3211
|
var uploaded = /* @__PURE__ */ new Map();
|
|
3212
|
+
var OPEN = /* @__PURE__ */ new Map();
|
|
3213
|
+
var openSessionOn = (platform) => OPEN.get(platform) ?? null;
|
|
3206
3214
|
var MobileSession = class _MobileSession {
|
|
3207
3215
|
constructor(driver, platform, appId, deviceName, cloud) {
|
|
3208
3216
|
this.driver = driver;
|
|
@@ -3243,6 +3251,7 @@ var MobileSession = class _MobileSession {
|
|
|
3243
3251
|
throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
|
|
3244
3252
|
});
|
|
3245
3253
|
const cloudSession = new _MobileSession(driver2, target.platform, app.id, choice2.device, true);
|
|
3254
|
+
OPEN.set(target.platform, cloudSession);
|
|
3246
3255
|
await cloudSession.settle(2e4);
|
|
3247
3256
|
return cloudSession;
|
|
3248
3257
|
}
|
|
@@ -3256,12 +3265,14 @@ var MobileSession = class _MobileSession {
|
|
|
3256
3265
|
const endpoint = await appiumServer();
|
|
3257
3266
|
const buildFile = app.build ? await localBuild(app.build) : null;
|
|
3258
3267
|
say(`Opening the app on ${known.name}\u2026`);
|
|
3259
|
-
const
|
|
3268
|
+
const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
|
|
3269
|
+
const capabilities = await localCapabilities(target, udid, { buildFile, ports });
|
|
3260
3270
|
const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
|
|
3261
3271
|
const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
|
|
3262
3272
|
throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
|
|
3263
3273
|
});
|
|
3264
3274
|
const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
|
|
3275
|
+
OPEN.set(target.platform, session);
|
|
3265
3276
|
await session.terminateApp().catch(() => {
|
|
3266
3277
|
});
|
|
3267
3278
|
await session.launchApp().catch(() => {
|
|
@@ -3494,6 +3505,7 @@ var MobileSession = class _MobileSession {
|
|
|
3494
3505
|
return { relPath: path4.basename(file), bytes: bytes.length };
|
|
3495
3506
|
}
|
|
3496
3507
|
async close() {
|
|
3508
|
+
if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
|
|
3497
3509
|
if (this.recording) await this.stopVideo(null);
|
|
3498
3510
|
await this.driver.close();
|
|
3499
3511
|
}
|
|
@@ -4152,6 +4164,7 @@ export {
|
|
|
4152
4164
|
CheckError,
|
|
4153
4165
|
textOf,
|
|
4154
4166
|
checkMobileAssertion,
|
|
4167
|
+
openSessionOn,
|
|
4155
4168
|
MobileSession,
|
|
4156
4169
|
MOBILE_TIMEOUTS,
|
|
4157
4170
|
performMobileStep,
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
runtimeValue,
|
|
24
24
|
start,
|
|
25
25
|
status
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-6AR63WTN.js";
|
|
27
27
|
import {
|
|
28
28
|
CLOUD_PROVIDERS,
|
|
29
29
|
CLOUD_PROVIDER_NAME,
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
screenLines,
|
|
50
50
|
stepRefusedOn,
|
|
51
51
|
targetFor
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-FIR6NOLA.js";
|
|
53
53
|
|
|
54
54
|
// src/cli.ts
|
|
55
55
|
import { parseArgs } from "util";
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
mobileSetup,
|
|
32
32
|
nodeAt,
|
|
33
33
|
openMobileSession,
|
|
34
|
+
openSessionOn,
|
|
34
35
|
performMobileStep,
|
|
35
36
|
readScreen,
|
|
36
37
|
replayMobileCase,
|
|
@@ -44,7 +45,7 @@ import {
|
|
|
44
45
|
textOf,
|
|
45
46
|
toolEnv,
|
|
46
47
|
typesSecrets
|
|
47
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-FIR6NOLA.js";
|
|
48
49
|
export {
|
|
49
50
|
AGENT_HOME,
|
|
50
51
|
APPIUM_DIR,
|
|
@@ -77,6 +78,7 @@ export {
|
|
|
77
78
|
mobileSetup,
|
|
78
79
|
nodeAt,
|
|
79
80
|
openMobileSession,
|
|
81
|
+
openSessionOn,
|
|
80
82
|
performMobileStep,
|
|
81
83
|
readScreen,
|
|
82
84
|
replayMobileCase,
|
package/dist/engine.js
CHANGED
package/package.json
CHANGED