@flowriki/agent 0.1.0-beta.64 → 0.1.0-beta.65
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.
|
@@ -16,10 +16,12 @@ import os3 from "os";
|
|
|
16
16
|
import path3 from "path";
|
|
17
17
|
import { execFile as execFile2 } from "child_process";
|
|
18
18
|
import fs4 from "fs";
|
|
19
|
-
import os4 from "os";
|
|
20
19
|
import path4 from "path";
|
|
20
|
+
import fs5 from "fs";
|
|
21
|
+
import os4 from "os";
|
|
22
|
+
import path5 from "path";
|
|
21
23
|
|
|
22
|
-
// ../test-model/dist/chunk-
|
|
24
|
+
// ../test-model/dist/chunk-6JIZJVPN.js
|
|
23
25
|
import { z as z2 } from "zod";
|
|
24
26
|
import { z } from "zod";
|
|
25
27
|
var PLATFORMS = ["web", "android", "ios"];
|
|
@@ -69,7 +71,10 @@ var CloudAccountSchema = z.object({
|
|
|
69
71
|
provider: z.enum(CLOUD_PROVIDERS),
|
|
70
72
|
username: z.string().trim().min(1).max(200),
|
|
71
73
|
/** Sauce Labs data centre: "us-west-1", "eu-central-1", "us-east-4". */
|
|
72
|
-
region:
|
|
74
|
+
/** A Sauce Labs region — a word like us-west-1, never an address: it is
|
|
75
|
+
* spelled into the host the farm's key is sent to. Anything else is
|
|
76
|
+
* dropped, and the default region used. */
|
|
77
|
+
region: z.string().trim().max(40).optional().transform((v) => v && /^[a-z]{2}-[a-z]+-\d$/.test(v) ? v : void 0),
|
|
73
78
|
keyRef: z.string().regex(/^[a-z][a-z0-9_]*$/)
|
|
74
79
|
}).strict();
|
|
75
80
|
var DeviceChoiceSchema = z.discriminatedUnion("kind", [
|
|
@@ -629,19 +634,19 @@ var CHECK_TARGET_FIELDS = [
|
|
|
629
634
|
var unset = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
|
|
630
635
|
function mirrorCheckTargets(steps) {
|
|
631
636
|
return steps.map((step) => {
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
634
|
-
const proposals =
|
|
637
|
+
const held2 = step;
|
|
638
|
+
if (held2["type"] !== "assert") return step;
|
|
639
|
+
const proposals = held2["proposals"];
|
|
635
640
|
if (!Array.isArray(proposals)) return step;
|
|
636
641
|
const first = proposals[0];
|
|
637
642
|
const target = first?.["target"];
|
|
638
643
|
if (!target) return step;
|
|
639
644
|
const fill = {};
|
|
640
645
|
for (const field of CHECK_TARGET_FIELDS) {
|
|
641
|
-
if (!unset(
|
|
646
|
+
if (!unset(held2[field]) || unset(target[field])) continue;
|
|
642
647
|
fill[field] = target[field];
|
|
643
648
|
}
|
|
644
|
-
return Object.keys(fill).length === 0 ? step : { ...
|
|
649
|
+
return Object.keys(fill).length === 0 ? step : { ...held2, ...fill };
|
|
645
650
|
});
|
|
646
651
|
}
|
|
647
652
|
|
|
@@ -660,6 +665,9 @@ import { z as z11 } from "zod";
|
|
|
660
665
|
import { z as z12 } from "zod";
|
|
661
666
|
import { z as z13 } from "zod";
|
|
662
667
|
import { z as z14 } from "zod";
|
|
668
|
+
import { z as z15 } from "zod";
|
|
669
|
+
import { z as z16 } from "zod";
|
|
670
|
+
import { z as z17 } from "zod";
|
|
663
671
|
var EngineSchema = z3.enum(["chromium", "firefox", "webkit", "android", "ios"]);
|
|
664
672
|
var CapabilitySchema = z3.object({
|
|
665
673
|
id: z3.string(),
|
|
@@ -1832,7 +1840,7 @@ function decodeEntities(text) {
|
|
|
1832
1840
|
var nameKey = (name) => decodeEntities(name).toLowerCase().replace(/&/g, " and ").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
1833
1841
|
var PLATFORM_PART = "(web|android|ios)";
|
|
1834
1842
|
var LEARNED_PATH = new RegExp(`^(explore\\/${PLATFORM_PART}\\.md|memory\\/sessions\\/[a-z0-9][a-z0-9-]{0,120}\\.md)$`);
|
|
1835
|
-
var isLearnedPath = (
|
|
1843
|
+
var isLearnedPath = (path9) => LEARNED_PATH.test(path9);
|
|
1836
1844
|
var LearnedSchema = z11.object({
|
|
1837
1845
|
id: z11.string().regex(/^lrn_[a-z0-9_-]+$/),
|
|
1838
1846
|
projectId: z11.string().min(1),
|
|
@@ -1873,6 +1881,29 @@ var WhatToDoSchema = z13.object({
|
|
|
1873
1881
|
skipped: z13.array(z13.string().min(1).max(80)).max(100),
|
|
1874
1882
|
updatedAt: z13.string()
|
|
1875
1883
|
}).strict();
|
|
1884
|
+
var BLOCKER_KINDS = ["app-bug", "needs-person", "unclear-step", "not-found", "stuck"];
|
|
1885
|
+
var CaseStandingSchema = z14.discriminatedUnion("state", [
|
|
1886
|
+
z14.object({
|
|
1887
|
+
state: z14.literal("blocked"),
|
|
1888
|
+
kind: z14.enum(BLOCKER_KINDS),
|
|
1889
|
+
/** The business step it stopped at, counted from 1 — when it was one. */
|
|
1890
|
+
line: z14.number().int().positive().max(500).optional(),
|
|
1891
|
+
lineText: z14.string().max(500).optional(),
|
|
1892
|
+
detail: z14.string().min(1).max(1500),
|
|
1893
|
+
/** What would unblock it, said for a person. */
|
|
1894
|
+
needs: z14.string().max(600).optional(),
|
|
1895
|
+
platform: z14.enum(["web", "android", "ios"]).optional(),
|
|
1896
|
+
by: z14.string().max(200),
|
|
1897
|
+
at: z14.string()
|
|
1898
|
+
}).strict(),
|
|
1899
|
+
z14.object({
|
|
1900
|
+
state: z14.literal("manual"),
|
|
1901
|
+
/** Why it is run by hand, when somebody said. */
|
|
1902
|
+
reason: z14.string().max(600).optional(),
|
|
1903
|
+
by: z14.string().max(200),
|
|
1904
|
+
at: z14.string()
|
|
1905
|
+
}).strict()
|
|
1906
|
+
]);
|
|
1876
1907
|
var CaseRegistrySchema = z14.object({
|
|
1877
1908
|
id: z14.string().regex(/^creg_/),
|
|
1878
1909
|
projectId: z14.string().min(1),
|
|
@@ -1880,11 +1911,83 @@ var CaseRegistrySchema = z14.object({
|
|
|
1880
1911
|
cases: z14.record(z14.string(), z14.number().int().positive()),
|
|
1881
1912
|
features: z14.record(z14.string(), z14.number().int().positive()),
|
|
1882
1913
|
reviews: z14.record(z14.string(), z14.object({ hash: z14.string(), by: z14.string().max(200), at: z14.string() }).strict()),
|
|
1914
|
+
/** Blocked and manual cases, by case id. Absent on a project with none. */
|
|
1915
|
+
standing: z14.record(z14.string(), CaseStandingSchema).optional(),
|
|
1883
1916
|
updatedAt: z14.string()
|
|
1884
1917
|
}).strict();
|
|
1918
|
+
var BranchChoiceSchema = z15.object({
|
|
1919
|
+
id: z15.string().regex(/^bch_/),
|
|
1920
|
+
projectId: z15.string().min(1),
|
|
1921
|
+
userId: z15.string().min(1),
|
|
1922
|
+
/** The branch (a change id), or null for the published version. */
|
|
1923
|
+
changeId: z15.string().min(1).max(100).nullable(),
|
|
1924
|
+
at: z15.string()
|
|
1925
|
+
}).strict();
|
|
1926
|
+
var TokensSchema = z16.object({
|
|
1927
|
+
/** Read fresh — the request, minus what came from the cache. */
|
|
1928
|
+
input: z16.number().int().nonnegative(),
|
|
1929
|
+
output: z16.number().int().nonnegative(),
|
|
1930
|
+
/** Read from the prompt cache: a tenth of the price of fresh input. */
|
|
1931
|
+
cacheRead: z16.number().int().nonnegative(),
|
|
1932
|
+
/** Written to the prompt cache, so later turns can read it cheaply. */
|
|
1933
|
+
cacheWrite: z16.number().int().nonnegative()
|
|
1934
|
+
}).strict();
|
|
1935
|
+
var USAGE_STAGES = ["write", "automate", "setup", "knowledge", "reply"];
|
|
1936
|
+
var USAGE_KINDS = ["automate", "reautomate", "heal", "update", "write", "explore", "chat"];
|
|
1937
|
+
function usageKindOf(shown) {
|
|
1938
|
+
const s = shown.trim();
|
|
1939
|
+
if (/^Heal\b/i.test(s)) return "heal";
|
|
1940
|
+
if (/^Re-automate\b/i.test(s)) return "reautomate";
|
|
1941
|
+
if (/^Automate\b/i.test(s)) return "automate";
|
|
1942
|
+
if (/business steps of .* (just )?changed/i.test(s) || /^Update\b/i.test(s)) return "update";
|
|
1943
|
+
if (/^Explore\b|\bexplore (the|this) (app|site|website)\b/i.test(s)) return "explore";
|
|
1944
|
+
if (/^(Write|Import)\b.*test cases?/i.test(s)) return "write";
|
|
1945
|
+
return "chat";
|
|
1946
|
+
}
|
|
1947
|
+
var UsagePartSchema = TokensSchema.extend({
|
|
1948
|
+
stage: z16.enum(USAGE_STAGES),
|
|
1949
|
+
/** The tool the turn called; null for a turn that called none. */
|
|
1950
|
+
tool: z16.string().max(80).nullable(),
|
|
1951
|
+
/** The test case the work was for, when one was being worked on. */
|
|
1952
|
+
caseName: z16.string().max(300).nullable(),
|
|
1953
|
+
folder: z16.string().max(300).nullable(),
|
|
1954
|
+
/** Its id — TC-12 — when the assistant named it by id. */
|
|
1955
|
+
caseKey: z16.string().max(40).nullable(),
|
|
1956
|
+
/** The flow being recorded, when one was. */
|
|
1957
|
+
flow: z16.string().max(300).nullable(),
|
|
1958
|
+
/** How many turns are in this line. */
|
|
1959
|
+
turns: z16.number().int().nonnegative()
|
|
1960
|
+
}).strict();
|
|
1961
|
+
var UsageRecordSchema = z16.object({
|
|
1962
|
+
id: z16.string().regex(/^use_/),
|
|
1963
|
+
projectId: z16.string().min(1),
|
|
1964
|
+
userId: z16.string().min(1),
|
|
1965
|
+
jobId: z16.string().min(1),
|
|
1966
|
+
kind: z16.enum(USAGE_KINDS),
|
|
1967
|
+
/** The start of what was asked, so a list of jobs reads as one. */
|
|
1968
|
+
asked: z16.string().max(200),
|
|
1969
|
+
platform: z16.enum(["web", "android", "ios"]).nullable(),
|
|
1970
|
+
startedAt: z16.string(),
|
|
1971
|
+
finishedAt: z16.string().nullable(),
|
|
1972
|
+
status: z16.enum(["running", "done", "failed", "stopped"]),
|
|
1973
|
+
/** Every turn added up, from the turns as they happened. */
|
|
1974
|
+
totals: TokensSchema,
|
|
1975
|
+
turns: z16.number().int().nonnegative(),
|
|
1976
|
+
/** What the model said the whole job cost, in US dollars — at API prices;
|
|
1977
|
+
* on a Claude plan it is what the same work would cost there. */
|
|
1978
|
+
costUsd: z16.number().nonnegative().nullable(),
|
|
1979
|
+
durationMs: z16.number().int().nonnegative().nullable(),
|
|
1980
|
+
parts: z16.array(UsagePartSchema).max(600)
|
|
1981
|
+
}).strict();
|
|
1982
|
+
var StepRenameSchema = z17.object({
|
|
1983
|
+
occurrenceId: z17.string().min(1).max(200),
|
|
1984
|
+
stepId: z17.string().min(1).max(200),
|
|
1985
|
+
from: z17.string().min(1).max(500),
|
|
1986
|
+
to: z17.string().trim().min(1).max(200)
|
|
1987
|
+
}).strict();
|
|
1885
1988
|
|
|
1886
1989
|
// ../engine-appium/dist/index.js
|
|
1887
|
-
import
|
|
1990
|
+
import path6 from "path";
|
|
1888
1991
|
|
|
1889
1992
|
// ../core/dist/index.js
|
|
1890
1993
|
function proposeAssertions(before, after) {
|
|
@@ -2217,9 +2320,9 @@ var STEP_PRESENTATION = {
|
|
|
2217
2320
|
};
|
|
2218
2321
|
|
|
2219
2322
|
// ../engine-appium/dist/index.js
|
|
2220
|
-
import path6 from "path";
|
|
2221
|
-
import fs5 from "fs";
|
|
2222
2323
|
import path7 from "path";
|
|
2324
|
+
import fs6 from "fs";
|
|
2325
|
+
import path8 from "path";
|
|
2223
2326
|
import { spawn as spawn3 } from "child_process";
|
|
2224
2327
|
var WebDriverError = class extends Error {
|
|
2225
2328
|
constructor(message, error, status) {
|
|
@@ -2231,10 +2334,10 @@ var WebDriverError = class extends Error {
|
|
|
2231
2334
|
status;
|
|
2232
2335
|
};
|
|
2233
2336
|
var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecc";
|
|
2234
|
-
async function call(endpoint, method,
|
|
2337
|
+
async function call(endpoint, method, path9, body, timeoutMs = 12e4) {
|
|
2235
2338
|
const headers = { "content-type": "application/json; charset=utf-8" };
|
|
2236
2339
|
if (endpoint.username) headers["authorization"] = `Basic ${Buffer.from(`${endpoint.username}:${endpoint.password ?? ""}`).toString("base64")}`;
|
|
2237
|
-
const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${
|
|
2340
|
+
const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path9}`, {
|
|
2238
2341
|
method,
|
|
2239
2342
|
headers,
|
|
2240
2343
|
...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {},
|
|
@@ -2273,11 +2376,11 @@ var WebDriverSession = class _WebDriverSession {
|
|
|
2273
2376
|
return new _WebDriverSession(endpoint, value.sessionId, value.capabilities ?? {});
|
|
2274
2377
|
}
|
|
2275
2378
|
/** Any other command — Appium's own extensions live under /appium/. */
|
|
2276
|
-
raw(method,
|
|
2277
|
-
return this.req(method,
|
|
2379
|
+
raw(method, path9, body, timeoutMs) {
|
|
2380
|
+
return this.req(method, path9, body, timeoutMs);
|
|
2278
2381
|
}
|
|
2279
|
-
req(method,
|
|
2280
|
-
return call(this.endpoint, method, `/session/${this.id}${
|
|
2382
|
+
req(method, path9, body, timeoutMs) {
|
|
2383
|
+
return call(this.endpoint, method, `/session/${this.id}${path9}`, body, timeoutMs);
|
|
2281
2384
|
}
|
|
2282
2385
|
source() {
|
|
2283
2386
|
return this.req("GET", "/source");
|
|
@@ -2380,8 +2483,8 @@ function appiumPath() {
|
|
|
2380
2483
|
}
|
|
2381
2484
|
function javaMajor(home) {
|
|
2382
2485
|
try {
|
|
2383
|
-
const
|
|
2384
|
-
const v = /JAVA_VERSION="(\d+)(?:\.(\d+))?/.exec(
|
|
2486
|
+
const release2 = fs.readFileSync(path.join(home, "release"), "utf8");
|
|
2487
|
+
const v = /JAVA_VERSION="(\d+)(?:\.(\d+))?/.exec(release2);
|
|
2385
2488
|
if (!v) return 0;
|
|
2386
2489
|
return v[1] === "1" ? Number(v[2] ?? 0) : Number(v[1]);
|
|
2387
2490
|
} catch {
|
|
@@ -3304,6 +3407,88 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
|
|
|
3304
3407
|
}
|
|
3305
3408
|
throw last;
|
|
3306
3409
|
}
|
|
3410
|
+
var held = /* @__PURE__ */ new Map();
|
|
3411
|
+
var lockDir = () => path4.join(process.env["FLOWRIKI_HOME"] || AGENT_HOME, "locks");
|
|
3412
|
+
var fileFor = (udid) => path4.join(lockDir(), `device-${udid.replace(/[^A-Za-z0-9._-]/g, "_")}.lock`);
|
|
3413
|
+
var alive = (pid) => {
|
|
3414
|
+
try {
|
|
3415
|
+
process.kill(pid, 0);
|
|
3416
|
+
return true;
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
return error.code === "EPERM";
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
var DeviceInUse = class extends Error {
|
|
3422
|
+
constructor(device, pid) {
|
|
3423
|
+
super(`${device} is being used by another Flowriki job on this computer \u2014 wait for it to finish, or stop it first`);
|
|
3424
|
+
this.device = device;
|
|
3425
|
+
this.pid = pid;
|
|
3426
|
+
this.name = "DeviceInUse";
|
|
3427
|
+
}
|
|
3428
|
+
device;
|
|
3429
|
+
pid;
|
|
3430
|
+
};
|
|
3431
|
+
function lockDevice(udid, name = udid) {
|
|
3432
|
+
const file = fileFor(udid);
|
|
3433
|
+
const mine = held.get(file);
|
|
3434
|
+
if (mine) {
|
|
3435
|
+
held.set(file, mine + 1);
|
|
3436
|
+
return once(() => release(file));
|
|
3437
|
+
}
|
|
3438
|
+
fs4.mkdirSync(lockDir(), { recursive: true, mode: 448 });
|
|
3439
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3440
|
+
try {
|
|
3441
|
+
const fd = fs4.openSync(file, "wx", 384);
|
|
3442
|
+
fs4.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
3443
|
+
fs4.closeSync(fd);
|
|
3444
|
+
held.set(file, 1);
|
|
3445
|
+
return once(() => release(file));
|
|
3446
|
+
} catch (error) {
|
|
3447
|
+
if (error.code !== "EEXIST") throw error;
|
|
3448
|
+
let owner = 0;
|
|
3449
|
+
try {
|
|
3450
|
+
owner = Number(JSON.parse(fs4.readFileSync(file, "utf8")).pid) || 0;
|
|
3451
|
+
} catch {
|
|
3452
|
+
}
|
|
3453
|
+
if (owner && owner !== process.pid && alive(owner)) throw new DeviceInUse(name, owner);
|
|
3454
|
+
try {
|
|
3455
|
+
fs4.unlinkSync(file);
|
|
3456
|
+
} catch {
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
throw new DeviceInUse(name, 0);
|
|
3461
|
+
}
|
|
3462
|
+
function release(file) {
|
|
3463
|
+
const count = held.get(file) ?? 0;
|
|
3464
|
+
if (count > 1) {
|
|
3465
|
+
held.set(file, count - 1);
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
held.delete(file);
|
|
3469
|
+
try {
|
|
3470
|
+
const owner = Number(JSON.parse(fs4.readFileSync(file, "utf8")).pid);
|
|
3471
|
+
if (owner === process.pid) fs4.unlinkSync(file);
|
|
3472
|
+
} catch {
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
function once(fn) {
|
|
3476
|
+
let done = false;
|
|
3477
|
+
return () => {
|
|
3478
|
+
if (!done) {
|
|
3479
|
+
done = true;
|
|
3480
|
+
fn();
|
|
3481
|
+
}
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
process.once("exit", () => {
|
|
3485
|
+
for (const file of held.keys()) {
|
|
3486
|
+
try {
|
|
3487
|
+
fs4.unlinkSync(file);
|
|
3488
|
+
} catch {
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3307
3492
|
var uploaded = /* @__PURE__ */ new Map();
|
|
3308
3493
|
var OPEN = /* @__PURE__ */ new Map();
|
|
3309
3494
|
var openSessionOn = (platform) => OPEN.get(platform) ?? null;
|
|
@@ -3321,6 +3506,8 @@ var MobileSession = class _MobileSession {
|
|
|
3321
3506
|
deviceName;
|
|
3322
3507
|
cloud;
|
|
3323
3508
|
cached = null;
|
|
3509
|
+
/** Lets go of this phone for other Flowriki processes — see device-lock. */
|
|
3510
|
+
releaseDevice = null;
|
|
3324
3511
|
static async open(target, opts = {}) {
|
|
3325
3512
|
const app = appOf(target);
|
|
3326
3513
|
const say = opts.onProgress ?? (() => {
|
|
@@ -3342,11 +3529,11 @@ var MobileSession = class _MobileSession {
|
|
|
3342
3529
|
}
|
|
3343
3530
|
}
|
|
3344
3531
|
say(`Opening ${choice2.device} on ${CLOUD_PROVIDER_NAME[choice2.provider]}\u2026`);
|
|
3345
|
-
const { endpoint
|
|
3346
|
-
const
|
|
3532
|
+
const { endpoint, capabilities } = cloudCapabilities(target, account, key, appRef);
|
|
3533
|
+
const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
|
|
3347
3534
|
throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
|
|
3348
3535
|
});
|
|
3349
|
-
const cloudSession = new _MobileSession(
|
|
3536
|
+
const cloudSession = new _MobileSession(driver, target.platform, app.id, choice2.device, true);
|
|
3350
3537
|
OPEN.set(target.platform, cloudSession);
|
|
3351
3538
|
await cloudSession.settle(2e4);
|
|
3352
3539
|
return cloudSession;
|
|
@@ -3358,23 +3545,30 @@ var MobileSession = class _MobileSession {
|
|
|
3358
3545
|
if (known.platform !== target.platform) throw new Error(`${known.name} is ${known.platform === "ios" ? "an iOS" : "an Android"} device; this flow runs on ${target.platform === "ios" ? "iOS" : "Android"}.`);
|
|
3359
3546
|
if (known.state !== "ready") say(`Starting ${known.name}\u2026`);
|
|
3360
3547
|
const udid = await bootDevice(known);
|
|
3361
|
-
const
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
const
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3548
|
+
const releaseDevice = lockDevice(udid, known.name);
|
|
3549
|
+
try {
|
|
3550
|
+
const endpoint = await appiumServer();
|
|
3551
|
+
const buildFile = app.build ? await localBuild(app.build) : null;
|
|
3552
|
+
say(`Opening the app on ${known.name}\u2026`);
|
|
3553
|
+
const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
|
|
3554
|
+
const activity = target.platform === "android" && !app.activity && !buildFile ? await launchActivityOf(udid, app.id) : null;
|
|
3555
|
+
const capabilities = await localCapabilities(target, udid, { buildFile, ports, ...activity ? { activity } : {} });
|
|
3556
|
+
const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
|
|
3557
|
+
const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
|
|
3558
|
+
throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
|
|
3559
|
+
});
|
|
3560
|
+
const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
|
|
3561
|
+
session.releaseDevice = releaseDevice;
|
|
3562
|
+
OPEN.set(target.platform, session);
|
|
3563
|
+
await session.terminateApp().catch(() => {
|
|
3564
|
+
});
|
|
3565
|
+
await session.launchApp().catch(() => {
|
|
3566
|
+
});
|
|
3567
|
+
return session;
|
|
3568
|
+
} catch (error) {
|
|
3569
|
+
releaseDevice();
|
|
3570
|
+
throw error;
|
|
3571
|
+
}
|
|
3378
3572
|
}
|
|
3379
3573
|
// ── Reading ────────────────────────────────────────────────────────────
|
|
3380
3574
|
async screen() {
|
|
@@ -3405,8 +3599,8 @@ var MobileSession = class _MobileSession {
|
|
|
3405
3599
|
async screenshot(file) {
|
|
3406
3600
|
const png = Buffer.from(await this.driver.screenshot(), "base64");
|
|
3407
3601
|
if (file) {
|
|
3408
|
-
|
|
3409
|
-
|
|
3602
|
+
fs5.mkdirSync(path5.dirname(file), { recursive: true });
|
|
3603
|
+
fs5.writeFileSync(file, png);
|
|
3410
3604
|
}
|
|
3411
3605
|
return png;
|
|
3412
3606
|
}
|
|
@@ -3607,14 +3801,19 @@ var MobileSession = class _MobileSession {
|
|
|
3607
3801
|
const b64 = await this.driver.raw("POST", "/appium/stop_recording_screen", { options: {} }, 12e4).catch(() => "");
|
|
3608
3802
|
if (!file || !b64) return null;
|
|
3609
3803
|
const bytes = Buffer.from(b64, "base64");
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
return { relPath:
|
|
3804
|
+
fs5.mkdirSync(path5.dirname(file), { recursive: true });
|
|
3805
|
+
fs5.writeFileSync(file, bytes);
|
|
3806
|
+
return { relPath: path5.basename(file), bytes: bytes.length };
|
|
3613
3807
|
}
|
|
3614
3808
|
async close() {
|
|
3615
3809
|
if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
|
|
3616
|
-
|
|
3617
|
-
|
|
3810
|
+
try {
|
|
3811
|
+
if (this.recording) await this.stopVideo(null);
|
|
3812
|
+
await this.driver.close();
|
|
3813
|
+
} finally {
|
|
3814
|
+
this.releaseDevice?.();
|
|
3815
|
+
this.releaseDevice = null;
|
|
3816
|
+
}
|
|
3618
3817
|
}
|
|
3619
3818
|
};
|
|
3620
3819
|
async function launchActivityOf(udid, appPackage) {
|
|
@@ -3630,18 +3829,18 @@ async function launchActivityOf(udid, appPackage) {
|
|
|
3630
3829
|
}
|
|
3631
3830
|
async function shrink(png, maxPx) {
|
|
3632
3831
|
if (process.platform !== "darwin") return png;
|
|
3633
|
-
const dir =
|
|
3634
|
-
const file =
|
|
3832
|
+
const dir = fs5.mkdtempSync(path5.join(os4.tmpdir(), "flowriki-shot-"));
|
|
3833
|
+
const file = path5.join(dir, "s.png");
|
|
3635
3834
|
try {
|
|
3636
|
-
|
|
3835
|
+
fs5.writeFileSync(file, png);
|
|
3637
3836
|
await new Promise((resolve, reject) => {
|
|
3638
3837
|
execFile2("/usr/bin/sips", ["-Z", String(maxPx), file], { timeout: 5e3 }, (error) => error ? reject(error) : resolve());
|
|
3639
3838
|
});
|
|
3640
|
-
return
|
|
3839
|
+
return fs5.readFileSync(file);
|
|
3641
3840
|
} catch {
|
|
3642
3841
|
return png;
|
|
3643
3842
|
} finally {
|
|
3644
|
-
|
|
3843
|
+
fs5.rmSync(dir, { recursive: true, force: true });
|
|
3645
3844
|
}
|
|
3646
3845
|
}
|
|
3647
3846
|
var ANDROID_KEYS = {
|
|
@@ -3901,7 +4100,7 @@ async function runMobileSteps(session, steps, opts) {
|
|
|
3901
4100
|
if (opts.evidenceDir && (shots === "always" || shots === "on-failure" && result.status === "failed")) {
|
|
3902
4101
|
const file = `step-${String((opts.stepOffset ?? 0) + results.length + 1).padStart(2, "0")}.png`;
|
|
3903
4102
|
try {
|
|
3904
|
-
await session.screenshot(
|
|
4103
|
+
await session.screenshot(path6.join(opts.evidenceDir, file));
|
|
3905
4104
|
result.evidence = { screenshot: file, url: await session.currentScreenName(), title: session.deviceName };
|
|
3906
4105
|
} catch {
|
|
3907
4106
|
}
|
|
@@ -3925,7 +4124,7 @@ var typesSecrets = (steps) => steps.some((s) => s.secret || Boolean(s.secretRef)
|
|
|
3925
4124
|
var filmsThis = (policy, steps, evidenceDir) => Boolean(evidenceDir) && Boolean(policy) && policy.mode !== "off" && (policy.allowUnmasked === true || !typesSecrets(steps));
|
|
3926
4125
|
async function finishVideo(session, policy, evidenceDir, passed, name = "phone") {
|
|
3927
4126
|
const keep = Boolean(evidenceDir) && (policy?.mode === "always" || policy?.mode === "on-failure" && !passed);
|
|
3928
|
-
const saved = await session.stopVideo(keep ?
|
|
4127
|
+
const saved = await session.stopVideo(keep ? path7.join(evidenceDir, `video-${name}.mp4`) : null).catch(() => null);
|
|
3929
4128
|
return saved ? [{ kind: "video", relPath: saved.relPath, bytes: saved.bytes }] : [];
|
|
3930
4129
|
}
|
|
3931
4130
|
async function chooseDevice(platform, mobile, asked, where) {
|
|
@@ -4054,20 +4253,20 @@ function stream(file, args, env, cwd, say) {
|
|
|
4054
4253
|
child.stdout.on("data", each);
|
|
4055
4254
|
child.stderr.on("data", each);
|
|
4056
4255
|
child.on("error", reject);
|
|
4057
|
-
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${
|
|
4256
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${path8.basename(file)} ${args.join(" ")} failed (${code})`)));
|
|
4058
4257
|
});
|
|
4059
4258
|
}
|
|
4060
4259
|
async function mobileSetup(say = console.log) {
|
|
4061
4260
|
const npm = onPath("npm");
|
|
4062
4261
|
if (!npm) throw new Error("npm was not found \u2014 install Node.js 20 or newer.");
|
|
4063
|
-
|
|
4064
|
-
if (!
|
|
4065
|
-
|
|
4262
|
+
fs6.mkdirSync(path8.join(APPIUM_DIR, "home"), { recursive: true });
|
|
4263
|
+
if (!fs6.existsSync(path8.join(APPIUM_DIR, "package.json"))) {
|
|
4264
|
+
fs6.writeFileSync(path8.join(APPIUM_DIR, "package.json"), JSON.stringify({ name: "flowriki-appium", private: true }, null, 2));
|
|
4066
4265
|
}
|
|
4067
|
-
const env = { ...await toolEnv(), APPIUM_HOME:
|
|
4266
|
+
const env = { ...await toolEnv(), APPIUM_HOME: path8.join(APPIUM_DIR, "home") };
|
|
4068
4267
|
say("Installing Appium\u2026");
|
|
4069
4268
|
await stream(npm, ["install", "--no-audit", "--no-fund", "appium@2"], env, APPIUM_DIR, say);
|
|
4070
|
-
const bin =
|
|
4269
|
+
const bin = path8.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
|
|
4071
4270
|
const have = await installedDrivers();
|
|
4072
4271
|
const wanted = ["uiautomator2", ...process.platform === "darwin" ? ["xcuitest"] : []];
|
|
4073
4272
|
for (const driver of wanted) {
|
|
@@ -4261,6 +4460,7 @@ export {
|
|
|
4261
4460
|
WEB_CLOUD_BROWSER_NAME,
|
|
4262
4461
|
webCloudOf,
|
|
4263
4462
|
nameKey,
|
|
4463
|
+
usageKindOf,
|
|
4264
4464
|
proposeAssertions,
|
|
4265
4465
|
hasObservableChange,
|
|
4266
4466
|
normalizeStartUrl,
|
|
@@ -4307,6 +4507,8 @@ export {
|
|
|
4307
4507
|
CheckError,
|
|
4308
4508
|
textOf,
|
|
4309
4509
|
checkMobileAssertion,
|
|
4510
|
+
DeviceInUse,
|
|
4511
|
+
lockDevice,
|
|
4310
4512
|
openSessionOn,
|
|
4311
4513
|
MobileSession,
|
|
4312
4514
|
shrink,
|