@flowriki/agent 0.1.0-beta.64 → 0.1.0-beta.66
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(),
|
|
@@ -1446,11 +1454,52 @@ var HttpHelperSchema = z5.object({
|
|
|
1446
1454
|
pattern: z5.string().max(500).default("")
|
|
1447
1455
|
}).strict().default({})
|
|
1448
1456
|
}).strict();
|
|
1457
|
+
var CommandLineSchema = z5.object({
|
|
1458
|
+
program: z5.string().trim().min(1).max(500),
|
|
1459
|
+
args: z5.array(z5.string().max(2e3)).max(50).default([])
|
|
1460
|
+
}).strict();
|
|
1461
|
+
var EnvName = z5.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "an environment variable name is letters, digits and _");
|
|
1462
|
+
var CommandHelperSchema = z5.object({
|
|
1463
|
+
kind: z5.literal("command"),
|
|
1464
|
+
/** What runs everywhere, unless a system has its own below. */
|
|
1465
|
+
run: CommandLineSchema,
|
|
1466
|
+
/** The same job on another system — a different program, or a .cmd on
|
|
1467
|
+
* Windows. A tester's computer uses its own when there is one. */
|
|
1468
|
+
perSystem: z5.object({
|
|
1469
|
+
macos: CommandLineSchema.optional(),
|
|
1470
|
+
windows: CommandLineSchema.optional(),
|
|
1471
|
+
linux: CommandLineSchema.optional()
|
|
1472
|
+
}).strict().default({}),
|
|
1473
|
+
/** Where it runs: a folder inside the project's checkout, or empty for the
|
|
1474
|
+
* project's folder (the agent's own folder when there is no checkout). */
|
|
1475
|
+
folder: z5.string().max(500).default(""),
|
|
1476
|
+
/** Plain environment variables, in the repository. */
|
|
1477
|
+
env: z5.record(EnvName, z5.string().max(2e3)).default({}),
|
|
1478
|
+
/** Environment variable → vault reference: how a token reaches the command
|
|
1479
|
+
* without ever being on its command line. */
|
|
1480
|
+
secretEnv: z5.record(EnvName, SecretRef).default({}),
|
|
1481
|
+
/** `run` waits for it to finish; `background` starts it, waits until it is
|
|
1482
|
+
* ready, and stops it when the test ends — a mock server, a simulator. */
|
|
1483
|
+
mode: z5.enum(["run", "background"]).default("run"),
|
|
1484
|
+
/** Background: the output line that says it is ready. Empty waits a
|
|
1485
|
+
* couple of seconds instead. */
|
|
1486
|
+
readyPattern: z5.string().max(500).default(""),
|
|
1487
|
+
timeoutSeconds: z5.number().int().min(1).max(600).default(60),
|
|
1488
|
+
/** Refused unless it ends with this exit code. */
|
|
1489
|
+
expectExit: z5.number().int().min(0).max(255).default(0),
|
|
1490
|
+
extract: z5.object({
|
|
1491
|
+
take: z5.enum(["output", "pattern", "json", "nothing"]).default("output"),
|
|
1492
|
+
pattern: z5.string().max(500).default(""),
|
|
1493
|
+
/** For json: a dot path — `device.id`. */
|
|
1494
|
+
path: z5.string().max(300).default("")
|
|
1495
|
+
}).strict().default({})
|
|
1496
|
+
}).strict();
|
|
1449
1497
|
var HelperConfigSchema = z5.discriminatedUnion("kind", [
|
|
1450
1498
|
MailboxHelperSchema,
|
|
1451
1499
|
TotpHelperSchema,
|
|
1452
1500
|
SmsHelperSchema,
|
|
1453
|
-
HttpHelperSchema
|
|
1501
|
+
HttpHelperSchema,
|
|
1502
|
+
CommandHelperSchema
|
|
1454
1503
|
]);
|
|
1455
1504
|
var HelperSchema = z5.object({
|
|
1456
1505
|
schemaVersion: z5.literal(HELPER_SCHEMA_VERSION),
|
|
@@ -1467,7 +1516,9 @@ var HelperSchema = z5.object({
|
|
|
1467
1516
|
const patterns = [
|
|
1468
1517
|
helper.config.kind === "mailbox" && helper.config.extract.take === "code" ? helper.config.extract.pattern : "",
|
|
1469
1518
|
helper.config.kind === "sms" ? helper.config.extract.pattern : "",
|
|
1470
|
-
helper.config.kind === "http" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : ""
|
|
1519
|
+
helper.config.kind === "http" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : "",
|
|
1520
|
+
helper.config.kind === "command" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : "",
|
|
1521
|
+
helper.config.kind === "command" ? helper.config.readyPattern : ""
|
|
1471
1522
|
];
|
|
1472
1523
|
for (const pattern of patterns) {
|
|
1473
1524
|
if (pattern && !PatternSchema.safeParse(pattern).success) {
|
|
@@ -1475,6 +1526,12 @@ var HelperSchema = z5.object({
|
|
|
1475
1526
|
}
|
|
1476
1527
|
}
|
|
1477
1528
|
});
|
|
1529
|
+
function commandFor(config, system) {
|
|
1530
|
+
return config.perSystem[system] ?? config.run;
|
|
1531
|
+
}
|
|
1532
|
+
function commandSystemOf(platform) {
|
|
1533
|
+
return platform === "win32" ? "windows" : platform === "darwin" ? "macos" : "linux";
|
|
1534
|
+
}
|
|
1478
1535
|
var DeliveredHelperSchema = z5.object({
|
|
1479
1536
|
id: z5.string().min(1),
|
|
1480
1537
|
name: z5.string(),
|
|
@@ -1832,7 +1889,7 @@ function decodeEntities(text) {
|
|
|
1832
1889
|
var nameKey = (name) => decodeEntities(name).toLowerCase().replace(/&/g, " and ").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
1833
1890
|
var PLATFORM_PART = "(web|android|ios)";
|
|
1834
1891
|
var LEARNED_PATH = new RegExp(`^(explore\\/${PLATFORM_PART}\\.md|memory\\/sessions\\/[a-z0-9][a-z0-9-]{0,120}\\.md)$`);
|
|
1835
|
-
var isLearnedPath = (
|
|
1892
|
+
var isLearnedPath = (path9) => LEARNED_PATH.test(path9);
|
|
1836
1893
|
var LearnedSchema = z11.object({
|
|
1837
1894
|
id: z11.string().regex(/^lrn_[a-z0-9_-]+$/),
|
|
1838
1895
|
projectId: z11.string().min(1),
|
|
@@ -1873,6 +1930,29 @@ var WhatToDoSchema = z13.object({
|
|
|
1873
1930
|
skipped: z13.array(z13.string().min(1).max(80)).max(100),
|
|
1874
1931
|
updatedAt: z13.string()
|
|
1875
1932
|
}).strict();
|
|
1933
|
+
var BLOCKER_KINDS = ["app-bug", "needs-person", "unclear-step", "not-found", "stuck"];
|
|
1934
|
+
var CaseStandingSchema = z14.discriminatedUnion("state", [
|
|
1935
|
+
z14.object({
|
|
1936
|
+
state: z14.literal("blocked"),
|
|
1937
|
+
kind: z14.enum(BLOCKER_KINDS),
|
|
1938
|
+
/** The business step it stopped at, counted from 1 — when it was one. */
|
|
1939
|
+
line: z14.number().int().positive().max(500).optional(),
|
|
1940
|
+
lineText: z14.string().max(500).optional(),
|
|
1941
|
+
detail: z14.string().min(1).max(1500),
|
|
1942
|
+
/** What would unblock it, said for a person. */
|
|
1943
|
+
needs: z14.string().max(600).optional(),
|
|
1944
|
+
platform: z14.enum(["web", "android", "ios"]).optional(),
|
|
1945
|
+
by: z14.string().max(200),
|
|
1946
|
+
at: z14.string()
|
|
1947
|
+
}).strict(),
|
|
1948
|
+
z14.object({
|
|
1949
|
+
state: z14.literal("manual"),
|
|
1950
|
+
/** Why it is run by hand, when somebody said. */
|
|
1951
|
+
reason: z14.string().max(600).optional(),
|
|
1952
|
+
by: z14.string().max(200),
|
|
1953
|
+
at: z14.string()
|
|
1954
|
+
}).strict()
|
|
1955
|
+
]);
|
|
1876
1956
|
var CaseRegistrySchema = z14.object({
|
|
1877
1957
|
id: z14.string().regex(/^creg_/),
|
|
1878
1958
|
projectId: z14.string().min(1),
|
|
@@ -1880,11 +1960,83 @@ var CaseRegistrySchema = z14.object({
|
|
|
1880
1960
|
cases: z14.record(z14.string(), z14.number().int().positive()),
|
|
1881
1961
|
features: z14.record(z14.string(), z14.number().int().positive()),
|
|
1882
1962
|
reviews: z14.record(z14.string(), z14.object({ hash: z14.string(), by: z14.string().max(200), at: z14.string() }).strict()),
|
|
1963
|
+
/** Blocked and manual cases, by case id. Absent on a project with none. */
|
|
1964
|
+
standing: z14.record(z14.string(), CaseStandingSchema).optional(),
|
|
1883
1965
|
updatedAt: z14.string()
|
|
1884
1966
|
}).strict();
|
|
1967
|
+
var BranchChoiceSchema = z15.object({
|
|
1968
|
+
id: z15.string().regex(/^bch_/),
|
|
1969
|
+
projectId: z15.string().min(1),
|
|
1970
|
+
userId: z15.string().min(1),
|
|
1971
|
+
/** The branch (a change id), or null for the published version. */
|
|
1972
|
+
changeId: z15.string().min(1).max(100).nullable(),
|
|
1973
|
+
at: z15.string()
|
|
1974
|
+
}).strict();
|
|
1975
|
+
var TokensSchema = z16.object({
|
|
1976
|
+
/** Read fresh — the request, minus what came from the cache. */
|
|
1977
|
+
input: z16.number().int().nonnegative(),
|
|
1978
|
+
output: z16.number().int().nonnegative(),
|
|
1979
|
+
/** Read from the prompt cache: a tenth of the price of fresh input. */
|
|
1980
|
+
cacheRead: z16.number().int().nonnegative(),
|
|
1981
|
+
/** Written to the prompt cache, so later turns can read it cheaply. */
|
|
1982
|
+
cacheWrite: z16.number().int().nonnegative()
|
|
1983
|
+
}).strict();
|
|
1984
|
+
var USAGE_STAGES = ["write", "automate", "setup", "knowledge", "reply"];
|
|
1985
|
+
var USAGE_KINDS = ["automate", "reautomate", "heal", "update", "write", "explore", "chat"];
|
|
1986
|
+
function usageKindOf(shown) {
|
|
1987
|
+
const s = shown.trim();
|
|
1988
|
+
if (/^Heal\b/i.test(s)) return "heal";
|
|
1989
|
+
if (/^Re-automate\b/i.test(s)) return "reautomate";
|
|
1990
|
+
if (/^Automate\b/i.test(s)) return "automate";
|
|
1991
|
+
if (/business steps of .* (just )?changed/i.test(s) || /^Update\b/i.test(s)) return "update";
|
|
1992
|
+
if (/^Explore\b|\bexplore (the|this) (app|site|website)\b/i.test(s)) return "explore";
|
|
1993
|
+
if (/^(Write|Import)\b.*test cases?/i.test(s)) return "write";
|
|
1994
|
+
return "chat";
|
|
1995
|
+
}
|
|
1996
|
+
var UsagePartSchema = TokensSchema.extend({
|
|
1997
|
+
stage: z16.enum(USAGE_STAGES),
|
|
1998
|
+
/** The tool the turn called; null for a turn that called none. */
|
|
1999
|
+
tool: z16.string().max(80).nullable(),
|
|
2000
|
+
/** The test case the work was for, when one was being worked on. */
|
|
2001
|
+
caseName: z16.string().max(300).nullable(),
|
|
2002
|
+
folder: z16.string().max(300).nullable(),
|
|
2003
|
+
/** Its id — TC-12 — when the assistant named it by id. */
|
|
2004
|
+
caseKey: z16.string().max(40).nullable(),
|
|
2005
|
+
/** The flow being recorded, when one was. */
|
|
2006
|
+
flow: z16.string().max(300).nullable(),
|
|
2007
|
+
/** How many turns are in this line. */
|
|
2008
|
+
turns: z16.number().int().nonnegative()
|
|
2009
|
+
}).strict();
|
|
2010
|
+
var UsageRecordSchema = z16.object({
|
|
2011
|
+
id: z16.string().regex(/^use_/),
|
|
2012
|
+
projectId: z16.string().min(1),
|
|
2013
|
+
userId: z16.string().min(1),
|
|
2014
|
+
jobId: z16.string().min(1),
|
|
2015
|
+
kind: z16.enum(USAGE_KINDS),
|
|
2016
|
+
/** The start of what was asked, so a list of jobs reads as one. */
|
|
2017
|
+
asked: z16.string().max(200),
|
|
2018
|
+
platform: z16.enum(["web", "android", "ios"]).nullable(),
|
|
2019
|
+
startedAt: z16.string(),
|
|
2020
|
+
finishedAt: z16.string().nullable(),
|
|
2021
|
+
status: z16.enum(["running", "done", "failed", "stopped"]),
|
|
2022
|
+
/** Every turn added up, from the turns as they happened. */
|
|
2023
|
+
totals: TokensSchema,
|
|
2024
|
+
turns: z16.number().int().nonnegative(),
|
|
2025
|
+
/** What the model said the whole job cost, in US dollars — at API prices;
|
|
2026
|
+
* on a Claude plan it is what the same work would cost there. */
|
|
2027
|
+
costUsd: z16.number().nonnegative().nullable(),
|
|
2028
|
+
durationMs: z16.number().int().nonnegative().nullable(),
|
|
2029
|
+
parts: z16.array(UsagePartSchema).max(600)
|
|
2030
|
+
}).strict();
|
|
2031
|
+
var StepRenameSchema = z17.object({
|
|
2032
|
+
occurrenceId: z17.string().min(1).max(200),
|
|
2033
|
+
stepId: z17.string().min(1).max(200),
|
|
2034
|
+
from: z17.string().min(1).max(500),
|
|
2035
|
+
to: z17.string().trim().min(1).max(200)
|
|
2036
|
+
}).strict();
|
|
1885
2037
|
|
|
1886
2038
|
// ../engine-appium/dist/index.js
|
|
1887
|
-
import
|
|
2039
|
+
import path6 from "path";
|
|
1888
2040
|
|
|
1889
2041
|
// ../core/dist/index.js
|
|
1890
2042
|
function proposeAssertions(before, after) {
|
|
@@ -2217,9 +2369,9 @@ var STEP_PRESENTATION = {
|
|
|
2217
2369
|
};
|
|
2218
2370
|
|
|
2219
2371
|
// ../engine-appium/dist/index.js
|
|
2220
|
-
import path6 from "path";
|
|
2221
|
-
import fs5 from "fs";
|
|
2222
2372
|
import path7 from "path";
|
|
2373
|
+
import fs6 from "fs";
|
|
2374
|
+
import path8 from "path";
|
|
2223
2375
|
import { spawn as spawn3 } from "child_process";
|
|
2224
2376
|
var WebDriverError = class extends Error {
|
|
2225
2377
|
constructor(message, error, status) {
|
|
@@ -2231,10 +2383,10 @@ var WebDriverError = class extends Error {
|
|
|
2231
2383
|
status;
|
|
2232
2384
|
};
|
|
2233
2385
|
var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecc";
|
|
2234
|
-
async function call(endpoint, method,
|
|
2386
|
+
async function call(endpoint, method, path9, body, timeoutMs = 12e4) {
|
|
2235
2387
|
const headers = { "content-type": "application/json; charset=utf-8" };
|
|
2236
2388
|
if (endpoint.username) headers["authorization"] = `Basic ${Buffer.from(`${endpoint.username}:${endpoint.password ?? ""}`).toString("base64")}`;
|
|
2237
|
-
const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${
|
|
2389
|
+
const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path9}`, {
|
|
2238
2390
|
method,
|
|
2239
2391
|
headers,
|
|
2240
2392
|
...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {},
|
|
@@ -2273,11 +2425,11 @@ var WebDriverSession = class _WebDriverSession {
|
|
|
2273
2425
|
return new _WebDriverSession(endpoint, value.sessionId, value.capabilities ?? {});
|
|
2274
2426
|
}
|
|
2275
2427
|
/** Any other command — Appium's own extensions live under /appium/. */
|
|
2276
|
-
raw(method,
|
|
2277
|
-
return this.req(method,
|
|
2428
|
+
raw(method, path9, body, timeoutMs) {
|
|
2429
|
+
return this.req(method, path9, body, timeoutMs);
|
|
2278
2430
|
}
|
|
2279
|
-
req(method,
|
|
2280
|
-
return call(this.endpoint, method, `/session/${this.id}${
|
|
2431
|
+
req(method, path9, body, timeoutMs) {
|
|
2432
|
+
return call(this.endpoint, method, `/session/${this.id}${path9}`, body, timeoutMs);
|
|
2281
2433
|
}
|
|
2282
2434
|
source() {
|
|
2283
2435
|
return this.req("GET", "/source");
|
|
@@ -2380,8 +2532,8 @@ function appiumPath() {
|
|
|
2380
2532
|
}
|
|
2381
2533
|
function javaMajor(home) {
|
|
2382
2534
|
try {
|
|
2383
|
-
const
|
|
2384
|
-
const v = /JAVA_VERSION="(\d+)(?:\.(\d+))?/.exec(
|
|
2535
|
+
const release2 = fs.readFileSync(path.join(home, "release"), "utf8");
|
|
2536
|
+
const v = /JAVA_VERSION="(\d+)(?:\.(\d+))?/.exec(release2);
|
|
2385
2537
|
if (!v) return 0;
|
|
2386
2538
|
return v[1] === "1" ? Number(v[2] ?? 0) : Number(v[1]);
|
|
2387
2539
|
} catch {
|
|
@@ -3304,6 +3456,88 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
|
|
|
3304
3456
|
}
|
|
3305
3457
|
throw last;
|
|
3306
3458
|
}
|
|
3459
|
+
var held = /* @__PURE__ */ new Map();
|
|
3460
|
+
var lockDir = () => path4.join(process.env["FLOWRIKI_HOME"] || AGENT_HOME, "locks");
|
|
3461
|
+
var fileFor = (udid) => path4.join(lockDir(), `device-${udid.replace(/[^A-Za-z0-9._-]/g, "_")}.lock`);
|
|
3462
|
+
var alive = (pid) => {
|
|
3463
|
+
try {
|
|
3464
|
+
process.kill(pid, 0);
|
|
3465
|
+
return true;
|
|
3466
|
+
} catch (error) {
|
|
3467
|
+
return error.code === "EPERM";
|
|
3468
|
+
}
|
|
3469
|
+
};
|
|
3470
|
+
var DeviceInUse = class extends Error {
|
|
3471
|
+
constructor(device, pid) {
|
|
3472
|
+
super(`${device} is being used by another Flowriki job on this computer \u2014 wait for it to finish, or stop it first`);
|
|
3473
|
+
this.device = device;
|
|
3474
|
+
this.pid = pid;
|
|
3475
|
+
this.name = "DeviceInUse";
|
|
3476
|
+
}
|
|
3477
|
+
device;
|
|
3478
|
+
pid;
|
|
3479
|
+
};
|
|
3480
|
+
function lockDevice(udid, name = udid) {
|
|
3481
|
+
const file = fileFor(udid);
|
|
3482
|
+
const mine = held.get(file);
|
|
3483
|
+
if (mine) {
|
|
3484
|
+
held.set(file, mine + 1);
|
|
3485
|
+
return once(() => release(file));
|
|
3486
|
+
}
|
|
3487
|
+
fs4.mkdirSync(lockDir(), { recursive: true, mode: 448 });
|
|
3488
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3489
|
+
try {
|
|
3490
|
+
const fd = fs4.openSync(file, "wx", 384);
|
|
3491
|
+
fs4.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
3492
|
+
fs4.closeSync(fd);
|
|
3493
|
+
held.set(file, 1);
|
|
3494
|
+
return once(() => release(file));
|
|
3495
|
+
} catch (error) {
|
|
3496
|
+
if (error.code !== "EEXIST") throw error;
|
|
3497
|
+
let owner = 0;
|
|
3498
|
+
try {
|
|
3499
|
+
owner = Number(JSON.parse(fs4.readFileSync(file, "utf8")).pid) || 0;
|
|
3500
|
+
} catch {
|
|
3501
|
+
}
|
|
3502
|
+
if (owner && owner !== process.pid && alive(owner)) throw new DeviceInUse(name, owner);
|
|
3503
|
+
try {
|
|
3504
|
+
fs4.unlinkSync(file);
|
|
3505
|
+
} catch {
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
throw new DeviceInUse(name, 0);
|
|
3510
|
+
}
|
|
3511
|
+
function release(file) {
|
|
3512
|
+
const count = held.get(file) ?? 0;
|
|
3513
|
+
if (count > 1) {
|
|
3514
|
+
held.set(file, count - 1);
|
|
3515
|
+
return;
|
|
3516
|
+
}
|
|
3517
|
+
held.delete(file);
|
|
3518
|
+
try {
|
|
3519
|
+
const owner = Number(JSON.parse(fs4.readFileSync(file, "utf8")).pid);
|
|
3520
|
+
if (owner === process.pid) fs4.unlinkSync(file);
|
|
3521
|
+
} catch {
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
function once(fn) {
|
|
3525
|
+
let done = false;
|
|
3526
|
+
return () => {
|
|
3527
|
+
if (!done) {
|
|
3528
|
+
done = true;
|
|
3529
|
+
fn();
|
|
3530
|
+
}
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
process.once("exit", () => {
|
|
3534
|
+
for (const file of held.keys()) {
|
|
3535
|
+
try {
|
|
3536
|
+
fs4.unlinkSync(file);
|
|
3537
|
+
} catch {
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3307
3541
|
var uploaded = /* @__PURE__ */ new Map();
|
|
3308
3542
|
var OPEN = /* @__PURE__ */ new Map();
|
|
3309
3543
|
var openSessionOn = (platform) => OPEN.get(platform) ?? null;
|
|
@@ -3321,6 +3555,8 @@ var MobileSession = class _MobileSession {
|
|
|
3321
3555
|
deviceName;
|
|
3322
3556
|
cloud;
|
|
3323
3557
|
cached = null;
|
|
3558
|
+
/** Lets go of this phone for other Flowriki processes — see device-lock. */
|
|
3559
|
+
releaseDevice = null;
|
|
3324
3560
|
static async open(target, opts = {}) {
|
|
3325
3561
|
const app = appOf(target);
|
|
3326
3562
|
const say = opts.onProgress ?? (() => {
|
|
@@ -3342,11 +3578,11 @@ var MobileSession = class _MobileSession {
|
|
|
3342
3578
|
}
|
|
3343
3579
|
}
|
|
3344
3580
|
say(`Opening ${choice2.device} on ${CLOUD_PROVIDER_NAME[choice2.provider]}\u2026`);
|
|
3345
|
-
const { endpoint
|
|
3346
|
-
const
|
|
3581
|
+
const { endpoint, capabilities } = cloudCapabilities(target, account, key, appRef);
|
|
3582
|
+
const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
|
|
3347
3583
|
throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
|
|
3348
3584
|
});
|
|
3349
|
-
const cloudSession = new _MobileSession(
|
|
3585
|
+
const cloudSession = new _MobileSession(driver, target.platform, app.id, choice2.device, true);
|
|
3350
3586
|
OPEN.set(target.platform, cloudSession);
|
|
3351
3587
|
await cloudSession.settle(2e4);
|
|
3352
3588
|
return cloudSession;
|
|
@@ -3358,23 +3594,30 @@ var MobileSession = class _MobileSession {
|
|
|
3358
3594
|
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
3595
|
if (known.state !== "ready") say(`Starting ${known.name}\u2026`);
|
|
3360
3596
|
const udid = await bootDevice(known);
|
|
3361
|
-
const
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
const
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3597
|
+
const releaseDevice = lockDevice(udid, known.name);
|
|
3598
|
+
try {
|
|
3599
|
+
const endpoint = await appiumServer();
|
|
3600
|
+
const buildFile = app.build ? await localBuild(app.build) : null;
|
|
3601
|
+
say(`Opening the app on ${known.name}\u2026`);
|
|
3602
|
+
const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
|
|
3603
|
+
const activity = target.platform === "android" && !app.activity && !buildFile ? await launchActivityOf(udid, app.id) : null;
|
|
3604
|
+
const capabilities = await localCapabilities(target, udid, { buildFile, ports, ...activity ? { activity } : {} });
|
|
3605
|
+
const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
|
|
3606
|
+
const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
|
|
3607
|
+
throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
|
|
3608
|
+
});
|
|
3609
|
+
const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
|
|
3610
|
+
session.releaseDevice = releaseDevice;
|
|
3611
|
+
OPEN.set(target.platform, session);
|
|
3612
|
+
await session.terminateApp().catch(() => {
|
|
3613
|
+
});
|
|
3614
|
+
await session.launchApp().catch(() => {
|
|
3615
|
+
});
|
|
3616
|
+
return session;
|
|
3617
|
+
} catch (error) {
|
|
3618
|
+
releaseDevice();
|
|
3619
|
+
throw error;
|
|
3620
|
+
}
|
|
3378
3621
|
}
|
|
3379
3622
|
// ── Reading ────────────────────────────────────────────────────────────
|
|
3380
3623
|
async screen() {
|
|
@@ -3405,8 +3648,8 @@ var MobileSession = class _MobileSession {
|
|
|
3405
3648
|
async screenshot(file) {
|
|
3406
3649
|
const png = Buffer.from(await this.driver.screenshot(), "base64");
|
|
3407
3650
|
if (file) {
|
|
3408
|
-
|
|
3409
|
-
|
|
3651
|
+
fs5.mkdirSync(path5.dirname(file), { recursive: true });
|
|
3652
|
+
fs5.writeFileSync(file, png);
|
|
3410
3653
|
}
|
|
3411
3654
|
return png;
|
|
3412
3655
|
}
|
|
@@ -3607,14 +3850,19 @@ var MobileSession = class _MobileSession {
|
|
|
3607
3850
|
const b64 = await this.driver.raw("POST", "/appium/stop_recording_screen", { options: {} }, 12e4).catch(() => "");
|
|
3608
3851
|
if (!file || !b64) return null;
|
|
3609
3852
|
const bytes = Buffer.from(b64, "base64");
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
return { relPath:
|
|
3853
|
+
fs5.mkdirSync(path5.dirname(file), { recursive: true });
|
|
3854
|
+
fs5.writeFileSync(file, bytes);
|
|
3855
|
+
return { relPath: path5.basename(file), bytes: bytes.length };
|
|
3613
3856
|
}
|
|
3614
3857
|
async close() {
|
|
3615
3858
|
if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
|
|
3616
|
-
|
|
3617
|
-
|
|
3859
|
+
try {
|
|
3860
|
+
if (this.recording) await this.stopVideo(null);
|
|
3861
|
+
await this.driver.close();
|
|
3862
|
+
} finally {
|
|
3863
|
+
this.releaseDevice?.();
|
|
3864
|
+
this.releaseDevice = null;
|
|
3865
|
+
}
|
|
3618
3866
|
}
|
|
3619
3867
|
};
|
|
3620
3868
|
async function launchActivityOf(udid, appPackage) {
|
|
@@ -3630,18 +3878,18 @@ async function launchActivityOf(udid, appPackage) {
|
|
|
3630
3878
|
}
|
|
3631
3879
|
async function shrink(png, maxPx) {
|
|
3632
3880
|
if (process.platform !== "darwin") return png;
|
|
3633
|
-
const dir =
|
|
3634
|
-
const file =
|
|
3881
|
+
const dir = fs5.mkdtempSync(path5.join(os4.tmpdir(), "flowriki-shot-"));
|
|
3882
|
+
const file = path5.join(dir, "s.png");
|
|
3635
3883
|
try {
|
|
3636
|
-
|
|
3884
|
+
fs5.writeFileSync(file, png);
|
|
3637
3885
|
await new Promise((resolve, reject) => {
|
|
3638
3886
|
execFile2("/usr/bin/sips", ["-Z", String(maxPx), file], { timeout: 5e3 }, (error) => error ? reject(error) : resolve());
|
|
3639
3887
|
});
|
|
3640
|
-
return
|
|
3888
|
+
return fs5.readFileSync(file);
|
|
3641
3889
|
} catch {
|
|
3642
3890
|
return png;
|
|
3643
3891
|
} finally {
|
|
3644
|
-
|
|
3892
|
+
fs5.rmSync(dir, { recursive: true, force: true });
|
|
3645
3893
|
}
|
|
3646
3894
|
}
|
|
3647
3895
|
var ANDROID_KEYS = {
|
|
@@ -3901,7 +4149,7 @@ async function runMobileSteps(session, steps, opts) {
|
|
|
3901
4149
|
if (opts.evidenceDir && (shots === "always" || shots === "on-failure" && result.status === "failed")) {
|
|
3902
4150
|
const file = `step-${String((opts.stepOffset ?? 0) + results.length + 1).padStart(2, "0")}.png`;
|
|
3903
4151
|
try {
|
|
3904
|
-
await session.screenshot(
|
|
4152
|
+
await session.screenshot(path6.join(opts.evidenceDir, file));
|
|
3905
4153
|
result.evidence = { screenshot: file, url: await session.currentScreenName(), title: session.deviceName };
|
|
3906
4154
|
} catch {
|
|
3907
4155
|
}
|
|
@@ -3925,7 +4173,7 @@ var typesSecrets = (steps) => steps.some((s) => s.secret || Boolean(s.secretRef)
|
|
|
3925
4173
|
var filmsThis = (policy, steps, evidenceDir) => Boolean(evidenceDir) && Boolean(policy) && policy.mode !== "off" && (policy.allowUnmasked === true || !typesSecrets(steps));
|
|
3926
4174
|
async function finishVideo(session, policy, evidenceDir, passed, name = "phone") {
|
|
3927
4175
|
const keep = Boolean(evidenceDir) && (policy?.mode === "always" || policy?.mode === "on-failure" && !passed);
|
|
3928
|
-
const saved = await session.stopVideo(keep ?
|
|
4176
|
+
const saved = await session.stopVideo(keep ? path7.join(evidenceDir, `video-${name}.mp4`) : null).catch(() => null);
|
|
3929
4177
|
return saved ? [{ kind: "video", relPath: saved.relPath, bytes: saved.bytes }] : [];
|
|
3930
4178
|
}
|
|
3931
4179
|
async function chooseDevice(platform, mobile, asked, where) {
|
|
@@ -4054,20 +4302,20 @@ function stream(file, args, env, cwd, say) {
|
|
|
4054
4302
|
child.stdout.on("data", each);
|
|
4055
4303
|
child.stderr.on("data", each);
|
|
4056
4304
|
child.on("error", reject);
|
|
4057
|
-
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${
|
|
4305
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${path8.basename(file)} ${args.join(" ")} failed (${code})`)));
|
|
4058
4306
|
});
|
|
4059
4307
|
}
|
|
4060
4308
|
async function mobileSetup(say = console.log) {
|
|
4061
4309
|
const npm = onPath("npm");
|
|
4062
4310
|
if (!npm) throw new Error("npm was not found \u2014 install Node.js 20 or newer.");
|
|
4063
|
-
|
|
4064
|
-
if (!
|
|
4065
|
-
|
|
4311
|
+
fs6.mkdirSync(path8.join(APPIUM_DIR, "home"), { recursive: true });
|
|
4312
|
+
if (!fs6.existsSync(path8.join(APPIUM_DIR, "package.json"))) {
|
|
4313
|
+
fs6.writeFileSync(path8.join(APPIUM_DIR, "package.json"), JSON.stringify({ name: "flowriki-appium", private: true }, null, 2));
|
|
4066
4314
|
}
|
|
4067
|
-
const env = { ...await toolEnv(), APPIUM_HOME:
|
|
4315
|
+
const env = { ...await toolEnv(), APPIUM_HOME: path8.join(APPIUM_DIR, "home") };
|
|
4068
4316
|
say("Installing Appium\u2026");
|
|
4069
4317
|
await stream(npm, ["install", "--no-audit", "--no-fund", "appium@2"], env, APPIUM_DIR, say);
|
|
4070
|
-
const bin =
|
|
4318
|
+
const bin = path8.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
|
|
4071
4319
|
const have = await installedDrivers();
|
|
4072
4320
|
const wanted = ["uiautomator2", ...process.platform === "darwin" ? ["xcuitest"] : []];
|
|
4073
4321
|
for (const driver of wanted) {
|
|
@@ -4253,6 +4501,8 @@ export {
|
|
|
4253
4501
|
sameIdentity,
|
|
4254
4502
|
PrerequisiteProblem,
|
|
4255
4503
|
expandForRun,
|
|
4504
|
+
commandFor,
|
|
4505
|
+
commandSystemOf,
|
|
4256
4506
|
DeliveredHelperSchema,
|
|
4257
4507
|
exploreSummary,
|
|
4258
4508
|
CODEBASE_TAG_NAME,
|
|
@@ -4261,6 +4511,7 @@ export {
|
|
|
4261
4511
|
WEB_CLOUD_BROWSER_NAME,
|
|
4262
4512
|
webCloudOf,
|
|
4263
4513
|
nameKey,
|
|
4514
|
+
usageKindOf,
|
|
4264
4515
|
proposeAssertions,
|
|
4265
4516
|
hasObservableChange,
|
|
4266
4517
|
normalizeStartUrl,
|
|
@@ -4307,6 +4558,8 @@ export {
|
|
|
4307
4558
|
CheckError,
|
|
4308
4559
|
textOf,
|
|
4309
4560
|
checkMobileAssertion,
|
|
4561
|
+
DeviceInUse,
|
|
4562
|
+
lockDevice,
|
|
4310
4563
|
openSessionOn,
|
|
4311
4564
|
MobileSession,
|
|
4312
4565
|
shrink,
|