adb-ready 0.6.0 → 0.8.0
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/CHANGELOG.md +39 -1
- package/COMPATIBILITY.md +31 -0
- package/README.md +18 -1
- package/dist/cli.js +2730 -80
- package/docs/automation.md +89 -0
- package/docs/completions.md +61 -0
- package/docs/configuration.md +6 -0
- package/docs/firebase-test-lab.md +126 -0
- package/docs/gradle-managed-devices.md +105 -0
- package/docs/target-pools.md +151 -0
- package/docs/targets-and-wireless.md +21 -0
- package/docs/troubleshooting.md +3 -0
- package/examples/README.md +6 -0
- package/examples/ci-emulator/README.md +32 -0
- package/examples/ci-emulator/adb-ready.config.json +20 -0
- package/examples/ci-emulator/dev-service.mjs +14 -0
- package/examples/ci-emulator/verify-emulator.mjs +108 -0
- package/examples/ci-physical/README.md +51 -0
- package/examples/ci-physical/adb-ready.config.json +20 -0
- package/examples/ci-physical/dev-service.mjs +14 -0
- package/examples/ci-physical/github-actions.yml +56 -0
- package/examples/ci-physical/run-job.mjs +77 -0
- package/examples/ci-physical/verify-device.mjs +90 -0
- package/examples/target-pools/README.md +22 -0
- package/examples/target-pools/adb-ready.config.json +29 -0
- package/package.json +2 -2
- package/schema/agent-tools-v1.json +1 -1
- package/schema/config-v1.schema.json +86 -0
- package/skills/adb-ready/SKILL.md +2 -2
package/dist/cli.js
CHANGED
|
@@ -3298,13 +3298,13 @@ async function activateAgentTaskFromEnvironment(env) {
|
|
|
3298
3298
|
}
|
|
3299
3299
|
|
|
3300
3300
|
// src/cli/main.ts
|
|
3301
|
-
import { randomUUID as
|
|
3302
|
-
import
|
|
3301
|
+
import { randomUUID as randomUUID17 } from "node:crypto";
|
|
3302
|
+
import path26 from "node:path";
|
|
3303
3303
|
import process15 from "node:process";
|
|
3304
3304
|
// package.json
|
|
3305
3305
|
var package_default = {
|
|
3306
3306
|
name: "adb-ready",
|
|
3307
|
-
version: "0.
|
|
3307
|
+
version: "0.8.0",
|
|
3308
3308
|
description: "Agent-ready Android CLI for reliable ADB sessions, app automation, and verified evidence.",
|
|
3309
3309
|
private: false,
|
|
3310
3310
|
type: "module",
|
|
@@ -3403,7 +3403,7 @@ var package_default = {
|
|
|
3403
3403
|
"@yarnpkg/cli-dist": "4.18.0",
|
|
3404
3404
|
npm: "12.0.2",
|
|
3405
3405
|
publint: "0.3.24",
|
|
3406
|
-
runpalette: "0.
|
|
3406
|
+
runpalette: "0.5.0",
|
|
3407
3407
|
tinyexec: "1.3.1",
|
|
3408
3408
|
typescript: "7.0.2",
|
|
3409
3409
|
yarn: "1.22.22"
|
|
@@ -37551,6 +37551,7 @@ async function runProcess(request) {
|
|
|
37551
37551
|
let spawnError;
|
|
37552
37552
|
let streamError;
|
|
37553
37553
|
return await new Promise((resolve) => {
|
|
37554
|
+
const ownsProcessGroup = request.killProcessGroup === true;
|
|
37554
37555
|
const child = spawn2(request.executable, args, {
|
|
37555
37556
|
cwd: request.cwd,
|
|
37556
37557
|
env: request.inheritEnv === false ? request.env : { ...process5.env, ...request.env },
|
|
@@ -37560,21 +37561,41 @@ async function runProcess(request) {
|
|
|
37560
37561
|
"pipe",
|
|
37561
37562
|
"pipe"
|
|
37562
37563
|
],
|
|
37563
|
-
windowsHide: true
|
|
37564
|
+
windowsHide: true,
|
|
37565
|
+
detached: ownsProcessGroup && process5.platform !== "win32"
|
|
37564
37566
|
});
|
|
37565
37567
|
const killSignal = request.killSignal ?? "SIGTERM";
|
|
37566
37568
|
let idleTimer;
|
|
37567
37569
|
let escalationTimer;
|
|
37570
|
+
let closed = false;
|
|
37571
|
+
const signalOwnedProcess = (signal) => {
|
|
37572
|
+
if (ownsProcessGroup && process5.platform === "win32" && child.pid !== undefined) {
|
|
37573
|
+
killEscalated = true;
|
|
37574
|
+
spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
37575
|
+
shell: false,
|
|
37576
|
+
stdio: "ignore",
|
|
37577
|
+
windowsHide: true
|
|
37578
|
+
}).once("error", () => child.kill(signal));
|
|
37579
|
+
return;
|
|
37580
|
+
}
|
|
37581
|
+
if (ownsProcessGroup && child.pid !== undefined) {
|
|
37582
|
+
try {
|
|
37583
|
+
process5.kill(-child.pid, signal);
|
|
37584
|
+
return;
|
|
37585
|
+
} catch {}
|
|
37586
|
+
}
|
|
37587
|
+
child.kill(signal);
|
|
37588
|
+
};
|
|
37568
37589
|
const terminate = () => {
|
|
37569
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
37590
|
+
if (closed || !ownsProcessGroup && (child.exitCode !== null || child.signalCode !== null)) {
|
|
37570
37591
|
return;
|
|
37571
37592
|
}
|
|
37572
|
-
|
|
37593
|
+
signalOwnedProcess(killSignal);
|
|
37573
37594
|
if (killSignal !== "SIGKILL" && escalationTimer === undefined) {
|
|
37574
37595
|
escalationTimer = setTimeout(() => {
|
|
37575
|
-
if (
|
|
37596
|
+
if (!closed) {
|
|
37576
37597
|
killEscalated = true;
|
|
37577
|
-
|
|
37598
|
+
signalOwnedProcess("SIGKILL");
|
|
37578
37599
|
}
|
|
37579
37600
|
}, killGraceMs);
|
|
37580
37601
|
}
|
|
@@ -37649,6 +37670,7 @@ async function runProcess(request) {
|
|
|
37649
37670
|
};
|
|
37650
37671
|
});
|
|
37651
37672
|
child.once("close", (exitCode, signal) => {
|
|
37673
|
+
closed = true;
|
|
37652
37674
|
if (timeout !== undefined) {
|
|
37653
37675
|
clearTimeout(timeout);
|
|
37654
37676
|
}
|
|
@@ -38063,6 +38085,137 @@ function parseLogcatThreadtimeLine(raw) {
|
|
|
38063
38085
|
return { timestamp, pid, tid, priority, tag, message, raw: line };
|
|
38064
38086
|
}
|
|
38065
38087
|
|
|
38088
|
+
// src/adb/remote-server-probe.ts
|
|
38089
|
+
import { createConnection } from "node:net";
|
|
38090
|
+
var REQUEST = "000chost:version";
|
|
38091
|
+
var MAX_RESPONSE_BYTES = 4096;
|
|
38092
|
+
function normalizedSocketHost(host) {
|
|
38093
|
+
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
38094
|
+
}
|
|
38095
|
+
function protocolFrame(buffer) {
|
|
38096
|
+
if (buffer.length < 4)
|
|
38097
|
+
return { kind: "incomplete" };
|
|
38098
|
+
const status = buffer.subarray(0, 4).toString("ascii");
|
|
38099
|
+
if (status !== "OKAY" && status !== "FAIL") {
|
|
38100
|
+
return { kind: "invalid", message: "The server returned an unknown ADB status." };
|
|
38101
|
+
}
|
|
38102
|
+
if (buffer.length < 8)
|
|
38103
|
+
return { kind: "incomplete" };
|
|
38104
|
+
const lengthText = buffer.subarray(4, 8).toString("ascii");
|
|
38105
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(lengthText)) {
|
|
38106
|
+
return { kind: "invalid", message: "The server returned an invalid ADB payload length." };
|
|
38107
|
+
}
|
|
38108
|
+
const length = Number.parseInt(lengthText, 16);
|
|
38109
|
+
if (length > MAX_RESPONSE_BYTES) {
|
|
38110
|
+
return { kind: "invalid", message: "The server returned an oversized ADB payload." };
|
|
38111
|
+
}
|
|
38112
|
+
if (buffer.length < 8 + length)
|
|
38113
|
+
return { kind: "incomplete" };
|
|
38114
|
+
const payload = buffer.subarray(8, 8 + length).toString("utf8");
|
|
38115
|
+
return status === "OKAY" ? { kind: "okay", payload } : { kind: "rejected", message: payload || "The server rejected host:version." };
|
|
38116
|
+
}
|
|
38117
|
+
function adbProtocolVersion(value) {
|
|
38118
|
+
const match = value?.match(/^1\.0\.(\d+)$/u);
|
|
38119
|
+
if (match?.[1] === undefined)
|
|
38120
|
+
return;
|
|
38121
|
+
const parsed = Number(match[1]);
|
|
38122
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 && parsed <= 65535 ? parsed : undefined;
|
|
38123
|
+
}
|
|
38124
|
+
var probeRemoteAdbServer = async (options) => {
|
|
38125
|
+
if (options.host.trim() === "")
|
|
38126
|
+
throw new RangeError("remote ADB host cannot be empty");
|
|
38127
|
+
if (!Number.isSafeInteger(options.port) || options.port < 1 || options.port > 65535) {
|
|
38128
|
+
throw new RangeError("remote ADB port must be from 1 to 65535");
|
|
38129
|
+
}
|
|
38130
|
+
if (!Number.isSafeInteger(options.expectedProtocolVersion) || options.expectedProtocolVersion < 0 || options.expectedProtocolVersion > 65535) {
|
|
38131
|
+
throw new RangeError("expected ADB protocol version must be from 0 to 65535");
|
|
38132
|
+
}
|
|
38133
|
+
if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 60000) {
|
|
38134
|
+
throw new RangeError("remote ADB probe timeout must be from 1 to 60000");
|
|
38135
|
+
}
|
|
38136
|
+
if (options.signal?.aborted === true) {
|
|
38137
|
+
return { ok: false, kind: "cancelled", message: "Remote ADB preflight was cancelled." };
|
|
38138
|
+
}
|
|
38139
|
+
return await new Promise((resolve) => {
|
|
38140
|
+
const socket = createConnection({
|
|
38141
|
+
host: normalizedSocketHost(options.host),
|
|
38142
|
+
port: options.port
|
|
38143
|
+
});
|
|
38144
|
+
let buffer = Buffer.alloc(0);
|
|
38145
|
+
let settled = false;
|
|
38146
|
+
const finish = (result) => {
|
|
38147
|
+
if (settled)
|
|
38148
|
+
return;
|
|
38149
|
+
settled = true;
|
|
38150
|
+
clearTimeout(timer);
|
|
38151
|
+
options.signal?.removeEventListener("abort", abort);
|
|
38152
|
+
socket.destroy();
|
|
38153
|
+
resolve(result);
|
|
38154
|
+
};
|
|
38155
|
+
const abort = () => finish({ ok: false, kind: "cancelled", message: "Remote ADB preflight was cancelled." });
|
|
38156
|
+
const timer = setTimeout(() => finish({
|
|
38157
|
+
ok: false,
|
|
38158
|
+
kind: "timeout",
|
|
38159
|
+
message: "The configured ADB server did not answer the safety preflight in time."
|
|
38160
|
+
}), options.timeoutMs);
|
|
38161
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
38162
|
+
socket.once("connect", () => socket.write(REQUEST, "ascii"));
|
|
38163
|
+
socket.on("data", (chunk) => {
|
|
38164
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
38165
|
+
if (buffer.length > MAX_RESPONSE_BYTES + 8) {
|
|
38166
|
+
finish({
|
|
38167
|
+
ok: false,
|
|
38168
|
+
kind: "invalid-response",
|
|
38169
|
+
message: "The configured endpoint returned an oversized ADB response."
|
|
38170
|
+
});
|
|
38171
|
+
return;
|
|
38172
|
+
}
|
|
38173
|
+
const frame = protocolFrame(buffer);
|
|
38174
|
+
if (frame.kind === "incomplete")
|
|
38175
|
+
return;
|
|
38176
|
+
if (frame.kind === "invalid") {
|
|
38177
|
+
finish({ ok: false, kind: "invalid-response", message: frame.message });
|
|
38178
|
+
return;
|
|
38179
|
+
}
|
|
38180
|
+
if (frame.kind === "rejected") {
|
|
38181
|
+
finish({ ok: false, kind: "rejected", message: frame.message });
|
|
38182
|
+
return;
|
|
38183
|
+
}
|
|
38184
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(frame.payload)) {
|
|
38185
|
+
finish({
|
|
38186
|
+
ok: false,
|
|
38187
|
+
kind: "invalid-response",
|
|
38188
|
+
message: "The configured endpoint returned an invalid ADB protocol version."
|
|
38189
|
+
});
|
|
38190
|
+
return;
|
|
38191
|
+
}
|
|
38192
|
+
const actualProtocolVersion = Number.parseInt(frame.payload, 16);
|
|
38193
|
+
if (actualProtocolVersion !== options.expectedProtocolVersion) {
|
|
38194
|
+
finish({
|
|
38195
|
+
ok: false,
|
|
38196
|
+
kind: "protocol-mismatch",
|
|
38197
|
+
message: "The remote ADB server protocol does not match the local ADB client.",
|
|
38198
|
+
expectedProtocolVersion: options.expectedProtocolVersion,
|
|
38199
|
+
actualProtocolVersion
|
|
38200
|
+
});
|
|
38201
|
+
return;
|
|
38202
|
+
}
|
|
38203
|
+
finish({ ok: true, protocolVersion: actualProtocolVersion });
|
|
38204
|
+
});
|
|
38205
|
+
socket.once("end", () => finish({
|
|
38206
|
+
ok: false,
|
|
38207
|
+
kind: "invalid-response",
|
|
38208
|
+
message: "The configured endpoint closed before completing the ADB preflight."
|
|
38209
|
+
}));
|
|
38210
|
+
socket.once("error", (caught) => finish({
|
|
38211
|
+
ok: false,
|
|
38212
|
+
kind: "unreachable",
|
|
38213
|
+
message: "The configured ADB server could not be reached.",
|
|
38214
|
+
...caught.code === undefined ? {} : { networkCode: caught.code }
|
|
38215
|
+
}));
|
|
38216
|
+
});
|
|
38217
|
+
};
|
|
38218
|
+
|
|
38066
38219
|
// src/adb/client.ts
|
|
38067
38220
|
function targetArguments(target) {
|
|
38068
38221
|
if (typeof target === "string") {
|
|
@@ -38095,13 +38248,21 @@ class AdbClient {
|
|
|
38095
38248
|
#options;
|
|
38096
38249
|
#runner;
|
|
38097
38250
|
#idFactory;
|
|
38251
|
+
#remoteServerProbe;
|
|
38252
|
+
#expectedProtocolVersion;
|
|
38098
38253
|
constructor(options) {
|
|
38099
38254
|
this.#options = options;
|
|
38100
38255
|
this.#runner = options.runner ?? runProcess;
|
|
38101
38256
|
this.#idFactory = options.idFactory ?? randomUUID2;
|
|
38257
|
+
this.#remoteServerProbe = options.remoteServerProbe ?? (options.runner === undefined ? probeRemoteAdbServer : false);
|
|
38102
38258
|
}
|
|
38103
38259
|
async version(signal) {
|
|
38104
|
-
|
|
38260
|
+
const observation = await this.#observe("version", "Checking ADB version", ["version"], parseAdbVersion, signal, { useServerArguments: false });
|
|
38261
|
+
const protocolVersion = adbProtocolVersion(observation.value.protocolVersion);
|
|
38262
|
+
if (protocolVersion !== undefined) {
|
|
38263
|
+
this.#expectedProtocolVersion = Promise.resolve(protocolVersion);
|
|
38264
|
+
}
|
|
38265
|
+
return observation;
|
|
38105
38266
|
}
|
|
38106
38267
|
async hostFeatures(signal) {
|
|
38107
38268
|
return await this.#observe("host-features", "Checking ADB host features", ["host-features"], parseFeatureList, signal);
|
|
@@ -38159,6 +38320,47 @@ class AdbClient {
|
|
|
38159
38320
|
...this.#options.port === undefined ? [] : ["-P", String(this.#options.port)]
|
|
38160
38321
|
];
|
|
38161
38322
|
}
|
|
38323
|
+
async#localProtocolVersion(signal) {
|
|
38324
|
+
if (this.#expectedProtocolVersion !== undefined)
|
|
38325
|
+
return await this.#expectedProtocolVersion;
|
|
38326
|
+
this.#expectedProtocolVersion = (async () => {
|
|
38327
|
+
const result = await this.#runner({
|
|
38328
|
+
executable: this.#options.executable,
|
|
38329
|
+
args: ["version"],
|
|
38330
|
+
timeoutMs: Math.min(this.#options.timeoutMs ?? 3000, 3000),
|
|
38331
|
+
...signal === undefined ? {} : { signal }
|
|
38332
|
+
});
|
|
38333
|
+
if (result.spawnError !== undefined || result.streamError !== undefined || result.exitCode !== 0 || result.timedOut || result.aborted) {
|
|
38334
|
+
return;
|
|
38335
|
+
}
|
|
38336
|
+
return adbProtocolVersion(parseAdbVersion(result.stdout).protocolVersion);
|
|
38337
|
+
})();
|
|
38338
|
+
return await this.#expectedProtocolVersion;
|
|
38339
|
+
}
|
|
38340
|
+
async#remoteSafetyPreflight(signal) {
|
|
38341
|
+
if (this.#options.host === undefined && this.#options.port === undefined || this.#remoteServerProbe === false) {
|
|
38342
|
+
return;
|
|
38343
|
+
}
|
|
38344
|
+
const expectedProtocolVersion = await this.#localProtocolVersion(signal);
|
|
38345
|
+
if (signal?.aborted === true) {
|
|
38346
|
+
return { ok: false, kind: "cancelled", message: "Remote ADB preflight was cancelled." };
|
|
38347
|
+
}
|
|
38348
|
+
if (expectedProtocolVersion === undefined) {
|
|
38349
|
+
return {
|
|
38350
|
+
ok: false,
|
|
38351
|
+
kind: "client-version-unavailable",
|
|
38352
|
+
message: "The local ADB protocol version could not be verified safely."
|
|
38353
|
+
};
|
|
38354
|
+
}
|
|
38355
|
+
const result = await this.#remoteServerProbe({
|
|
38356
|
+
host: this.#options.host ?? "localhost",
|
|
38357
|
+
port: this.#options.port ?? 5037,
|
|
38358
|
+
expectedProtocolVersion,
|
|
38359
|
+
timeoutMs: Math.min(this.#options.timeoutMs ?? 3000, 3000),
|
|
38360
|
+
...signal === undefined ? {} : { signal }
|
|
38361
|
+
});
|
|
38362
|
+
return result.ok ? undefined : result;
|
|
38363
|
+
}
|
|
38162
38364
|
async#observe(operation, message, args, parse10, signal, options = {}) {
|
|
38163
38365
|
const operationId = this.#idFactory();
|
|
38164
38366
|
const correlation = { ...this.#options.correlation, operationId };
|
|
@@ -38179,6 +38381,41 @@ class AdbClient {
|
|
|
38179
38381
|
...background ? { presentation: "background", retention: "transient" } : {}
|
|
38180
38382
|
}
|
|
38181
38383
|
});
|
|
38384
|
+
const clientFailure = options.useServerArguments === false ? undefined : await this.#remoteSafetyPreflight(signal);
|
|
38385
|
+
if (clientFailure !== undefined) {
|
|
38386
|
+
const now = new Date().toISOString();
|
|
38387
|
+
const result2 = {
|
|
38388
|
+
executable: this.#options.executable,
|
|
38389
|
+
args: finalArgs,
|
|
38390
|
+
startedAt: now,
|
|
38391
|
+
finishedAt: now,
|
|
38392
|
+
durationMs: 0,
|
|
38393
|
+
exitCode: clientFailure.kind === "cancelled" ? null : 1,
|
|
38394
|
+
signal: null,
|
|
38395
|
+
stdout: "",
|
|
38396
|
+
stderr: clientFailure.message,
|
|
38397
|
+
stdoutTruncated: false,
|
|
38398
|
+
stderrTruncated: false,
|
|
38399
|
+
timedOut: clientFailure.kind === "timeout",
|
|
38400
|
+
aborted: clientFailure.kind === "cancelled",
|
|
38401
|
+
stoppedAfterIdle: false,
|
|
38402
|
+
killEscalated: false
|
|
38403
|
+
};
|
|
38404
|
+
this.#options.bus.emit({
|
|
38405
|
+
type: "operation.failed",
|
|
38406
|
+
source: `adb.${operation}`,
|
|
38407
|
+
severity: clientFailure.kind === "cancelled" && background ? "debug" : "error",
|
|
38408
|
+
message: clientFailure.kind === "cancelled" && background ? `${message} cancelled` : `${message} failed`,
|
|
38409
|
+
correlation,
|
|
38410
|
+
data: {
|
|
38411
|
+
...processMetadata(result2),
|
|
38412
|
+
preflight: "remote-server",
|
|
38413
|
+
failure: clientFailure.kind,
|
|
38414
|
+
...background ? { presentation: "background" } : {}
|
|
38415
|
+
}
|
|
38416
|
+
});
|
|
38417
|
+
return { operationId, process: result2, value: parse10(""), clientFailure };
|
|
38418
|
+
}
|
|
38182
38419
|
const request = {
|
|
38183
38420
|
executable: this.#options.executable,
|
|
38184
38421
|
args: finalArgs,
|
|
@@ -38554,6 +38791,9 @@ var ProblemCode = {
|
|
|
38554
38791
|
AdbNotFound: "ADB_NOT_FOUND",
|
|
38555
38792
|
AdbOptionalProbeFailed: "ADB_OPTIONAL_PROBE_FAILED",
|
|
38556
38793
|
AdbServerUnavailable: "ADB_SERVER_UNAVAILABLE",
|
|
38794
|
+
AdbRemoteServerInvalidResponse: "ADB_REMOTE_SERVER_INVALID_RESPONSE",
|
|
38795
|
+
AdbRemoteServerProtocolMismatch: "ADB_REMOTE_SERVER_PROTOCOL_MISMATCH",
|
|
38796
|
+
AdbRemoteServerUnreachable: "ADB_REMOTE_SERVER_UNREACHABLE",
|
|
38557
38797
|
AdbVersionMismatch: "ADB_VERSION_MISMATCH",
|
|
38558
38798
|
AdbTimeout: "ADB_TIMEOUT",
|
|
38559
38799
|
InteractiveSelectionUnavailable: "INTERACTIVE_SELECTION_UNAVAILABLE",
|
|
@@ -38839,7 +39079,7 @@ function problemsForServerStatus(status, clientVersion, correlation) {
|
|
|
38839
39079
|
}
|
|
38840
39080
|
return problems;
|
|
38841
39081
|
}
|
|
38842
|
-
function adbProcessProblem(operation, result, correlation) {
|
|
39082
|
+
function adbProcessProblem(operation, result, correlation, clientFailure) {
|
|
38843
39083
|
if (result.spawnError?.code === "ENOENT") {
|
|
38844
39084
|
return adbNotFoundProblem(correlation, result.executable);
|
|
38845
39085
|
}
|
|
@@ -38856,6 +39096,88 @@ function adbProcessProblem(operation, result, correlation) {
|
|
|
38856
39096
|
correlation
|
|
38857
39097
|
};
|
|
38858
39098
|
}
|
|
39099
|
+
if (clientFailure?.kind === "protocol-mismatch") {
|
|
39100
|
+
return {
|
|
39101
|
+
code: ProblemCode.AdbRemoteServerProtocolMismatch,
|
|
39102
|
+
category: "adb.remote-server",
|
|
39103
|
+
severity: "error",
|
|
39104
|
+
summary: "The remote ADB server uses a different protocol version.",
|
|
39105
|
+
detail: "ADB Ready stopped before launching the ADB client because upstream ADB can terminate a mismatched shared server automatically.",
|
|
39106
|
+
retryable: true,
|
|
39107
|
+
evidence: [
|
|
39108
|
+
{
|
|
39109
|
+
source: "adb.remote-preflight",
|
|
39110
|
+
field: "expectedProtocolVersion",
|
|
39111
|
+
value: clientFailure.expectedProtocolVersion ?? null
|
|
39112
|
+
},
|
|
39113
|
+
{
|
|
39114
|
+
source: "adb.remote-preflight",
|
|
39115
|
+
field: "actualProtocolVersion",
|
|
39116
|
+
value: clientFailure.actualProtocolVersion ?? null
|
|
39117
|
+
}
|
|
39118
|
+
],
|
|
39119
|
+
actions: [
|
|
39120
|
+
{
|
|
39121
|
+
id: "align_remote_platform_tools",
|
|
39122
|
+
title: "Align Platform-Tools on the client and remote ADB host",
|
|
39123
|
+
kind: "user",
|
|
39124
|
+
risk: "shared-global",
|
|
39125
|
+
automatic: false
|
|
39126
|
+
}
|
|
39127
|
+
],
|
|
39128
|
+
correlation
|
|
39129
|
+
};
|
|
39130
|
+
}
|
|
39131
|
+
if (clientFailure?.kind === "unreachable") {
|
|
39132
|
+
return {
|
|
39133
|
+
code: ProblemCode.AdbRemoteServerUnreachable,
|
|
39134
|
+
category: "network.route",
|
|
39135
|
+
severity: "error",
|
|
39136
|
+
summary: "The configured remote ADB server is unreachable.",
|
|
39137
|
+
detail: "Verify the explicit host and port, then check the WSL, container, VM, VPN, firewall, or tunnel route without exposing the ADB socket publicly.",
|
|
39138
|
+
retryable: true,
|
|
39139
|
+
evidence: [
|
|
39140
|
+
...clientFailure.networkCode === undefined ? [] : [
|
|
39141
|
+
{
|
|
39142
|
+
source: "adb.remote-preflight",
|
|
39143
|
+
field: "networkCode",
|
|
39144
|
+
value: clientFailure.networkCode
|
|
39145
|
+
}
|
|
39146
|
+
]
|
|
39147
|
+
],
|
|
39148
|
+
actions: [
|
|
39149
|
+
{
|
|
39150
|
+
id: "review_remote_adb_route",
|
|
39151
|
+
title: "Check the protected route to the configured ADB server",
|
|
39152
|
+
kind: "user",
|
|
39153
|
+
risk: "none",
|
|
39154
|
+
automatic: false
|
|
39155
|
+
}
|
|
39156
|
+
],
|
|
39157
|
+
correlation
|
|
39158
|
+
};
|
|
39159
|
+
}
|
|
39160
|
+
if (clientFailure !== undefined && (clientFailure.kind === "invalid-response" || clientFailure.kind === "rejected" || clientFailure.kind === "client-version-unavailable")) {
|
|
39161
|
+
return {
|
|
39162
|
+
code: ProblemCode.AdbRemoteServerInvalidResponse,
|
|
39163
|
+
category: "adb.remote-server",
|
|
39164
|
+
severity: "error",
|
|
39165
|
+
summary: "The configured endpoint could not be verified as a compatible ADB server.",
|
|
39166
|
+
detail: "ADB Ready refused to send a normal ADB command until the endpoint and local client protocol can be verified safely.",
|
|
39167
|
+
retryable: true,
|
|
39168
|
+
evidence: [{ source: "adb.remote-preflight", field: "failure", value: clientFailure.kind }],
|
|
39169
|
+
actions: [
|
|
39170
|
+
{
|
|
39171
|
+
id: "verify_remote_adb_endpoint",
|
|
39172
|
+
title: "Verify the configured ADB host, port, and Platform-Tools installation",
|
|
39173
|
+
kind: "user",
|
|
39174
|
+
risk: "none",
|
|
39175
|
+
automatic: false
|
|
39176
|
+
}
|
|
39177
|
+
],
|
|
39178
|
+
correlation
|
|
39179
|
+
};
|
|
39180
|
+
}
|
|
38859
39181
|
if (result.timedOut) {
|
|
38860
39182
|
return {
|
|
38861
39183
|
code: ProblemCode.AdbTimeout,
|
|
@@ -38905,8 +39227,8 @@ ${result.stdout}`;
|
|
|
38905
39227
|
correlation
|
|
38906
39228
|
};
|
|
38907
39229
|
}
|
|
38908
|
-
function adbOptionalProbeProblem(operation, result, correlation) {
|
|
38909
|
-
const failure3 = adbProcessProblem(operation, result, correlation);
|
|
39230
|
+
function adbOptionalProbeProblem(operation, result, correlation, clientFailure) {
|
|
39231
|
+
const failure3 = adbProcessProblem(operation, result, correlation, clientFailure);
|
|
38910
39232
|
return {
|
|
38911
39233
|
...failure3,
|
|
38912
39234
|
code: ProblemCode.AdbOptionalProbeFailed,
|
|
@@ -39391,6 +39713,7 @@ var NATIVE_EXTENSIONS = new Set([
|
|
|
39391
39713
|
".log",
|
|
39392
39714
|
".mp4",
|
|
39393
39715
|
".png",
|
|
39716
|
+
".proto",
|
|
39394
39717
|
".txt",
|
|
39395
39718
|
".webm",
|
|
39396
39719
|
".xml"
|
|
@@ -39536,7 +39859,7 @@ async function collectNativeVerifierArtifacts(directory, limits = {}) {
|
|
|
39536
39859
|
const file = await stat4(absolute).catch(() => {
|
|
39537
39860
|
return;
|
|
39538
39861
|
});
|
|
39539
|
-
if (file === undefined || file.size > maxFileBytes || total + file.size > maxTotalBytes || artifacts.length >= maxFiles) {
|
|
39862
|
+
if (file === undefined || limits.modifiedSinceMs !== undefined && file.mtimeMs < limits.modifiedSinceMs || file.size > maxFileBytes || total + file.size > maxTotalBytes || artifacts.length >= maxFiles) {
|
|
39540
39863
|
omitted += 1;
|
|
39541
39864
|
continue;
|
|
39542
39865
|
}
|
|
@@ -39812,7 +40135,7 @@ async function staleLease(directory, owner, now, options) {
|
|
|
39812
40135
|
});
|
|
39813
40136
|
return metadata !== undefined && now.getTime() - metadata.mtimeMs >= (options.ttlMs ?? DEFAULT_TTL_MS);
|
|
39814
40137
|
}
|
|
39815
|
-
async function
|
|
40138
|
+
async function tryAcquireTargetLease(request, options = {}) {
|
|
39816
40139
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
39817
40140
|
const heartbeatMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
|
|
39818
40141
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1000) {
|
|
@@ -39932,6 +40255,51 @@ async function acquireTargetLease(request, options = {}) {
|
|
|
39932
40255
|
message: "ADB Ready could not safely recover the stale target lease."
|
|
39933
40256
|
};
|
|
39934
40257
|
}
|
|
40258
|
+
function wait2(milliseconds, signal) {
|
|
40259
|
+
return new Promise((resolve) => {
|
|
40260
|
+
if (signal?.aborted === true) {
|
|
40261
|
+
resolve();
|
|
40262
|
+
return;
|
|
40263
|
+
}
|
|
40264
|
+
const finish = () => {
|
|
40265
|
+
signal?.removeEventListener("abort", abort);
|
|
40266
|
+
resolve();
|
|
40267
|
+
};
|
|
40268
|
+
const timer = setTimeout(finish, milliseconds);
|
|
40269
|
+
const abort = () => {
|
|
40270
|
+
clearTimeout(timer);
|
|
40271
|
+
finish();
|
|
40272
|
+
};
|
|
40273
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
40274
|
+
});
|
|
40275
|
+
}
|
|
40276
|
+
async function acquireTargetLease(request, options = {}) {
|
|
40277
|
+
const waitTimeoutMs = options.waitTimeoutMs ?? 0;
|
|
40278
|
+
const pollIntervalMs = options.waitPollIntervalMs ?? 250;
|
|
40279
|
+
if (!Number.isSafeInteger(waitTimeoutMs) || waitTimeoutMs < 0 || waitTimeoutMs > 3600000) {
|
|
40280
|
+
throw new RangeError("target lease waitTimeoutMs must be from 0 to 3600000");
|
|
40281
|
+
}
|
|
40282
|
+
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 10 || pollIntervalMs > 1e4) {
|
|
40283
|
+
throw new RangeError("target lease waitPollIntervalMs must be from 10 to 10000");
|
|
40284
|
+
}
|
|
40285
|
+
const clock = options.clock ?? (() => new Date);
|
|
40286
|
+
const deadline = clock().getTime() + waitTimeoutMs;
|
|
40287
|
+
while (true) {
|
|
40288
|
+
if (options.signal?.aborted === true) {
|
|
40289
|
+
return {
|
|
40290
|
+
ok: false,
|
|
40291
|
+
code: "TARGET_LEASE_CANCELLED",
|
|
40292
|
+
message: "Target ownership wait was cancelled."
|
|
40293
|
+
};
|
|
40294
|
+
}
|
|
40295
|
+
const acquired = await tryAcquireTargetLease(request, options);
|
|
40296
|
+
if (acquired.ok || acquired.code !== "TARGET_BUSY" || clock().getTime() >= deadline) {
|
|
40297
|
+
return acquired;
|
|
40298
|
+
}
|
|
40299
|
+
const remaining = Math.max(0, deadline - clock().getTime());
|
|
40300
|
+
await (options.sleep ?? wait2)(Math.min(pollIntervalMs, remaining), options.signal);
|
|
40301
|
+
}
|
|
40302
|
+
}
|
|
39935
40303
|
|
|
39936
40304
|
// src/evidence/ui-hierarchy-capture.ts
|
|
39937
40305
|
var DEFAULT_LOCK_POLL_INTERVAL_MS = 50;
|
|
@@ -40630,7 +40998,7 @@ async function sendExpoControl(options) {
|
|
|
40630
40998
|
|
|
40631
40999
|
// src/dev/expo-launch.ts
|
|
40632
41000
|
var DEFAULT_TIMEOUT_MS2 = 2000;
|
|
40633
|
-
var
|
|
41001
|
+
var MAX_RESPONSE_BYTES2 = 16 * 1024;
|
|
40634
41002
|
var MAX_URL_LENGTH = 8 * 1024;
|
|
40635
41003
|
function aborted4(signal) {
|
|
40636
41004
|
return signal?.aborted === true;
|
|
@@ -40692,7 +41060,7 @@ async function boundedBody(response) {
|
|
|
40692
41060
|
if (chunk.done)
|
|
40693
41061
|
break;
|
|
40694
41062
|
bytes += chunk.value.byteLength;
|
|
40695
|
-
if (bytes >
|
|
41063
|
+
if (bytes > MAX_RESPONSE_BYTES2)
|
|
40696
41064
|
throw new RangeError("Expo response is too large");
|
|
40697
41065
|
body += decoder.decode(chunk.value, { stream: true });
|
|
40698
41066
|
}
|
|
@@ -42386,6 +42754,7 @@ function createClient(executable, context, config4, dependencies) {
|
|
|
42386
42754
|
...config4.adbPort === undefined ? {} : { port: config4.adbPort },
|
|
42387
42755
|
...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
|
|
42388
42756
|
...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
|
|
42757
|
+
...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
|
|
42389
42758
|
...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory }
|
|
42390
42759
|
});
|
|
42391
42760
|
}
|
|
@@ -42393,7 +42762,7 @@ function operationProblem2(operation, observation, commandId) {
|
|
|
42393
42762
|
return adbProcessProblem(operation, observation.process, {
|
|
42394
42763
|
commandId,
|
|
42395
42764
|
operationId: observation.operationId
|
|
42396
|
-
});
|
|
42765
|
+
}, observation.clientFailure);
|
|
42397
42766
|
}
|
|
42398
42767
|
function isConnectableDiscoveryService(service) {
|
|
42399
42768
|
return service.serviceType === "legacy" || service.serviceType === "connect" && service.knownDevice !== false;
|
|
@@ -42584,7 +42953,7 @@ async function runDoctor(config4 = {}, dependencies = {}, signal) {
|
|
|
42584
42953
|
const hostFeatures = await client.hostFeatures(signal);
|
|
42585
42954
|
if (!processSucceeded(hostFeatures.process)) {
|
|
42586
42955
|
const problem2 = operationProblem2("host-features", hostFeatures, context.commandId);
|
|
42587
|
-
if (problem2.code === ProblemCode.OperationInterrupted) {
|
|
42956
|
+
if (problem2.code === ProblemCode.OperationInterrupted || hostFeatures.clientFailure !== undefined) {
|
|
42588
42957
|
problems.push(problem2);
|
|
42589
42958
|
return finish(context, null, problems);
|
|
42590
42959
|
}
|
|
@@ -42601,7 +42970,7 @@ async function runDoctor(config4 = {}, dependencies = {}, signal) {
|
|
|
42601
42970
|
}));
|
|
42602
42971
|
} else {
|
|
42603
42972
|
const problem2 = operationProblem2("server-status", status, context.commandId);
|
|
42604
|
-
if (problem2.code === ProblemCode.OperationInterrupted) {
|
|
42973
|
+
if (problem2.code === ProblemCode.OperationInterrupted || status.clientFailure !== undefined) {
|
|
42605
42974
|
problems.push(problem2);
|
|
42606
42975
|
return finish(context, null, problems);
|
|
42607
42976
|
}
|
|
@@ -43239,6 +43608,7 @@ async function runLogs(options, config4 = {}, dependencies = {}, signal) {
|
|
|
43239
43608
|
...config4.adbPort === undefined ? {} : { port: config4.adbPort },
|
|
43240
43609
|
...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
|
|
43241
43610
|
...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
|
|
43611
|
+
...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
|
|
43242
43612
|
...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory }
|
|
43243
43613
|
});
|
|
43244
43614
|
let resolvedPid = options.pid;
|
|
@@ -44020,6 +44390,7 @@ async function runDev(options, config4 = {}, dependencies = {}, signal) {
|
|
|
44020
44390
|
...config4.adbPort === undefined ? {} : { port: config4.adbPort },
|
|
44021
44391
|
...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
|
|
44022
44392
|
...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
|
|
44393
|
+
...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
|
|
44023
44394
|
idFactory
|
|
44024
44395
|
});
|
|
44025
44396
|
const devices = await discoveryClient.devices(signal);
|
|
@@ -44107,6 +44478,7 @@ async function runDev(options, config4 = {}, dependencies = {}, signal) {
|
|
|
44107
44478
|
...config4.adbPort === undefined ? {} : { port: config4.adbPort },
|
|
44108
44479
|
...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
|
|
44109
44480
|
...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
|
|
44481
|
+
...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
|
|
44110
44482
|
idFactory
|
|
44111
44483
|
};
|
|
44112
44484
|
const client = new AdbClient(clientOptions);
|
|
@@ -45480,7 +45852,7 @@ function operationProblem(name, observation, commandId) {
|
|
|
45480
45852
|
return adbProcessProblem(name, observation.process, {
|
|
45481
45853
|
commandId,
|
|
45482
45854
|
operationId: observation.operationId
|
|
45483
|
-
});
|
|
45855
|
+
}, observation.clientFailure);
|
|
45484
45856
|
}
|
|
45485
45857
|
async function readyTarget(current, config4, dependencies, problems, signal) {
|
|
45486
45858
|
const inventory = await runDevices(config4, { ...dependencies, bus: current.bus }, signal);
|
|
@@ -45526,6 +45898,7 @@ async function readyTarget(current, config4, dependencies, problems, signal) {
|
|
|
45526
45898
|
...config4.adbPort === undefined ? {} : { port: config4.adbPort },
|
|
45527
45899
|
...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
|
|
45528
45900
|
...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
|
|
45901
|
+
...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
|
|
45529
45902
|
...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory }
|
|
45530
45903
|
})
|
|
45531
45904
|
};
|
|
@@ -45647,11 +46020,11 @@ async function launchApp(ready, applicationId, explicitActivity, commandId, prob
|
|
|
45647
46020
|
if (succeeded(active.process) && active.value?.applicationId === applicationId)
|
|
45648
46021
|
break;
|
|
45649
46022
|
if (attempt < 4) {
|
|
45650
|
-
const
|
|
46023
|
+
const wait3 = dependencies.sleep ?? (async (milliseconds) => {
|
|
45651
46024
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
45652
46025
|
return true;
|
|
45653
46026
|
});
|
|
45654
|
-
if (!await
|
|
46027
|
+
if (!await wait3(150, signal ?? new AbortController().signal))
|
|
45655
46028
|
break;
|
|
45656
46029
|
}
|
|
45657
46030
|
}
|
|
@@ -46395,7 +46768,27 @@ var ROOT_KEYS2 = new Set([
|
|
|
46395
46768
|
var PROFILE_KEYS = new Set(["extends", "adb", "timeoutMs", "output", "targets", "app", "dev"]);
|
|
46396
46769
|
var ADB_KEYS = new Set(["path", "host", "port"]);
|
|
46397
46770
|
var OUTPUT_KEYS = new Set(["color", "unicode", "animation", "interactive"]);
|
|
46398
|
-
var TARGET_KEYS = new Set(["aliases"]);
|
|
46771
|
+
var TARGET_KEYS = new Set(["aliases", "pools"]);
|
|
46772
|
+
var TARGET_POOL_KEYS = new Set(["members", "maxConcurrency", "failFast", "leaseWaitMs"]);
|
|
46773
|
+
var TARGET_POOL_MEMBER_KEYS = new Set([
|
|
46774
|
+
"id",
|
|
46775
|
+
"kind",
|
|
46776
|
+
"required",
|
|
46777
|
+
"serial",
|
|
46778
|
+
"name",
|
|
46779
|
+
"host",
|
|
46780
|
+
"port",
|
|
46781
|
+
"model",
|
|
46782
|
+
"version",
|
|
46783
|
+
"locale",
|
|
46784
|
+
"orientation"
|
|
46785
|
+
]);
|
|
46786
|
+
var TARGET_POOL_MEMBER_KEYS_BY_KIND = {
|
|
46787
|
+
adb: new Set(["id", "kind", "required", "serial"]),
|
|
46788
|
+
avd: new Set(["id", "kind", "required", "name"]),
|
|
46789
|
+
"remote-adb": new Set(["id", "kind", "required", "host", "port", "serial"]),
|
|
46790
|
+
firebase: new Set(["id", "kind", "required", "model", "version", "locale", "orientation"])
|
|
46791
|
+
};
|
|
46399
46792
|
var APP_KEYS = new Set(["android"]);
|
|
46400
46793
|
var ANDROID_APP_KEYS = new Set(["package"]);
|
|
46401
46794
|
var DEV_KEYS = new Set([
|
|
@@ -46635,6 +47028,127 @@ function validateDocument(document2, source, location) {
|
|
|
46635
47028
|
values.targetAliases = aliases;
|
|
46636
47029
|
}
|
|
46637
47030
|
}
|
|
47031
|
+
if (document2.targets.pools !== undefined) {
|
|
47032
|
+
if (!isObject4(document2.targets.pools)) {
|
|
47033
|
+
errors6.push(error4(source, location, "targets.pools", "targets.pools must be an object."));
|
|
47034
|
+
} else {
|
|
47035
|
+
const pools = {};
|
|
47036
|
+
for (const [poolName, candidate] of Object.entries(document2.targets.pools)) {
|
|
47037
|
+
const poolPath = `targets.pools.${poolName}`;
|
|
47038
|
+
if (!PROFILE_NAME.test(poolName)) {
|
|
47039
|
+
errors6.push(error4(source, location, poolPath, "Target pool name must be 1-64 letters, numbers, dots, underscores, or hyphens."));
|
|
47040
|
+
continue;
|
|
47041
|
+
}
|
|
47042
|
+
if (!isObject4(candidate)) {
|
|
47043
|
+
errors6.push(error4(source, location, poolPath, "Target pool must be an object."));
|
|
47044
|
+
continue;
|
|
47045
|
+
}
|
|
47046
|
+
validateUnknownKeys(candidate, TARGET_POOL_KEYS, `${poolPath}.`, source, location, errors6);
|
|
47047
|
+
const members4 = candidate.members;
|
|
47048
|
+
const maxConcurrency = candidate.maxConcurrency;
|
|
47049
|
+
const failFast = candidate.failFast ?? false;
|
|
47050
|
+
const leaseWaitMs = candidate.leaseWaitMs ?? 30000;
|
|
47051
|
+
let valid = true;
|
|
47052
|
+
if (!Array.isArray(members4) || members4.length < 1 || members4.length > 64) {
|
|
47053
|
+
errors6.push(error4(source, location, `${poolPath}.members`, "Target pool members must be an array with 1-64 entries."));
|
|
47054
|
+
valid = false;
|
|
47055
|
+
}
|
|
47056
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 32) {
|
|
47057
|
+
errors6.push(error4(source, location, `${poolPath}.maxConcurrency`, "Target pool maxConcurrency must be an integer from 1 to 32."));
|
|
47058
|
+
valid = false;
|
|
47059
|
+
}
|
|
47060
|
+
if (typeof failFast !== "boolean") {
|
|
47061
|
+
errors6.push(error4(source, location, `${poolPath}.failFast`, "Target pool failFast must be a boolean."));
|
|
47062
|
+
valid = false;
|
|
47063
|
+
}
|
|
47064
|
+
if (!Number.isSafeInteger(leaseWaitMs) || leaseWaitMs < 0 || leaseWaitMs > 3600000) {
|
|
47065
|
+
errors6.push(error4(source, location, `${poolPath}.leaseWaitMs`, "Target pool leaseWaitMs must be an integer from 0 to 3600000."));
|
|
47066
|
+
valid = false;
|
|
47067
|
+
}
|
|
47068
|
+
const parsedMembers = [];
|
|
47069
|
+
const memberIds = new Set;
|
|
47070
|
+
const resourceIds = new Set;
|
|
47071
|
+
if (Array.isArray(members4)) {
|
|
47072
|
+
members4.forEach((member, index) => {
|
|
47073
|
+
const memberPath = `${poolPath}.members.${String(index)}`;
|
|
47074
|
+
if (!isObject4(member)) {
|
|
47075
|
+
errors6.push(error4(source, location, memberPath, "Target pool member must be an object."));
|
|
47076
|
+
valid = false;
|
|
47077
|
+
return;
|
|
47078
|
+
}
|
|
47079
|
+
validateUnknownKeys(member, TARGET_POOL_MEMBER_KEYS, `${memberPath}.`, source, location, errors6);
|
|
47080
|
+
const id = member.id;
|
|
47081
|
+
const kind = member.kind;
|
|
47082
|
+
const required4 = member.required ?? true;
|
|
47083
|
+
if (kind === "adb" || kind === "avd" || kind === "remote-adb" || kind === "firebase") {
|
|
47084
|
+
validateUnknownKeys(member, TARGET_POOL_MEMBER_KEYS_BY_KIND[kind], `${memberPath}.`, source, location, errors6);
|
|
47085
|
+
}
|
|
47086
|
+
if (typeof id !== "string" || !PROFILE_NAME.test(id) || memberIds.has(id)) {
|
|
47087
|
+
errors6.push(error4(source, location, `${memberPath}.id`, "Target pool member id must be unique and use 1-64 letters, numbers, dots, underscores, or hyphens."));
|
|
47088
|
+
valid = false;
|
|
47089
|
+
return;
|
|
47090
|
+
}
|
|
47091
|
+
memberIds.add(id);
|
|
47092
|
+
if (typeof required4 !== "boolean") {
|
|
47093
|
+
errors6.push(error4(source, location, `${memberPath}.required`, "Target pool member required must be a boolean."));
|
|
47094
|
+
valid = false;
|
|
47095
|
+
return;
|
|
47096
|
+
}
|
|
47097
|
+
const optionalRequired = required4 ? {} : { required: false };
|
|
47098
|
+
const nonEmpty = (value) => typeof value === "string" && value.trim() === value && value.length > 0;
|
|
47099
|
+
let resourceId;
|
|
47100
|
+
if (kind === "adb" && nonEmpty(member.serial)) {
|
|
47101
|
+
parsedMembers.push({ id, kind, serial: member.serial, ...optionalRequired });
|
|
47102
|
+
resourceId = `adb:${member.serial}`;
|
|
47103
|
+
} else if (kind === "avd" && nonEmpty(member.name)) {
|
|
47104
|
+
parsedMembers.push({ id, kind, name: member.name, ...optionalRequired });
|
|
47105
|
+
resourceId = `avd:${member.name}`;
|
|
47106
|
+
} else if (kind === "remote-adb" && nonEmpty(member.host) && nonEmpty(member.serial) && (member.port === undefined || Number.isSafeInteger(member.port) && member.port >= 1 && member.port <= 65535)) {
|
|
47107
|
+
parsedMembers.push({
|
|
47108
|
+
id,
|
|
47109
|
+
kind,
|
|
47110
|
+
host: member.host,
|
|
47111
|
+
port: member.port ?? 5037,
|
|
47112
|
+
serial: member.serial,
|
|
47113
|
+
...optionalRequired
|
|
47114
|
+
});
|
|
47115
|
+
resourceId = `remote-adb:${member.host}:${String(member.port ?? 5037)}:${member.serial}`;
|
|
47116
|
+
} else if (kind === "firebase" && nonEmpty(member.model) && nonEmpty(member.version) && (member.locale === undefined || nonEmpty(member.locale)) && (member.orientation === undefined || member.orientation === "landscape" || member.orientation === "portrait")) {
|
|
47117
|
+
parsedMembers.push({
|
|
47118
|
+
id,
|
|
47119
|
+
kind,
|
|
47120
|
+
model: member.model,
|
|
47121
|
+
version: member.version,
|
|
47122
|
+
locale: member.locale ?? "en",
|
|
47123
|
+
orientation: member.orientation ?? "portrait",
|
|
47124
|
+
...optionalRequired
|
|
47125
|
+
});
|
|
47126
|
+
resourceId = `firebase:${member.model}:${member.version}:${String(member.locale ?? "en")}:${String(member.orientation ?? "portrait")}`;
|
|
47127
|
+
} else {
|
|
47128
|
+
errors6.push(error4(source, location, memberPath, "Target pool member must be adb(serial), avd(name), remote-adb(host, optional port, serial), or firebase(model, version, optional locale/orientation)."));
|
|
47129
|
+
valid = false;
|
|
47130
|
+
}
|
|
47131
|
+
if (resourceId !== undefined) {
|
|
47132
|
+
if (resourceIds.has(resourceId)) {
|
|
47133
|
+
errors6.push(error4(source, location, memberPath, "Target pool members must identify unique resources or provider dimensions."));
|
|
47134
|
+
valid = false;
|
|
47135
|
+
}
|
|
47136
|
+
resourceIds.add(resourceId);
|
|
47137
|
+
}
|
|
47138
|
+
});
|
|
47139
|
+
}
|
|
47140
|
+
if (valid) {
|
|
47141
|
+
pools[poolName] = {
|
|
47142
|
+
members: parsedMembers,
|
|
47143
|
+
maxConcurrency,
|
|
47144
|
+
failFast,
|
|
47145
|
+
leaseWaitMs
|
|
47146
|
+
};
|
|
47147
|
+
}
|
|
47148
|
+
}
|
|
47149
|
+
values.targetPools = pools;
|
|
47150
|
+
}
|
|
47151
|
+
}
|
|
46638
47152
|
}
|
|
46639
47153
|
}
|
|
46640
47154
|
if (document2.app !== undefined) {
|
|
@@ -53209,6 +53723,1143 @@ ${githubSummary(result)}
|
|
|
53209
53723
|
}
|
|
53210
53724
|
}
|
|
53211
53725
|
|
|
53726
|
+
// src/automation/firebase-test-lab.ts
|
|
53727
|
+
import { randomUUID as randomUUID13 } from "node:crypto";
|
|
53728
|
+
import { chmod, mkdir as mkdir8, rename as rename8, rm as rm9, stat as stat15, writeFile as writeFile8 } from "node:fs/promises";
|
|
53729
|
+
import path23 from "node:path";
|
|
53730
|
+
var AUTH_PATTERN = /active account|auth login|authentication|credentials|permission denied|unauthenticated|unauthorized/iu;
|
|
53731
|
+
var MATRIX_PATTERN = /Test \[([^\]\r\n]+)\] has been created in the Google Cloud\./u;
|
|
53732
|
+
var CONSOLE_URL_PATTERN = /https:\/\/console\.firebase\.google\.com\/[A-Za-z0-9%_./?=&-]+/u;
|
|
53733
|
+
var SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
|
|
53734
|
+
var PROVIDER_FILE_LIMIT = 100;
|
|
53735
|
+
var PROVIDER_FILE_BYTES = 20 * 1024 * 1024;
|
|
53736
|
+
var PROVIDER_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
53737
|
+
var PROVIDER_EXTENSIONS = new Set([
|
|
53738
|
+
".ec",
|
|
53739
|
+
".html",
|
|
53740
|
+
".jpeg",
|
|
53741
|
+
".jpg",
|
|
53742
|
+
".json",
|
|
53743
|
+
".log",
|
|
53744
|
+
".mp4",
|
|
53745
|
+
".png",
|
|
53746
|
+
".proto",
|
|
53747
|
+
".txt",
|
|
53748
|
+
".webm",
|
|
53749
|
+
".xml"
|
|
53750
|
+
]);
|
|
53751
|
+
function problem2(code, category, summary2, detail, commandId, retryable = true) {
|
|
53752
|
+
return {
|
|
53753
|
+
code,
|
|
53754
|
+
category,
|
|
53755
|
+
severity: "error",
|
|
53756
|
+
summary: summary2,
|
|
53757
|
+
detail,
|
|
53758
|
+
retryable,
|
|
53759
|
+
evidence: [],
|
|
53760
|
+
actions: [],
|
|
53761
|
+
correlation: { commandId }
|
|
53762
|
+
};
|
|
53763
|
+
}
|
|
53764
|
+
function finish3(started, finished, commandId, data, problems, exitCode2) {
|
|
53765
|
+
return {
|
|
53766
|
+
exitCode: exitCode2,
|
|
53767
|
+
result: {
|
|
53768
|
+
schemaVersion: SCHEMA_VERSION,
|
|
53769
|
+
command: "test firebase",
|
|
53770
|
+
commandId,
|
|
53771
|
+
ok: exitCode2 === 0 /* Success */,
|
|
53772
|
+
startedAt: started.toISOString(),
|
|
53773
|
+
finishedAt: finished.toISOString(),
|
|
53774
|
+
durationMs: Math.max(0, finished.getTime() - started.getTime()),
|
|
53775
|
+
data,
|
|
53776
|
+
problems
|
|
53777
|
+
}
|
|
53778
|
+
};
|
|
53779
|
+
}
|
|
53780
|
+
function parseJsonArray(value) {
|
|
53781
|
+
try {
|
|
53782
|
+
const parsed = JSON.parse(value);
|
|
53783
|
+
return Array.isArray(parsed) ? parsed : undefined;
|
|
53784
|
+
} catch {
|
|
53785
|
+
return;
|
|
53786
|
+
}
|
|
53787
|
+
}
|
|
53788
|
+
function success(result) {
|
|
53789
|
+
return result.spawnError === undefined && result.streamError === undefined && result.exitCode === 0 && !result.timedOut && !result.aborted;
|
|
53790
|
+
}
|
|
53791
|
+
function capacity(value) {
|
|
53792
|
+
const normalized = value?.replace(/^DEVICE_CAPACITY_/u, "").toLowerCase();
|
|
53793
|
+
return normalized === "high" || normalized === "medium" || normalized === "low" || normalized === "none" ? normalized : "unknown";
|
|
53794
|
+
}
|
|
53795
|
+
function hasTag(tags, expected) {
|
|
53796
|
+
return tags.some((tag) => tag.toLowerCase().includes(expected));
|
|
53797
|
+
}
|
|
53798
|
+
function artifactArgument(cwd, value) {
|
|
53799
|
+
return value.startsWith("gs://") ? value : path23.resolve(cwd, value);
|
|
53800
|
+
}
|
|
53801
|
+
function parseFirebaseDevice(value) {
|
|
53802
|
+
const fields = new Map;
|
|
53803
|
+
for (const part of value.split(",")) {
|
|
53804
|
+
const separator = part.indexOf("=");
|
|
53805
|
+
if (separator < 1)
|
|
53806
|
+
return;
|
|
53807
|
+
const key = part.slice(0, separator).trim();
|
|
53808
|
+
const fieldValue = part.slice(separator + 1).trim();
|
|
53809
|
+
if (!new Set(["locale", "model", "orientation", "version"]).has(key) || fieldValue === "") {
|
|
53810
|
+
return;
|
|
53811
|
+
}
|
|
53812
|
+
if (fields.has(key))
|
|
53813
|
+
return;
|
|
53814
|
+
fields.set(key, fieldValue);
|
|
53815
|
+
}
|
|
53816
|
+
const model = fields.get("model");
|
|
53817
|
+
const version4 = fields.get("version");
|
|
53818
|
+
const locale = fields.get("locale") ?? "en";
|
|
53819
|
+
const orientation = fields.get("orientation") ?? "portrait";
|
|
53820
|
+
if (model === undefined || version4 === undefined || !SAFE_ID.test(model) || !SAFE_ID.test(version4) || !/^[A-Za-z]{2,3}(?:[_-][A-Za-z0-9]{2,8})?$/u.test(locale) || orientation !== "portrait" && orientation !== "landscape") {
|
|
53821
|
+
return;
|
|
53822
|
+
}
|
|
53823
|
+
return { model, version: version4, locale, orientation };
|
|
53824
|
+
}
|
|
53825
|
+
function parseFirebaseCatalog(modelsOutput, versionsOutput) {
|
|
53826
|
+
const models = parseJsonArray(modelsOutput);
|
|
53827
|
+
const versions4 = parseJsonArray(versionsOutput);
|
|
53828
|
+
if (models === undefined || versions4 === undefined)
|
|
53829
|
+
return;
|
|
53830
|
+
const versionTags = new Map(versions4.filter((version4) => typeof version4.id === "string").map((version4) => [version4.id, version4.tags ?? []]));
|
|
53831
|
+
const devices = [];
|
|
53832
|
+
for (const model of models) {
|
|
53833
|
+
if (typeof model.id !== "string" || !SAFE_ID.test(model.id))
|
|
53834
|
+
continue;
|
|
53835
|
+
for (const version4 of model.supportedVersionIds ?? []) {
|
|
53836
|
+
if (!SAFE_ID.test(version4))
|
|
53837
|
+
continue;
|
|
53838
|
+
const info = model.perVersionInfo?.find((candidate) => candidate.versionId === version4);
|
|
53839
|
+
devices.push({
|
|
53840
|
+
model: model.id,
|
|
53841
|
+
version: version4,
|
|
53842
|
+
locale: "en",
|
|
53843
|
+
orientation: "portrait",
|
|
53844
|
+
name: model.name ?? model.id,
|
|
53845
|
+
form: model.form ?? "UNKNOWN",
|
|
53846
|
+
...model.formFactor === undefined ? {} : { formFactor: model.formFactor },
|
|
53847
|
+
capacity: capacity(info?.deviceCapacity),
|
|
53848
|
+
tags: [
|
|
53849
|
+
...new Set([
|
|
53850
|
+
...model.tags ?? [],
|
|
53851
|
+
...(model.accessDeniedReasons?.length ?? 0) === 0 ? [] : ["access_denied"],
|
|
53852
|
+
...versionTags.get(version4) ?? [],
|
|
53853
|
+
...info?.tags ?? []
|
|
53854
|
+
])
|
|
53855
|
+
].map((tag) => tag.toLowerCase()).sort()
|
|
53856
|
+
});
|
|
53857
|
+
}
|
|
53858
|
+
}
|
|
53859
|
+
return devices.sort((left, right) => `${left.model}/${left.version}`.localeCompare(`${right.model}/${right.version}`));
|
|
53860
|
+
}
|
|
53861
|
+
function validateDevices(requested, catalog, options) {
|
|
53862
|
+
for (const device of requested) {
|
|
53863
|
+
const current = catalog.find((candidate) => candidate.model === device.model && candidate.version === device.version);
|
|
53864
|
+
if (current === undefined)
|
|
53865
|
+
return `${device.model}/${device.version} is unavailable or incompatible.`;
|
|
53866
|
+
if (hasTag(current.tags, "access_denied"))
|
|
53867
|
+
return `${device.model}/${device.version} is not accessible to this project.`;
|
|
53868
|
+
if (hasTag(current.tags, "deprecated") && options.allowDeprecated !== true)
|
|
53869
|
+
return `${device.model}/${device.version} is deprecated.`;
|
|
53870
|
+
if (hasTag(current.tags, "reduced_stability") && options.allowReducedStability !== true)
|
|
53871
|
+
return `${device.model}/${device.version} currently has reduced stability.`;
|
|
53872
|
+
if (current.capacity === "none")
|
|
53873
|
+
return `${device.model}/${device.version} currently has no capacity.`;
|
|
53874
|
+
if (current.capacity === "low" && options.allowLowCapacity !== true)
|
|
53875
|
+
return `${device.model}/${device.version} currently has low capacity.`;
|
|
53876
|
+
}
|
|
53877
|
+
return;
|
|
53878
|
+
}
|
|
53879
|
+
function gcloudBase(options) {
|
|
53880
|
+
return ["--quiet", ...options.project === undefined ? [] : [`--project=${options.project}`]];
|
|
53881
|
+
}
|
|
53882
|
+
function deviceArgument(device) {
|
|
53883
|
+
return `model=${device.model},version=${device.version},locale=${device.locale},orientation=${device.orientation}`;
|
|
53884
|
+
}
|
|
53885
|
+
function runArguments(options, resultsDir) {
|
|
53886
|
+
return [
|
|
53887
|
+
"firebase",
|
|
53888
|
+
"test",
|
|
53889
|
+
"android",
|
|
53890
|
+
"run",
|
|
53891
|
+
`--type=${options.action}`,
|
|
53892
|
+
`--app=${artifactArgument(options.cwd, options.app ?? "")}`,
|
|
53893
|
+
...options.action === "instrumentation" ? [`--test=${artifactArgument(options.cwd, options.test ?? "")}`] : [],
|
|
53894
|
+
...(options.devices ?? []).flatMap((device) => ["--device", deviceArgument(device)]),
|
|
53895
|
+
...options.testTimeout === undefined ? [] : [`--timeout=${options.testTimeout}`],
|
|
53896
|
+
...options.resultsBucket === undefined ? [] : [`--results-bucket=${options.resultsBucket}`],
|
|
53897
|
+
...options.resultsBucket === undefined ? [] : [`--results-dir=${resultsDir}`],
|
|
53898
|
+
"--format=json",
|
|
53899
|
+
...gcloudBase(options)
|
|
53900
|
+
];
|
|
53901
|
+
}
|
|
53902
|
+
function extractProviderIdentity(output) {
|
|
53903
|
+
const matrixId = output.match(MATRIX_PATTERN)?.[1];
|
|
53904
|
+
const consoleUrl = output.match(CONSOLE_URL_PATTERN)?.[0];
|
|
53905
|
+
return {
|
|
53906
|
+
...matrixId === undefined ? {} : { matrixId },
|
|
53907
|
+
...consoleUrl === undefined ? {} : { consoleUrl }
|
|
53908
|
+
};
|
|
53909
|
+
}
|
|
53910
|
+
function parseOutcomes(output) {
|
|
53911
|
+
const values = parseJsonArray(output) ?? [];
|
|
53912
|
+
return values.flatMap((value) => {
|
|
53913
|
+
if (typeof value.outcome !== "string")
|
|
53914
|
+
return [];
|
|
53915
|
+
return [
|
|
53916
|
+
{
|
|
53917
|
+
outcome: value.outcome,
|
|
53918
|
+
axis: typeof value.axis_value === "string" ? value.axis_value : "unknown",
|
|
53919
|
+
details: typeof value.test_details === "string" ? value.test_details : ""
|
|
53920
|
+
}
|
|
53921
|
+
];
|
|
53922
|
+
});
|
|
53923
|
+
}
|
|
53924
|
+
function classify(result, outcomes) {
|
|
53925
|
+
if (result.timedOut)
|
|
53926
|
+
return {
|
|
53927
|
+
outcome: "observation-timed-out",
|
|
53928
|
+
exitCode: 40 /* ChildProcess */,
|
|
53929
|
+
code: "FTL_OBSERVATION_TIMED_OUT"
|
|
53930
|
+
};
|
|
53931
|
+
if (result.aborted)
|
|
53932
|
+
return {
|
|
53933
|
+
outcome: "observation-stopped",
|
|
53934
|
+
exitCode: 130 /* Interrupted */,
|
|
53935
|
+
code: "FTL_OBSERVATION_STOPPED"
|
|
53936
|
+
};
|
|
53937
|
+
if (AUTH_PATTERN.test(`${result.stderr}
|
|
53938
|
+
${result.stdout}`))
|
|
53939
|
+
return { outcome: "auth-failed", exitCode: 10 /* Environment */, code: "FTL_AUTH_FAILED" };
|
|
53940
|
+
if (result.exitCode === 0) {
|
|
53941
|
+
return {
|
|
53942
|
+
outcome: outcomes.some(({ outcome: outcome2 }) => outcome2.toLowerCase() === "flaky") ? "flaky" : "passed",
|
|
53943
|
+
exitCode: 0 /* Success */,
|
|
53944
|
+
code: ""
|
|
53945
|
+
};
|
|
53946
|
+
}
|
|
53947
|
+
if (result.exitCode === 10)
|
|
53948
|
+
return {
|
|
53949
|
+
outcome: "assertion-failed",
|
|
53950
|
+
exitCode: 40 /* ChildProcess */,
|
|
53951
|
+
code: "FTL_ASSERTION_FAILED"
|
|
53952
|
+
};
|
|
53953
|
+
if (result.exitCode === 15)
|
|
53954
|
+
return { outcome: "inconclusive", exitCode: 40 /* ChildProcess */, code: "FTL_INCONCLUSIVE" };
|
|
53955
|
+
if (result.exitCode === 18)
|
|
53956
|
+
return { outcome: "unsupported", exitCode: 2 /* InvalidInput */, code: "FTL_UNSUPPORTED" };
|
|
53957
|
+
if (result.exitCode === 19)
|
|
53958
|
+
return { outcome: "cancelled", exitCode: 130 /* Interrupted */, code: "FTL_CANCELLED" };
|
|
53959
|
+
return {
|
|
53960
|
+
outcome: "infrastructure-failed",
|
|
53961
|
+
exitCode: 40 /* ChildProcess */,
|
|
53962
|
+
code: "FTL_INFRASTRUCTURE_FAILED"
|
|
53963
|
+
};
|
|
53964
|
+
}
|
|
53965
|
+
function parseStorageObjects(output) {
|
|
53966
|
+
try {
|
|
53967
|
+
const parsed = JSON.parse(output);
|
|
53968
|
+
if (Array.isArray(parsed))
|
|
53969
|
+
return parsed;
|
|
53970
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
53971
|
+
return [parsed];
|
|
53972
|
+
} catch {
|
|
53973
|
+
const records = [];
|
|
53974
|
+
for (const line of output.split(/\r?\n/u).filter((value) => value.trim() !== "")) {
|
|
53975
|
+
try {
|
|
53976
|
+
const parsed = JSON.parse(line);
|
|
53977
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
53978
|
+
return;
|
|
53979
|
+
records.push(parsed);
|
|
53980
|
+
} catch {
|
|
53981
|
+
return;
|
|
53982
|
+
}
|
|
53983
|
+
}
|
|
53984
|
+
return records.length === 0 ? undefined : records;
|
|
53985
|
+
}
|
|
53986
|
+
return;
|
|
53987
|
+
}
|
|
53988
|
+
function safeProviderName(index, url4) {
|
|
53989
|
+
const raw = url4.slice(url4.lastIndexOf("/") + 1) || "artifact";
|
|
53990
|
+
const safe = raw.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 120) || "artifact";
|
|
53991
|
+
return `${String(index + 1).padStart(3, "0")}-${safe}`;
|
|
53992
|
+
}
|
|
53993
|
+
async function collectProviderEvidence(options, runner, destination, resultsDir, remoteRunning) {
|
|
53994
|
+
if (options.resultsBucket === undefined) {
|
|
53995
|
+
return { providerFiles: 0, omittedFiles: 0, collection: "not-configured" };
|
|
53996
|
+
}
|
|
53997
|
+
const remotePrefix = `${options.resultsBucket}/${resultsDir.replace(/^\/+|\/+$/gu, "")}`;
|
|
53998
|
+
if (remoteRunning) {
|
|
53999
|
+
return {
|
|
54000
|
+
providerFiles: 0,
|
|
54001
|
+
omittedFiles: 0,
|
|
54002
|
+
collection: "remote-running",
|
|
54003
|
+
remotePrefix
|
|
54004
|
+
};
|
|
54005
|
+
}
|
|
54006
|
+
const listed = await runner({
|
|
54007
|
+
executable: options.gcloudPath ?? "gcloud",
|
|
54008
|
+
args: ["storage", "ls", "--recursive", "--json", remotePrefix, ...gcloudBase(options)],
|
|
54009
|
+
cwd: options.cwd,
|
|
54010
|
+
timeoutMs: 120000,
|
|
54011
|
+
maxBufferBytes: 4 * 1024 * 1024
|
|
54012
|
+
});
|
|
54013
|
+
if (!success(listed)) {
|
|
54014
|
+
return {
|
|
54015
|
+
providerFiles: 0,
|
|
54016
|
+
omittedFiles: 0,
|
|
54017
|
+
collection: "unavailable",
|
|
54018
|
+
remotePrefix,
|
|
54019
|
+
warning: redactText(listed.stderr || listed.stdout || "gcloud storage listing failed.").value
|
|
54020
|
+
};
|
|
54021
|
+
}
|
|
54022
|
+
const objects = parseStorageObjects(listed.stdout);
|
|
54023
|
+
if (objects === undefined) {
|
|
54024
|
+
return {
|
|
54025
|
+
providerFiles: 0,
|
|
54026
|
+
omittedFiles: 0,
|
|
54027
|
+
collection: "unavailable",
|
|
54028
|
+
remotePrefix,
|
|
54029
|
+
warning: "gcloud storage returned an unreadable artifact listing."
|
|
54030
|
+
};
|
|
54031
|
+
}
|
|
54032
|
+
const prefix = `${remotePrefix}/`;
|
|
54033
|
+
const eligible = objects.flatMap((object5) => {
|
|
54034
|
+
if (object5.type !== "cloud_object" || typeof object5.url !== "string" || !object5.url.startsWith(prefix) || object5.url.includes("?") || !PROVIDER_EXTENSIONS.has(path23.extname(object5.url).toLowerCase())) {
|
|
54035
|
+
return [];
|
|
54036
|
+
}
|
|
54037
|
+
const bytes = Number(object5.metadata?.size);
|
|
54038
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > PROVIDER_FILE_BYTES)
|
|
54039
|
+
return [];
|
|
54040
|
+
return [{ url: object5.url, bytes }];
|
|
54041
|
+
});
|
|
54042
|
+
const selected = [];
|
|
54043
|
+
let totalBytes = 0;
|
|
54044
|
+
for (const object5 of eligible) {
|
|
54045
|
+
if (selected.length >= PROVIDER_FILE_LIMIT || totalBytes + object5.bytes > PROVIDER_TOTAL_BYTES) {
|
|
54046
|
+
continue;
|
|
54047
|
+
}
|
|
54048
|
+
selected.push(object5);
|
|
54049
|
+
totalBytes += object5.bytes;
|
|
54050
|
+
}
|
|
54051
|
+
const providerDirectory = path23.join(destination, "provider");
|
|
54052
|
+
await mkdir8(providerDirectory, { recursive: true, mode: 448 });
|
|
54053
|
+
const manifest = [];
|
|
54054
|
+
for (const [index, object5] of selected.entries()) {
|
|
54055
|
+
const file = safeProviderName(index, object5.url);
|
|
54056
|
+
const copied = await runner({
|
|
54057
|
+
executable: options.gcloudPath ?? "gcloud",
|
|
54058
|
+
args: [
|
|
54059
|
+
"storage",
|
|
54060
|
+
"cp",
|
|
54061
|
+
object5.url,
|
|
54062
|
+
path23.join(providerDirectory, file),
|
|
54063
|
+
...gcloudBase(options)
|
|
54064
|
+
],
|
|
54065
|
+
cwd: options.cwd,
|
|
54066
|
+
timeoutMs: 120000,
|
|
54067
|
+
maxBufferBytes: 256 * 1024
|
|
54068
|
+
});
|
|
54069
|
+
if (!success(copied)) {
|
|
54070
|
+
return {
|
|
54071
|
+
providerFiles: manifest.length,
|
|
54072
|
+
omittedFiles: objects.length - manifest.length,
|
|
54073
|
+
collection: "unavailable",
|
|
54074
|
+
remotePrefix,
|
|
54075
|
+
warning: redactText(copied.stderr || copied.stdout || "gcloud storage copy failed.").value
|
|
54076
|
+
};
|
|
54077
|
+
}
|
|
54078
|
+
const localPath = path23.join(providerDirectory, file);
|
|
54079
|
+
const local = await stat15(localPath).catch(() => {
|
|
54080
|
+
return;
|
|
54081
|
+
});
|
|
54082
|
+
if (local === undefined || !local.isFile() || local.size !== object5.bytes) {
|
|
54083
|
+
return {
|
|
54084
|
+
providerFiles: manifest.length,
|
|
54085
|
+
omittedFiles: objects.length - manifest.length,
|
|
54086
|
+
collection: "unavailable",
|
|
54087
|
+
remotePrefix,
|
|
54088
|
+
warning: `Downloaded provider artifact failed size verification: ${file}`
|
|
54089
|
+
};
|
|
54090
|
+
}
|
|
54091
|
+
await chmod(localPath, 384);
|
|
54092
|
+
manifest.push({ source: object5.url, file, bytes: object5.bytes });
|
|
54093
|
+
}
|
|
54094
|
+
await writeFile8(path23.join(destination, "provider-files.json"), `${JSON.stringify(manifest, null, 2)}
|
|
54095
|
+
`, { mode: 384 });
|
|
54096
|
+
return {
|
|
54097
|
+
providerFiles: manifest.length,
|
|
54098
|
+
omittedFiles: Math.max(0, objects.length - manifest.length),
|
|
54099
|
+
collection: "complete",
|
|
54100
|
+
remotePrefix
|
|
54101
|
+
};
|
|
54102
|
+
}
|
|
54103
|
+
async function retainEvidence(options, commandId, processResult, data, runner, resultsDir) {
|
|
54104
|
+
const relative = path23.join(".adb-ready", "artifacts", `firebase-${commandId}`);
|
|
54105
|
+
const destination = path23.resolve(options.cwd, relative);
|
|
54106
|
+
const temporary = `${destination}.tmp`;
|
|
54107
|
+
await rm9(temporary, { recursive: true, force: true });
|
|
54108
|
+
await mkdir8(temporary, { recursive: true, mode: 448 });
|
|
54109
|
+
try {
|
|
54110
|
+
const provider = await collectProviderEvidence(options, runner, temporary, resultsDir, processResult.timedOut || processResult.aborted);
|
|
54111
|
+
const evidence = {
|
|
54112
|
+
path: relative.split(path23.sep).join("/"),
|
|
54113
|
+
providerFiles: provider.providerFiles,
|
|
54114
|
+
omittedFiles: provider.omittedFiles,
|
|
54115
|
+
collection: provider.collection,
|
|
54116
|
+
...provider.remotePrefix === undefined ? {} : { remotePrefix: provider.remotePrefix }
|
|
54117
|
+
};
|
|
54118
|
+
data.evidence = evidence;
|
|
54119
|
+
await writeFile8(path23.join(temporary, "stdout.txt"), redactText(processResult.stdout).value, {
|
|
54120
|
+
mode: 384
|
|
54121
|
+
});
|
|
54122
|
+
await writeFile8(path23.join(temporary, "stderr.txt"), redactText(processResult.stderr).value, {
|
|
54123
|
+
mode: 384
|
|
54124
|
+
});
|
|
54125
|
+
await writeFile8(path23.join(temporary, "result.json"), `${JSON.stringify(data, null, 2)}
|
|
54126
|
+
`, {
|
|
54127
|
+
mode: 384
|
|
54128
|
+
});
|
|
54129
|
+
await rm9(destination, { recursive: true, force: true });
|
|
54130
|
+
await rename8(temporary, destination);
|
|
54131
|
+
return { evidence, ...provider.warning === undefined ? {} : { warning: provider.warning } };
|
|
54132
|
+
} catch (error5) {
|
|
54133
|
+
await rm9(temporary, { recursive: true, force: true }).catch(() => {
|
|
54134
|
+
return;
|
|
54135
|
+
});
|
|
54136
|
+
throw error5;
|
|
54137
|
+
}
|
|
54138
|
+
}
|
|
54139
|
+
async function readCatalog(options, runner) {
|
|
54140
|
+
const executable = options.gcloudPath ?? "gcloud";
|
|
54141
|
+
const base = gcloudBase(options);
|
|
54142
|
+
const models = await runner({
|
|
54143
|
+
executable,
|
|
54144
|
+
args: ["firebase", "test", "android", "models", "list", "--format=json", ...base],
|
|
54145
|
+
cwd: options.cwd,
|
|
54146
|
+
timeoutMs: 120000
|
|
54147
|
+
});
|
|
54148
|
+
if (!success(models))
|
|
54149
|
+
return { failure: models };
|
|
54150
|
+
const versions4 = await runner({
|
|
54151
|
+
executable,
|
|
54152
|
+
args: ["firebase", "test", "android", "versions", "list", "--format=json", ...base],
|
|
54153
|
+
cwd: options.cwd,
|
|
54154
|
+
timeoutMs: 120000
|
|
54155
|
+
});
|
|
54156
|
+
if (!success(versions4))
|
|
54157
|
+
return { failure: versions4 };
|
|
54158
|
+
const catalog = parseFirebaseCatalog(models.stdout, versions4.stdout);
|
|
54159
|
+
return catalog === undefined ? {} : { catalog };
|
|
54160
|
+
}
|
|
54161
|
+
async function cancelMatrix(options, runner, request) {
|
|
54162
|
+
const token = await runner({
|
|
54163
|
+
executable: options.gcloudPath ?? "gcloud",
|
|
54164
|
+
args: ["auth", "print-access-token", "--quiet"],
|
|
54165
|
+
cwd: options.cwd,
|
|
54166
|
+
timeoutMs: 30000,
|
|
54167
|
+
maxBufferBytes: 64 * 1024
|
|
54168
|
+
});
|
|
54169
|
+
if (!success(token) || token.stdout.trim() === "") {
|
|
54170
|
+
return { ok: false, detail: "Google Cloud authentication is unavailable.", auth: true };
|
|
54171
|
+
}
|
|
54172
|
+
const response = await request(`https://testing.googleapis.com/v1/projects/${encodeURIComponent(options.project ?? "")}/testMatrices/${encodeURIComponent(options.matrixId ?? "")}:cancel`, {
|
|
54173
|
+
method: "POST",
|
|
54174
|
+
headers: { authorization: `Bearer ${token.stdout.trim()}` },
|
|
54175
|
+
signal: AbortSignal.timeout(30000)
|
|
54176
|
+
}).catch(() => {
|
|
54177
|
+
return;
|
|
54178
|
+
});
|
|
54179
|
+
if (response === undefined)
|
|
54180
|
+
return {
|
|
54181
|
+
ok: false,
|
|
54182
|
+
detail: "Firebase Test Lab cancellation could not be reached.",
|
|
54183
|
+
auth: false
|
|
54184
|
+
};
|
|
54185
|
+
if (!response.ok) {
|
|
54186
|
+
const detail = redactText(await response.text()).value;
|
|
54187
|
+
return {
|
|
54188
|
+
ok: false,
|
|
54189
|
+
detail: detail || `Firebase Test Lab returned HTTP ${String(response.status)}.`,
|
|
54190
|
+
auth: response.status === 401 || response.status === 403
|
|
54191
|
+
};
|
|
54192
|
+
}
|
|
54193
|
+
return { ok: true };
|
|
54194
|
+
}
|
|
54195
|
+
async function runFirebaseTestLab(options, dependencies = {}, signal) {
|
|
54196
|
+
const clock = dependencies.clock ?? (() => new Date);
|
|
54197
|
+
const started = clock();
|
|
54198
|
+
const commandId = (dependencies.idFactory ?? randomUUID13)();
|
|
54199
|
+
const runner = dependencies.runner ?? runProcess;
|
|
54200
|
+
const executable = options.gcloudPath ?? "gcloud";
|
|
54201
|
+
if (options.action === "cancel") {
|
|
54202
|
+
if (options.project === undefined || options.matrixId === undefined) {
|
|
54203
|
+
return finish3(started, clock(), commandId, null, [
|
|
54204
|
+
problem2("FTL_CANCEL_INPUT_INVALID", "input.firebase", "Cancellation requires a project and matrix ID.", "Pass --project PROJECT and one exact matrix ID.", commandId, false)
|
|
54205
|
+
], 2 /* InvalidInput */);
|
|
54206
|
+
}
|
|
54207
|
+
if (options.dryRun === true) {
|
|
54208
|
+
const plan2 = {
|
|
54209
|
+
schemaVersion: SCHEMA_VERSION,
|
|
54210
|
+
dryRun: true,
|
|
54211
|
+
steps: [
|
|
54212
|
+
{
|
|
54213
|
+
id: "firebase-cancel",
|
|
54214
|
+
title: `Cancel Firebase Test Lab matrix ${options.matrixId}`,
|
|
54215
|
+
risk: "destructive",
|
|
54216
|
+
executable: "Firebase Testing API",
|
|
54217
|
+
args: [options.project, options.matrixId]
|
|
54218
|
+
}
|
|
54219
|
+
]
|
|
54220
|
+
};
|
|
54221
|
+
return finish3(started, clock(), commandId, {
|
|
54222
|
+
status: "planned",
|
|
54223
|
+
action: "cancel",
|
|
54224
|
+
project: options.project,
|
|
54225
|
+
matrixId: options.matrixId,
|
|
54226
|
+
plan: plan2
|
|
54227
|
+
}, [], 0 /* Success */);
|
|
54228
|
+
}
|
|
54229
|
+
const cancelled = await cancelMatrix(options, runner, dependencies.fetch ?? fetch);
|
|
54230
|
+
if (!cancelled.ok) {
|
|
54231
|
+
return finish3(started, clock(), commandId, null, [
|
|
54232
|
+
problem2(cancelled.auth ? "FTL_AUTH_FAILED" : "FTL_CANCEL_FAILED", cancelled.auth ? "environment.auth" : "provider.firebase", "Firebase Test Lab matrix cancellation failed.", cancelled.detail, commandId)
|
|
54233
|
+
], cancelled.auth ? 10 /* Environment */ : 40 /* ChildProcess */);
|
|
54234
|
+
}
|
|
54235
|
+
return finish3(started, clock(), commandId, {
|
|
54236
|
+
status: "cancelled",
|
|
54237
|
+
action: "cancel",
|
|
54238
|
+
project: options.project,
|
|
54239
|
+
matrixId: options.matrixId,
|
|
54240
|
+
outcome: "cancelled"
|
|
54241
|
+
}, [], 0 /* Success */);
|
|
54242
|
+
}
|
|
54243
|
+
if (options.action === "devices") {
|
|
54244
|
+
const catalogResult2 = await readCatalog(options, runner);
|
|
54245
|
+
if (catalogResult2.failure !== undefined || catalogResult2.catalog === undefined) {
|
|
54246
|
+
const failure4 = catalogResult2.failure;
|
|
54247
|
+
const auth = AUTH_PATTERN.test(`${failure4?.stderr ?? ""}
|
|
54248
|
+
${failure4?.stdout ?? ""}`);
|
|
54249
|
+
return finish3(started, clock(), commandId, null, [
|
|
54250
|
+
problem2(auth ? "FTL_AUTH_FAILED" : "FTL_CATALOG_FAILED", auth ? "environment.auth" : "provider.firebase.catalog", "Firebase Test Lab device catalog is unavailable.", auth ? "Authenticate gcloud for the selected project." : "gcloud did not return a valid machine-readable device catalog.", commandId)
|
|
54251
|
+
], auth ? 10 /* Environment */ : 40 /* ChildProcess */);
|
|
54252
|
+
}
|
|
54253
|
+
return finish3(started, clock(), commandId, {
|
|
54254
|
+
status: "catalogued",
|
|
54255
|
+
action: "devices",
|
|
54256
|
+
...options.project === undefined ? {} : { project: options.project },
|
|
54257
|
+
devices: catalogResult2.catalog
|
|
54258
|
+
}, [], 0 /* Success */);
|
|
54259
|
+
}
|
|
54260
|
+
if (options.app === undefined || options.action === "instrumentation" && options.test === undefined || options.devices === undefined || options.devices.length === 0) {
|
|
54261
|
+
return finish3(started, clock(), commandId, null, [
|
|
54262
|
+
problem2("FTL_RUN_INPUT_INVALID", "input.firebase", "Firebase Test Lab requires explicit artifacts and at least one device.", "Pass --app, --test for instrumentation, and one or more --test-device values.", commandId, false)
|
|
54263
|
+
], 2 /* InvalidInput */);
|
|
54264
|
+
}
|
|
54265
|
+
for (const candidate of [options.app, ...options.test === undefined ? [] : [options.test]]) {
|
|
54266
|
+
if (candidate.startsWith("gs://"))
|
|
54267
|
+
continue;
|
|
54268
|
+
const exists = await stat15(path23.resolve(options.cwd, candidate)).then((entry) => entry.isFile()).catch(() => false);
|
|
54269
|
+
if (!exists)
|
|
54270
|
+
return finish3(started, clock(), commandId, null, [
|
|
54271
|
+
problem2("FTL_ARTIFACT_NOT_FOUND", "input.artifact", "A Firebase Test Lab artifact was not found.", candidate, commandId, false)
|
|
54272
|
+
], 2 /* InvalidInput */);
|
|
54273
|
+
}
|
|
54274
|
+
const catalogResult = await readCatalog(options, runner);
|
|
54275
|
+
if (catalogResult.failure !== undefined || catalogResult.catalog === undefined) {
|
|
54276
|
+
const failure4 = catalogResult.failure;
|
|
54277
|
+
const auth = AUTH_PATTERN.test(`${failure4?.stderr ?? ""}
|
|
54278
|
+
${failure4?.stdout ?? ""}`);
|
|
54279
|
+
return finish3(started, clock(), commandId, null, [
|
|
54280
|
+
problem2(auth ? "FTL_AUTH_FAILED" : "FTL_CATALOG_FAILED", auth ? "environment.auth" : "provider.firebase.catalog", "Firebase Test Lab preflight could not read the live device catalog.", auth ? "Authenticate gcloud for the selected project." : "Retry after checking gcloud and Firebase Test Lab availability.", commandId)
|
|
54281
|
+
], auth ? 10 /* Environment */ : 40 /* ChildProcess */);
|
|
54282
|
+
}
|
|
54283
|
+
const rejected = validateDevices(options.devices, catalogResult.catalog, options);
|
|
54284
|
+
if (rejected !== undefined)
|
|
54285
|
+
return finish3(started, clock(), commandId, null, [
|
|
54286
|
+
problem2("FTL_DEVICE_POLICY_REJECTED", "input.firebase.device", "The requested Firebase Test Lab device was rejected by policy.", rejected, commandId, false)
|
|
54287
|
+
], 2 /* InvalidInput */);
|
|
54288
|
+
const resultsDir = options.resultsDir ?? `adb-ready/${commandId}`;
|
|
54289
|
+
const args = runArguments(options, resultsDir);
|
|
54290
|
+
const plan = {
|
|
54291
|
+
schemaVersion: SCHEMA_VERSION,
|
|
54292
|
+
dryRun: options.dryRun === true,
|
|
54293
|
+
steps: [
|
|
54294
|
+
{
|
|
54295
|
+
id: "firebase-test",
|
|
54296
|
+
title: `Run one bounded Firebase Test Lab ${options.action} matrix`,
|
|
54297
|
+
risk: "open-world",
|
|
54298
|
+
executable,
|
|
54299
|
+
args
|
|
54300
|
+
}
|
|
54301
|
+
]
|
|
54302
|
+
};
|
|
54303
|
+
if (options.dryRun === true)
|
|
54304
|
+
return finish3(started, clock(), commandId, {
|
|
54305
|
+
status: "planned",
|
|
54306
|
+
action: options.action,
|
|
54307
|
+
...options.project === undefined ? {} : { project: options.project },
|
|
54308
|
+
selectedDevices: options.devices,
|
|
54309
|
+
command: { executable, args },
|
|
54310
|
+
plan
|
|
54311
|
+
}, [], 0 /* Success */);
|
|
54312
|
+
const executed = await runner({
|
|
54313
|
+
executable,
|
|
54314
|
+
args,
|
|
54315
|
+
cwd: options.cwd,
|
|
54316
|
+
...signal === undefined ? {} : { signal },
|
|
54317
|
+
timeoutMs: options.observationTimeoutMs ?? 30 * 60000,
|
|
54318
|
+
killSignal: "SIGKILL",
|
|
54319
|
+
killProcessGroup: true,
|
|
54320
|
+
maxBufferBytes: 4 * 1024 * 1024
|
|
54321
|
+
});
|
|
54322
|
+
const results = parseOutcomes(executed.stdout);
|
|
54323
|
+
const classified = classify(executed, results);
|
|
54324
|
+
const identity = extractProviderIdentity(`${executed.stderr}
|
|
54325
|
+
${executed.stdout}`);
|
|
54326
|
+
const data = {
|
|
54327
|
+
status: "completed",
|
|
54328
|
+
action: options.action,
|
|
54329
|
+
...options.project === undefined ? {} : { project: options.project },
|
|
54330
|
+
selectedDevices: options.devices,
|
|
54331
|
+
command: { executable, args },
|
|
54332
|
+
outcome: classified.outcome,
|
|
54333
|
+
...identity,
|
|
54334
|
+
results,
|
|
54335
|
+
...(executed.timedOut || executed.aborted) && identity.matrixId !== undefined ? { remoteContinues: true } : {}
|
|
54336
|
+
};
|
|
54337
|
+
const retained = await retainEvidence(options, commandId, executed, data, runner, resultsDir);
|
|
54338
|
+
const problems = classified.exitCode === 0 /* Success */ ? [] : [
|
|
54339
|
+
problem2(classified.code, `provider.firebase.${classified.outcome}`, `Firebase Test Lab ${classified.outcome.replaceAll("-", " ")}.`, executed.timedOut || executed.aborted ? "Local observation stopped without cancelling the remote matrix. Use test firebase cancel explicitly if cancellation is intended." : redactText(executed.stderr || executed.stdout || "Firebase Test Lab returned no diagnostic output.").value, commandId, classified.outcome !== "assertion-failed")
|
|
54340
|
+
];
|
|
54341
|
+
if (retained.warning !== undefined) {
|
|
54342
|
+
problems.push({
|
|
54343
|
+
...problem2("FTL_EVIDENCE_COLLECTION_FAILED", "evidence.firebase", "Firebase Test Lab provider evidence could not be collected completely.", retained.warning, commandId),
|
|
54344
|
+
severity: "warning"
|
|
54345
|
+
});
|
|
54346
|
+
}
|
|
54347
|
+
return finish3(started, clock(), commandId, data, problems, classified.exitCode);
|
|
54348
|
+
}
|
|
54349
|
+
|
|
54350
|
+
// src/automation/gradle-managed-devices.ts
|
|
54351
|
+
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
54352
|
+
import { mkdir as mkdir9, readdir as readdir5, rename as rename9, rm as rm10, stat as stat16, writeFile as writeFile9 } from "node:fs/promises";
|
|
54353
|
+
import path24 from "node:path";
|
|
54354
|
+
var MANAGED_RESULT_SUFFIX = path24.join("build", "outputs", "androidTest-results", "managedDevice");
|
|
54355
|
+
var MANAGED_REPORT_SUFFIX = path24.join("build", "reports", "androidTests", "managedDevice");
|
|
54356
|
+
var MANAGED_ADDITIONAL_OUTPUT_SUFFIX = path24.join("build", "outputs", "managed_device_android_test_additional_output");
|
|
54357
|
+
var SKIPPED_DIRECTORIES = new Set([".git", ".gradle", ".idea", ".adb-ready", "node_modules"]);
|
|
54358
|
+
function problem3(code, category, summary2, detail, commandId, evidence = []) {
|
|
54359
|
+
return {
|
|
54360
|
+
code,
|
|
54361
|
+
category,
|
|
54362
|
+
severity: "error",
|
|
54363
|
+
summary: summary2,
|
|
54364
|
+
detail,
|
|
54365
|
+
retryable: true,
|
|
54366
|
+
evidence,
|
|
54367
|
+
actions: [],
|
|
54368
|
+
correlation: { commandId }
|
|
54369
|
+
};
|
|
54370
|
+
}
|
|
54371
|
+
function finish4(started, finished, commandId, data, problems, exitCode2) {
|
|
54372
|
+
return {
|
|
54373
|
+
exitCode: exitCode2,
|
|
54374
|
+
result: {
|
|
54375
|
+
schemaVersion: SCHEMA_VERSION,
|
|
54376
|
+
command: "test gradle",
|
|
54377
|
+
commandId,
|
|
54378
|
+
ok: exitCode2 === 0 /* Success */,
|
|
54379
|
+
startedAt: started.toISOString(),
|
|
54380
|
+
finishedAt: finished.toISOString(),
|
|
54381
|
+
durationMs: Math.max(0, finished.getTime() - started.getTime()),
|
|
54382
|
+
data,
|
|
54383
|
+
problems
|
|
54384
|
+
}
|
|
54385
|
+
};
|
|
54386
|
+
}
|
|
54387
|
+
async function regularFile3(candidate) {
|
|
54388
|
+
return await stat16(candidate).then((entry) => entry.isFile()).catch(() => false);
|
|
54389
|
+
}
|
|
54390
|
+
async function resolveGradle2(cwd, explicit) {
|
|
54391
|
+
if (explicit !== undefined) {
|
|
54392
|
+
const executable = path24.isAbsolute(explicit) ? explicit : path24.resolve(cwd, explicit);
|
|
54393
|
+
return { executable, display: explicit };
|
|
54394
|
+
}
|
|
54395
|
+
const wrapper = process.platform === "win32" ? "gradlew.bat" : "gradlew";
|
|
54396
|
+
for (const relative of [wrapper, path24.join("android", wrapper)]) {
|
|
54397
|
+
const executable = path24.join(cwd, relative);
|
|
54398
|
+
if (await regularFile3(executable))
|
|
54399
|
+
return { executable, display: relative.split(path24.sep).join("/") };
|
|
54400
|
+
}
|
|
54401
|
+
return;
|
|
54402
|
+
}
|
|
54403
|
+
function validManagedTask(task) {
|
|
54404
|
+
const leaf = task.split(":").filter(Boolean).at(-1) ?? "";
|
|
54405
|
+
return /^[A-Za-z][A-Za-z0-9_]*AndroidTest$/u.test(leaf) && !/^connected[A-Z]/u.test(leaf) && !/^device[A-Z].*AndroidTest$/u.test(leaf);
|
|
54406
|
+
}
|
|
54407
|
+
function parseGradleManagedTasks(output) {
|
|
54408
|
+
const tasks = new Set;
|
|
54409
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
54410
|
+
const match = line.match(/^\s*(:?[A-Za-z0-9_.:-]+AndroidTest)\s+-\s+(.+)$/u);
|
|
54411
|
+
if (match === null)
|
|
54412
|
+
continue;
|
|
54413
|
+
const task = match[1];
|
|
54414
|
+
const description = (match[2] ?? "").toLowerCase();
|
|
54415
|
+
const leaf = task?.split(":").filter(Boolean).at(-1) ?? "";
|
|
54416
|
+
if (task !== undefined && validManagedTask(task) && (/managed device|all devices (?:defined|in)/u.test(description) || /Group[A-Z].*AndroidTest$/u.test(leaf))) {
|
|
54417
|
+
tasks.add(task);
|
|
54418
|
+
}
|
|
54419
|
+
}
|
|
54420
|
+
return [...tasks].sort((left, right) => left.localeCompare(right));
|
|
54421
|
+
}
|
|
54422
|
+
function gradleArgs(options, task) {
|
|
54423
|
+
return [
|
|
54424
|
+
task,
|
|
54425
|
+
"--console=plain",
|
|
54426
|
+
"--no-daemon",
|
|
54427
|
+
...options.shards === undefined ? [] : [`-Pandroid.experimental.androidTest.numManagedDeviceShards=${String(options.shards)}`],
|
|
54428
|
+
...options.softwareRendering === true ? ["-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect"] : []
|
|
54429
|
+
];
|
|
54430
|
+
}
|
|
54431
|
+
async function findManagedRoots(root) {
|
|
54432
|
+
const found = [];
|
|
54433
|
+
const visit = async (directory, depth) => {
|
|
54434
|
+
if (depth > 8 || found.length >= 100)
|
|
54435
|
+
return;
|
|
54436
|
+
const resultRoot = path24.join(directory, MANAGED_RESULT_SUFFIX);
|
|
54437
|
+
const reportRoot = path24.join(directory, MANAGED_REPORT_SUFFIX);
|
|
54438
|
+
const additionalOutputRoot = path24.join(directory, MANAGED_ADDITIONAL_OUTPUT_SUFFIX);
|
|
54439
|
+
for (const candidate of [resultRoot, reportRoot, additionalOutputRoot]) {
|
|
54440
|
+
const directoryExists = await stat16(candidate).then((entry) => entry.isDirectory()).catch(() => false);
|
|
54441
|
+
if (directoryExists)
|
|
54442
|
+
found.push(candidate);
|
|
54443
|
+
}
|
|
54444
|
+
const entries = await readdir5(directory, { withFileTypes: true }).catch(() => []);
|
|
54445
|
+
for (const entry of entries) {
|
|
54446
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || SKIPPED_DIRECTORIES.has(entry.name))
|
|
54447
|
+
continue;
|
|
54448
|
+
if (entry.name === "build")
|
|
54449
|
+
continue;
|
|
54450
|
+
await visit(path24.join(directory, entry.name), depth + 1);
|
|
54451
|
+
}
|
|
54452
|
+
};
|
|
54453
|
+
await visit(root, 0);
|
|
54454
|
+
return [...new Set(found)].sort((left, right) => left.localeCompare(right));
|
|
54455
|
+
}
|
|
54456
|
+
function junitSummary(documents) {
|
|
54457
|
+
const summary2 = { total: 0, failed: 0, errors: 0, skipped: 0 };
|
|
54458
|
+
for (const document2 of documents) {
|
|
54459
|
+
for (const match of document2.matchAll(/<testsuite\b([^>]*)>/giu)) {
|
|
54460
|
+
const attributes2 = match[1] ?? "";
|
|
54461
|
+
const read = (name) => {
|
|
54462
|
+
const value = attributes2.match(new RegExp(`\\b${name}=["'](\\d+)["']`, "iu"))?.[1];
|
|
54463
|
+
return value === undefined ? 0 : Number(value);
|
|
54464
|
+
};
|
|
54465
|
+
summary2.total += read("tests");
|
|
54466
|
+
summary2.failed += read("failures");
|
|
54467
|
+
summary2.errors += read("errors");
|
|
54468
|
+
summary2.skipped += read("skipped");
|
|
54469
|
+
}
|
|
54470
|
+
}
|
|
54471
|
+
return summary2;
|
|
54472
|
+
}
|
|
54473
|
+
async function retainEvidence2(options) {
|
|
54474
|
+
const safeId = options.commandId.replace(/[^A-Za-z0-9._-]+/gu, "-").slice(0, 80);
|
|
54475
|
+
const relative = path24.posix.join(".adb-ready", "artifacts", `gradle-managed-${safeId}`);
|
|
54476
|
+
const destination = path24.join(options.cwd, ...relative.split("/"));
|
|
54477
|
+
const temporary = `${destination}.tmp-${randomUUID14()}`;
|
|
54478
|
+
const collect = async (freshOnly) => {
|
|
54479
|
+
const files = [];
|
|
54480
|
+
let omitted = 0;
|
|
54481
|
+
let retainedBytes = 0;
|
|
54482
|
+
for (const [rootIndex, root] of options.roots.entries()) {
|
|
54483
|
+
const native = await collectNativeVerifierArtifacts(root, {
|
|
54484
|
+
maxFiles: Math.max(0, 100 - files.length),
|
|
54485
|
+
maxFileBytes: 20 * 1024 * 1024,
|
|
54486
|
+
maxTotalBytes: Math.max(0, 50 * 1024 * 1024 - retainedBytes),
|
|
54487
|
+
...freshOnly ? { modifiedSinceMs: Date.parse(options.process.startedAt) - 2000 } : {}
|
|
54488
|
+
});
|
|
54489
|
+
omitted += native.omitted;
|
|
54490
|
+
for (const artifact of native.artifacts) {
|
|
54491
|
+
files.push({
|
|
54492
|
+
name: path24.posix.join(`root-${String(rootIndex + 1)}`, artifact.relativePath),
|
|
54493
|
+
content: artifact.content
|
|
54494
|
+
});
|
|
54495
|
+
retainedBytes += artifact.content.byteLength;
|
|
54496
|
+
}
|
|
54497
|
+
}
|
|
54498
|
+
return { files, omitted };
|
|
54499
|
+
};
|
|
54500
|
+
let collected = await collect(true);
|
|
54501
|
+
let provenance = collected.files.length === 0 ? "none" : "fresh";
|
|
54502
|
+
if (collected.files.length === 0 && options.outcome === "passed") {
|
|
54503
|
+
collected = await collect(false);
|
|
54504
|
+
if (collected.files.length > 0)
|
|
54505
|
+
provenance = "gradle-cache";
|
|
54506
|
+
}
|
|
54507
|
+
const nativeFiles = collected.files;
|
|
54508
|
+
const omittedFiles = collected.omitted;
|
|
54509
|
+
const reports = nativeFiles.map(({ name }) => path24.posix.join(relative, "native", name));
|
|
54510
|
+
const junitDocuments = nativeFiles.filter(({ name }) => name.toLowerCase().endsWith(".xml")).map(({ content }) => new TextDecoder().decode(content));
|
|
54511
|
+
const tests = junitSummary(junitDocuments);
|
|
54512
|
+
const outcome2 = provenance === "fresh" && tests.failed + tests.errors > 0 ? "assertion-failed" : options.outcome;
|
|
54513
|
+
await mkdir9(path24.dirname(destination), { recursive: true, mode: 448 });
|
|
54514
|
+
await mkdir9(path24.join(temporary, "native"), { recursive: true, mode: 448 });
|
|
54515
|
+
try {
|
|
54516
|
+
for (const file of nativeFiles) {
|
|
54517
|
+
const target = path24.join(temporary, "native", ...file.name.split("/"));
|
|
54518
|
+
await mkdir9(path24.dirname(target), { recursive: true, mode: 448 });
|
|
54519
|
+
const text = /\.(?:html|json|log|txt|xml)$/iu.test(file.name);
|
|
54520
|
+
await writeFile9(target, text ? redactText(new TextDecoder().decode(file.content)).value : file.content, { mode: 384 });
|
|
54521
|
+
}
|
|
54522
|
+
await writeFile9(path24.join(temporary, "stdout.txt"), redactText(options.process.stdout).value, {
|
|
54523
|
+
mode: 384
|
|
54524
|
+
});
|
|
54525
|
+
await writeFile9(path24.join(temporary, "stderr.txt"), redactText(options.process.stderr).value, {
|
|
54526
|
+
mode: 384
|
|
54527
|
+
});
|
|
54528
|
+
await writeFile9(path24.join(temporary, "result.json"), `${JSON.stringify({ schemaVersion: 1, task: options.task, outcome: outcome2, provenance, tests, reports }, null, 2)}
|
|
54529
|
+
`, { mode: 384 });
|
|
54530
|
+
await rename9(temporary, destination);
|
|
54531
|
+
} catch (error5) {
|
|
54532
|
+
await rm10(temporary, { recursive: true, force: true }).catch(() => {
|
|
54533
|
+
return;
|
|
54534
|
+
});
|
|
54535
|
+
throw error5;
|
|
54536
|
+
}
|
|
54537
|
+
return {
|
|
54538
|
+
path: relative,
|
|
54539
|
+
nativeFiles: nativeFiles.length,
|
|
54540
|
+
omittedFiles,
|
|
54541
|
+
reports,
|
|
54542
|
+
tests,
|
|
54543
|
+
outcome: outcome2,
|
|
54544
|
+
provenance
|
|
54545
|
+
};
|
|
54546
|
+
}
|
|
54547
|
+
function successfulProcess(result) {
|
|
54548
|
+
return result.spawnError === undefined && result.streamError === undefined && result.exitCode === 0 && !result.timedOut && !result.aborted;
|
|
54549
|
+
}
|
|
54550
|
+
function identifiesManagedDeviceTask(output, task) {
|
|
54551
|
+
const leaf = task.split(":").filter(Boolean).at(-1) ?? "";
|
|
54552
|
+
return /ManagedDevice|managed[ -]device/iu.test(output) || /Group[A-Z].*AndroidTest$/u.test(leaf) && /all devices defined/iu.test(output);
|
|
54553
|
+
}
|
|
54554
|
+
async function runGradleManagedTest(options, dependencies = {}, signal) {
|
|
54555
|
+
const clock = dependencies.clock ?? (() => new Date);
|
|
54556
|
+
const commandId = (dependencies.idFactory ?? randomUUID14)();
|
|
54557
|
+
const started = clock();
|
|
54558
|
+
const runner = dependencies.runner ?? runProcess;
|
|
54559
|
+
const gradle = await resolveGradle2(options.cwd, options.gradlePath);
|
|
54560
|
+
if (gradle === undefined) {
|
|
54561
|
+
return finish4(started, clock(), commandId, null, [
|
|
54562
|
+
problem3("GRADLE_WRAPPER_NOT_FOUND", "environment.gradle", "No Gradle Wrapper was found for this project.", "Run from the Android project root, keep the Wrapper checked in, or pass --gradle PATH.", commandId)
|
|
54563
|
+
], 10 /* Environment */);
|
|
54564
|
+
}
|
|
54565
|
+
if (options.task === undefined) {
|
|
54566
|
+
const discovered = await runner({
|
|
54567
|
+
executable: gradle.executable,
|
|
54568
|
+
args: ["tasks", "--all", "--console=plain", "--no-daemon"],
|
|
54569
|
+
cwd: options.cwd,
|
|
54570
|
+
...signal === undefined ? {} : { signal },
|
|
54571
|
+
timeoutMs: options.timeoutMs ?? 120000,
|
|
54572
|
+
killProcessGroup: true
|
|
54573
|
+
});
|
|
54574
|
+
if (!successfulProcess(discovered)) {
|
|
54575
|
+
const interrupted = discovered.aborted;
|
|
54576
|
+
return finish4(started, clock(), commandId, null, [
|
|
54577
|
+
problem3(interrupted ? "GRADLE_DISCOVERY_CANCELLED" : "GRADLE_DISCOVERY_FAILED", "automation.gradle.discovery", interrupted ? "Gradle managed-device discovery was cancelled." : "Gradle managed-device tasks could not be discovered.", discovered.timedOut ? "Gradle task discovery exceeded its timeout." : redactText(discovered.stderr || discovered.stdout || "Gradle did not start.").value, commandId)
|
|
54578
|
+
], interrupted ? 130 /* Interrupted */ : 40 /* ChildProcess */);
|
|
54579
|
+
}
|
|
54580
|
+
return finish4(started, clock(), commandId, {
|
|
54581
|
+
status: "discovered",
|
|
54582
|
+
wrapper: gradle.display,
|
|
54583
|
+
tasks: parseGradleManagedTasks(discovered.stdout)
|
|
54584
|
+
}, [], 0 /* Success */);
|
|
54585
|
+
}
|
|
54586
|
+
if (!validManagedTask(options.task)) {
|
|
54587
|
+
return finish4(started, clock(), commandId, null, [
|
|
54588
|
+
problem3("GRADLE_MANAGED_TASK_INVALID", "input.gradle.task", "The requested Gradle task is not a managed-device test task.", "Use a declared task ending in AndroidTest; connected* tasks remain target-bound and belong under adb-ready run.", commandId, [{ source: "cli", field: "task", value: options.task }])
|
|
54589
|
+
], 2 /* InvalidInput */);
|
|
54590
|
+
}
|
|
54591
|
+
const args = gradleArgs(options, options.task);
|
|
54592
|
+
const command = { executable: gradle.display, args };
|
|
54593
|
+
const plan = {
|
|
54594
|
+
schemaVersion: SCHEMA_VERSION,
|
|
54595
|
+
dryRun: options.dryRun === true,
|
|
54596
|
+
steps: [
|
|
54597
|
+
{
|
|
54598
|
+
id: "gradle-managed-test",
|
|
54599
|
+
title: `Let Gradle provision, test, and tear down ${options.task}`,
|
|
54600
|
+
risk: "local-additive",
|
|
54601
|
+
executable: gradle.display,
|
|
54602
|
+
args
|
|
54603
|
+
},
|
|
54604
|
+
{
|
|
54605
|
+
id: "retain-gradle-evidence",
|
|
54606
|
+
title: "Retain bounded native reports and normalized result",
|
|
54607
|
+
risk: "local-additive"
|
|
54608
|
+
}
|
|
54609
|
+
]
|
|
54610
|
+
};
|
|
54611
|
+
if (options.dryRun) {
|
|
54612
|
+
return finish4(started, clock(), commandId, { status: "planned", wrapper: gradle.display, task: options.task, command, plan }, [], 0 /* Success */);
|
|
54613
|
+
}
|
|
54614
|
+
const preflight = await runner({
|
|
54615
|
+
executable: gradle.executable,
|
|
54616
|
+
args: ["help", "--task", options.task, "--console=plain", "--no-daemon"],
|
|
54617
|
+
cwd: options.cwd,
|
|
54618
|
+
...signal === undefined ? {} : { signal },
|
|
54619
|
+
timeoutMs: Math.min(options.timeoutMs ?? 15 * 60000, 120000),
|
|
54620
|
+
killProcessGroup: true
|
|
54621
|
+
});
|
|
54622
|
+
if (!successfulProcess(preflight)) {
|
|
54623
|
+
const cancelled = preflight.aborted;
|
|
54624
|
+
return finish4(started, clock(), commandId, null, [
|
|
54625
|
+
problem3(cancelled ? "GRADLE_TASK_PREFLIGHT_CANCELLED" : "GRADLE_MANAGED_TASK_NOT_FOUND", "automation.gradle.task", cancelled ? "Gradle task validation was cancelled." : "The requested Gradle managed-device task is not declared by this build.", cancelled ? "No Gradle test task was started." : "Run adb-ready test gradle to discover declared managed-device tasks.", commandId, [{ source: "gradle", field: "task", value: options.task }])
|
|
54626
|
+
], cancelled ? 130 /* Interrupted */ : 2 /* InvalidInput */);
|
|
54627
|
+
}
|
|
54628
|
+
if (!identifiesManagedDeviceTask(`${preflight.stdout}
|
|
54629
|
+
${preflight.stderr}`, options.task)) {
|
|
54630
|
+
return finish4(started, clock(), commandId, null, [
|
|
54631
|
+
problem3("GRADLE_TASK_NOT_MANAGED_DEVICE", "input.gradle.task", "Gradle resolved the task, but did not identify it as a managed-device task.", "Run adb-ready test gradle and select a task declared by Android Gradle Plugin managedDevices.", commandId, [{ source: "gradle", field: "task", value: options.task }])
|
|
54632
|
+
], 2 /* InvalidInput */);
|
|
54633
|
+
}
|
|
54634
|
+
const executed = await runner({
|
|
54635
|
+
executable: gradle.executable,
|
|
54636
|
+
args,
|
|
54637
|
+
cwd: options.cwd,
|
|
54638
|
+
...signal === undefined ? {} : { signal },
|
|
54639
|
+
timeoutMs: options.timeoutMs ?? 15 * 60000,
|
|
54640
|
+
killProcessGroup: true
|
|
54641
|
+
});
|
|
54642
|
+
const roots = await findManagedRoots(options.cwd);
|
|
54643
|
+
let provisionalOutcome = successfulProcess(executed) ? "passed" : executed.aborted ? "cancelled" : executed.timedOut ? "timed-out" : "infrastructure-failed";
|
|
54644
|
+
const retained = await retainEvidence2({
|
|
54645
|
+
cwd: options.cwd,
|
|
54646
|
+
commandId,
|
|
54647
|
+
process: executed,
|
|
54648
|
+
task: options.task,
|
|
54649
|
+
outcome: provisionalOutcome,
|
|
54650
|
+
roots
|
|
54651
|
+
});
|
|
54652
|
+
provisionalOutcome = retained.outcome;
|
|
54653
|
+
const exitCode2 = provisionalOutcome === "passed" ? 0 /* Success */ : provisionalOutcome === "cancelled" ? 130 /* Interrupted */ : 40 /* ChildProcess */;
|
|
54654
|
+
const problems = provisionalOutcome === "passed" ? [] : [
|
|
54655
|
+
problem3(provisionalOutcome === "assertion-failed" ? "GRADLE_TEST_ASSERTION_FAILED" : provisionalOutcome === "timed-out" ? "GRADLE_MANAGED_TEST_TIMED_OUT" : provisionalOutcome === "cancelled" ? "GRADLE_MANAGED_TEST_CANCELLED" : "GRADLE_MANAGED_INFRASTRUCTURE_FAILED", provisionalOutcome === "assertion-failed" ? "automation.test.assertion" : "automation.gradle.infrastructure", provisionalOutcome === "assertion-failed" ? "One or more Gradle managed-device assertions failed." : provisionalOutcome === "timed-out" ? "The Gradle managed-device task exceeded its timeout." : provisionalOutcome === "cancelled" ? "The Gradle managed-device task was cancelled." : "Gradle could not complete the managed-device infrastructure lifecycle.", `Review the retained native reports and Gradle output in ${retained.path}.`, commandId)
|
|
54656
|
+
];
|
|
54657
|
+
return finish4(started, clock(), commandId, {
|
|
54658
|
+
status: "completed",
|
|
54659
|
+
wrapper: gradle.display,
|
|
54660
|
+
task: options.task,
|
|
54661
|
+
command,
|
|
54662
|
+
outcome: provisionalOutcome,
|
|
54663
|
+
tests: retained.tests,
|
|
54664
|
+
reports: retained.reports,
|
|
54665
|
+
evidence: {
|
|
54666
|
+
path: retained.path,
|
|
54667
|
+
nativeFiles: retained.nativeFiles,
|
|
54668
|
+
omittedFiles: retained.omittedFiles,
|
|
54669
|
+
provenance: retained.provenance
|
|
54670
|
+
}
|
|
54671
|
+
}, problems, exitCode2);
|
|
54672
|
+
}
|
|
54673
|
+
|
|
54674
|
+
// src/automation/target-pool.ts
|
|
54675
|
+
import { randomUUID as randomUUID15 } from "node:crypto";
|
|
54676
|
+
function skipped(member, status) {
|
|
54677
|
+
return {
|
|
54678
|
+
id: member.id,
|
|
54679
|
+
kind: member.kind,
|
|
54680
|
+
required: member.required !== false,
|
|
54681
|
+
status,
|
|
54682
|
+
exitCode: 130 /* Interrupted */
|
|
54683
|
+
};
|
|
54684
|
+
}
|
|
54685
|
+
async function runTargetPool(options, runMember) {
|
|
54686
|
+
const started = (options.clock ?? (() => new Date))();
|
|
54687
|
+
const commandId = options.idFactory?.() ?? randomUUID15();
|
|
54688
|
+
const maxConcurrency = options.maxConcurrency ?? options.pool.maxConcurrency;
|
|
54689
|
+
const failFast = options.failFast ?? options.pool.failFast;
|
|
54690
|
+
const controller = new AbortController;
|
|
54691
|
+
const cancel = () => controller.abort(options.signal?.reason);
|
|
54692
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
54693
|
+
if (options.signal?.aborted === true)
|
|
54694
|
+
cancel();
|
|
54695
|
+
const members4 = options.pool.members;
|
|
54696
|
+
const results = new Array(members4.length);
|
|
54697
|
+
let nextIndex = 0;
|
|
54698
|
+
let stopScheduling = controller.signal.aborted;
|
|
54699
|
+
const worker = async () => {
|
|
54700
|
+
while (!stopScheduling) {
|
|
54701
|
+
const index = nextIndex;
|
|
54702
|
+
nextIndex += 1;
|
|
54703
|
+
const member = members4[index];
|
|
54704
|
+
if (member === undefined)
|
|
54705
|
+
return;
|
|
54706
|
+
if (controller.signal.aborted) {
|
|
54707
|
+
results[index] = skipped(member, "cancelled");
|
|
54708
|
+
options.onMemberFinish?.(results[index]);
|
|
54709
|
+
return;
|
|
54710
|
+
}
|
|
54711
|
+
try {
|
|
54712
|
+
options.onMemberStart?.(member);
|
|
54713
|
+
const executed = await runMember(member, controller.signal);
|
|
54714
|
+
const status = executed.exitCode === 0 /* Success */ ? "passed" : controller.signal.aborted ? "cancelled" : "failed";
|
|
54715
|
+
results[index] = {
|
|
54716
|
+
id: member.id,
|
|
54717
|
+
kind: member.kind,
|
|
54718
|
+
required: member.required !== false,
|
|
54719
|
+
status,
|
|
54720
|
+
exitCode: executed.exitCode,
|
|
54721
|
+
result: executed.result
|
|
54722
|
+
};
|
|
54723
|
+
options.onMemberFinish?.(results[index]);
|
|
54724
|
+
if (failFast && member.required !== false && status !== "passed") {
|
|
54725
|
+
stopScheduling = true;
|
|
54726
|
+
if (options.cancelRunningOnFailFast !== false) {
|
|
54727
|
+
controller.abort(new Error(`Required pool member ${member.id} failed.`));
|
|
54728
|
+
}
|
|
54729
|
+
}
|
|
54730
|
+
} catch (caught) {
|
|
54731
|
+
const interrupted = controller.signal.aborted;
|
|
54732
|
+
results[index] = {
|
|
54733
|
+
id: member.id,
|
|
54734
|
+
kind: member.kind,
|
|
54735
|
+
required: member.required !== false,
|
|
54736
|
+
status: interrupted ? "cancelled" : "failed",
|
|
54737
|
+
exitCode: interrupted ? 130 /* Interrupted */ : 70 /* Internal */
|
|
54738
|
+
};
|
|
54739
|
+
options.onMemberFinish?.(results[index]);
|
|
54740
|
+
if (failFast && member.required !== false) {
|
|
54741
|
+
stopScheduling = true;
|
|
54742
|
+
if (options.cancelRunningOnFailFast !== false)
|
|
54743
|
+
controller.abort(caught);
|
|
54744
|
+
}
|
|
54745
|
+
}
|
|
54746
|
+
}
|
|
54747
|
+
};
|
|
54748
|
+
await Promise.all(Array.from({ length: Math.min(maxConcurrency, members4.length) }, async () => await worker()));
|
|
54749
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
54750
|
+
for (let index = 0;index < members4.length; index += 1) {
|
|
54751
|
+
if (results[index] === undefined) {
|
|
54752
|
+
const member = members4[index];
|
|
54753
|
+
if (member !== undefined) {
|
|
54754
|
+
const result = skipped(member, controller.signal.aborted ? "cancelled" : "skipped");
|
|
54755
|
+
results[index] = result;
|
|
54756
|
+
options.onMemberFinish?.(result);
|
|
54757
|
+
}
|
|
54758
|
+
}
|
|
54759
|
+
}
|
|
54760
|
+
const completed = results.filter((result) => result !== undefined);
|
|
54761
|
+
const summary2 = {
|
|
54762
|
+
passed: completed.filter(({ status }) => status === "passed").length,
|
|
54763
|
+
failed: completed.filter(({ status }) => status === "failed").length,
|
|
54764
|
+
cancelled: completed.filter(({ status }) => status === "cancelled").length,
|
|
54765
|
+
skipped: completed.filter(({ status }) => status === "skipped").length
|
|
54766
|
+
};
|
|
54767
|
+
const requiredFailure = completed.find(({ required: required4, status }) => required4 && status !== "passed");
|
|
54768
|
+
const exitCode2 = requiredFailure === undefined ? 0 /* Success */ : requiredFailure.status === "cancelled" || requiredFailure.status === "skipped" ? 130 /* Interrupted */ : requiredFailure.exitCode;
|
|
54769
|
+
const finished = (options.clock ?? (() => new Date))();
|
|
54770
|
+
const problems = requiredFailure === undefined ? [] : [
|
|
54771
|
+
{
|
|
54772
|
+
code: "TARGET_POOL_REQUIRED_MEMBER_FAILED",
|
|
54773
|
+
category: "automation.pool",
|
|
54774
|
+
severity: "error",
|
|
54775
|
+
summary: `Required target pool member ${requiredFailure.id} did not pass.`,
|
|
54776
|
+
detail: `The aggregate preserves every member result and cannot succeed while a required member is ${requiredFailure.status}.`,
|
|
54777
|
+
retryable: true,
|
|
54778
|
+
evidence: [
|
|
54779
|
+
{ source: "target.pool", field: "member", value: requiredFailure.id },
|
|
54780
|
+
{ source: "target.pool", field: "status", value: requiredFailure.status }
|
|
54781
|
+
],
|
|
54782
|
+
actions: [],
|
|
54783
|
+
correlation: { commandId }
|
|
54784
|
+
}
|
|
54785
|
+
];
|
|
54786
|
+
return {
|
|
54787
|
+
exitCode: exitCode2,
|
|
54788
|
+
result: {
|
|
54789
|
+
schemaVersion: SCHEMA_VERSION,
|
|
54790
|
+
command: options.command ?? "run pool",
|
|
54791
|
+
commandId,
|
|
54792
|
+
ok: exitCode2 === 0 /* Success */,
|
|
54793
|
+
startedAt: started.toISOString(),
|
|
54794
|
+
finishedAt: finished.toISOString(),
|
|
54795
|
+
durationMs: Math.max(0, finished.getTime() - started.getTime()),
|
|
54796
|
+
data: {
|
|
54797
|
+
pool: options.name,
|
|
54798
|
+
maxConcurrency,
|
|
54799
|
+
failFast,
|
|
54800
|
+
coordination: options.coordination ?? "host-local-filesystem",
|
|
54801
|
+
members: completed,
|
|
54802
|
+
summary: summary2
|
|
54803
|
+
},
|
|
54804
|
+
problems
|
|
54805
|
+
}
|
|
54806
|
+
};
|
|
54807
|
+
}
|
|
54808
|
+
var VALUE_OPTIONS = new Set([
|
|
54809
|
+
"--pool",
|
|
54810
|
+
"--max-concurrency",
|
|
54811
|
+
"--lease-wait",
|
|
54812
|
+
"--device",
|
|
54813
|
+
"--serial",
|
|
54814
|
+
"-s",
|
|
54815
|
+
"--transport-id",
|
|
54816
|
+
"--avd",
|
|
54817
|
+
"--adb-host",
|
|
54818
|
+
"--adb-port",
|
|
54819
|
+
"--format"
|
|
54820
|
+
]);
|
|
54821
|
+
var FLAG_OPTIONS = new Set([
|
|
54822
|
+
"--fail-fast",
|
|
54823
|
+
"--no-fail-fast",
|
|
54824
|
+
"--json",
|
|
54825
|
+
"--select",
|
|
54826
|
+
"--last",
|
|
54827
|
+
"--non-interactive",
|
|
54828
|
+
"--color",
|
|
54829
|
+
"--no-color",
|
|
54830
|
+
"--animation",
|
|
54831
|
+
"--no-animation"
|
|
54832
|
+
]);
|
|
54833
|
+
function targetPoolMemberArguments(argv, member) {
|
|
54834
|
+
const separator = argv.indexOf("--");
|
|
54835
|
+
const options = separator === -1 ? argv : argv.slice(0, separator);
|
|
54836
|
+
const passthrough = separator === -1 ? [] : argv.slice(separator);
|
|
54837
|
+
const retained = [];
|
|
54838
|
+
for (let index = 0;index < options.length; index += 1) {
|
|
54839
|
+
const argument = options[index];
|
|
54840
|
+
if (argument === undefined)
|
|
54841
|
+
continue;
|
|
54842
|
+
const option = argument.split("=", 1)[0] ?? argument;
|
|
54843
|
+
if (VALUE_OPTIONS.has(option)) {
|
|
54844
|
+
if (!argument.includes("="))
|
|
54845
|
+
index += 1;
|
|
54846
|
+
continue;
|
|
54847
|
+
}
|
|
54848
|
+
if (FLAG_OPTIONS.has(option))
|
|
54849
|
+
continue;
|
|
54850
|
+
retained.push(argument);
|
|
54851
|
+
}
|
|
54852
|
+
retained.push("--format", "json", "--non-interactive", "--no-color", "--no-animation");
|
|
54853
|
+
if (member.kind === "adb")
|
|
54854
|
+
retained.push("--device", member.serial);
|
|
54855
|
+
else if (member.kind === "avd")
|
|
54856
|
+
retained.push("--avd", member.name);
|
|
54857
|
+
else {
|
|
54858
|
+
retained.push("--adb-host", member.host, "--adb-port", String(member.port), "--device", member.serial);
|
|
54859
|
+
}
|
|
54860
|
+
return [...retained, ...passthrough];
|
|
54861
|
+
}
|
|
54862
|
+
|
|
53212
54863
|
// src/automation/target-profile.ts
|
|
53213
54864
|
var PROBE_TIMEOUT_MS3 = 1e4;
|
|
53214
54865
|
function validValue(value) {
|
|
@@ -53342,10 +54993,10 @@ async function inspectAndroidTargetProfile(options, runner = runProcess) {
|
|
|
53342
54993
|
}
|
|
53343
54994
|
|
|
53344
54995
|
// src/state/target-state.ts
|
|
53345
|
-
import { randomUUID as
|
|
53346
|
-
import { mkdir as
|
|
54996
|
+
import { randomUUID as randomUUID16 } from "node:crypto";
|
|
54997
|
+
import { mkdir as mkdir10, readFile as readFile14, rename as rename10, rm as rm11, writeFile as writeFile10 } from "node:fs/promises";
|
|
53347
54998
|
import { homedir as homedir8 } from "node:os";
|
|
53348
|
-
import
|
|
54999
|
+
import path25 from "node:path";
|
|
53349
55000
|
import process14 from "node:process";
|
|
53350
55001
|
var EMPTY_STATE = { version: 1, targets: {} };
|
|
53351
55002
|
var MAX_REMEMBERED_SERVERS = 20;
|
|
@@ -53370,7 +55021,7 @@ function parseState(value) {
|
|
|
53370
55021
|
return { version: 1, targets };
|
|
53371
55022
|
}
|
|
53372
55023
|
function defaultTargetStatePath(platform, env, homeDirectory) {
|
|
53373
|
-
const platformPath = platform === "win32" ?
|
|
55024
|
+
const platformPath = platform === "win32" ? path25.win32 : path25.posix;
|
|
53374
55025
|
if (env.XDG_STATE_HOME !== undefined && env.XDG_STATE_HOME.trim() !== "") {
|
|
53375
55026
|
return platformPath.join(env.XDG_STATE_HOME, "adb-ready", "state.json");
|
|
53376
55027
|
}
|
|
@@ -53449,20 +55100,20 @@ async function writeRememberedTarget(target, options = {}) {
|
|
|
53449
55100
|
}
|
|
53450
55101
|
}).sort(([, left], [, right]) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, MAX_REMEMBERED_SERVERS);
|
|
53451
55102
|
const document2 = { version: 1, targets: Object.fromEntries(entries) };
|
|
53452
|
-
const directory =
|
|
53453
|
-
const temporary =
|
|
55103
|
+
const directory = path25.dirname(statePath);
|
|
55104
|
+
const temporary = path25.join(directory, `.state-${String(process14.pid)}-${randomUUID16()}.tmp`);
|
|
53454
55105
|
try {
|
|
53455
|
-
await
|
|
53456
|
-
await
|
|
55106
|
+
await mkdir10(directory, { recursive: true, mode: 448 });
|
|
55107
|
+
await writeFile10(temporary, `${JSON.stringify(document2, null, 2)}
|
|
53457
55108
|
`, {
|
|
53458
55109
|
encoding: "utf8",
|
|
53459
55110
|
flag: "wx",
|
|
53460
55111
|
mode: 384
|
|
53461
55112
|
});
|
|
53462
|
-
await
|
|
55113
|
+
await rename10(temporary, statePath);
|
|
53463
55114
|
return { ok: true, path: statePath };
|
|
53464
55115
|
} catch {
|
|
53465
|
-
await
|
|
55116
|
+
await rm11(temporary, { force: true }).catch(() => {
|
|
53466
55117
|
return;
|
|
53467
55118
|
});
|
|
53468
55119
|
return {
|
|
@@ -54358,6 +56009,16 @@ async function showHomeScreen(options) {
|
|
|
54358
56009
|
label: "Preview project plan",
|
|
54359
56010
|
description: "Resolve the development workflow without allocating a target."
|
|
54360
56011
|
},
|
|
56012
|
+
{
|
|
56013
|
+
value: "test-gradle",
|
|
56014
|
+
label: "Gradle managed-device tests",
|
|
56015
|
+
description: "Discover build-owned virtual-device and group test tasks."
|
|
56016
|
+
},
|
|
56017
|
+
{
|
|
56018
|
+
value: "test-firebase",
|
|
56019
|
+
label: "Firebase Test Lab",
|
|
56020
|
+
description: "Browse cloud devices before running a remote test matrix."
|
|
56021
|
+
},
|
|
54361
56022
|
{
|
|
54362
56023
|
value: "run-help",
|
|
54363
56024
|
label: "Run a bounded verification",
|
|
@@ -54924,6 +56585,18 @@ function isDevData(value) {
|
|
|
54924
56585
|
function isAutomationRunData(value) {
|
|
54925
56586
|
return isRecord2(value) && typeof value.outcome === "string" && isRecord2(value.evidence) && (value.session === null || isDevData(value.session));
|
|
54926
56587
|
}
|
|
56588
|
+
function isGradleManagedTestData(value) {
|
|
56589
|
+
return isRecord2(value) && (value.status === "completed" || value.status === "discovered" || value.status === "planned") && typeof value.wrapper === "string";
|
|
56590
|
+
}
|
|
56591
|
+
function isFirebaseTestLabData(value) {
|
|
56592
|
+
return isRecord2(value) && (value.status === "cancelled" || value.status === "catalogued" || value.status === "completed" || value.status === "planned") && (value.action === "cancel" || value.action === "devices" || value.action === "instrumentation" || value.action === "robo");
|
|
56593
|
+
}
|
|
56594
|
+
function isTargetPoolData(value) {
|
|
56595
|
+
if (!isRecord2(value) || typeof value.pool !== "string" || !Array.isArray(value.members)) {
|
|
56596
|
+
return false;
|
|
56597
|
+
}
|
|
56598
|
+
return typeof value.maxConcurrency === "number" && typeof value.failFast === "boolean" && isRecord2(value.summary) && value.members.every((member) => isRecord2(member) && typeof member.id === "string" && typeof member.kind === "string" && typeof member.required === "boolean" && typeof member.status === "string" && typeof member.exitCode === "number");
|
|
56599
|
+
}
|
|
54927
56600
|
function isVersionData(value) {
|
|
54928
56601
|
return isRecord2(value) && typeof value.version === "string";
|
|
54929
56602
|
}
|
|
@@ -54995,19 +56668,19 @@ function wirelessServiceLabel(service) {
|
|
|
54995
56668
|
const pairingState = service.serviceType !== "connect" || service.knownDevice === undefined ? "" : service.knownDevice ? " · paired" : " · pair first";
|
|
54996
56669
|
return `${clean(identity)} · ${clean(service.endpoint.serial)} · ${clean(service.serviceType)}${pairingState}`;
|
|
54997
56670
|
}
|
|
54998
|
-
function problemLines(
|
|
56671
|
+
function problemLines(problem4, capabilities, verbose) {
|
|
54999
56672
|
const glyphs = symbols(capabilities);
|
|
55000
|
-
const marker =
|
|
56673
|
+
const marker = problem4.severity === "error" && problem4.code !== ProblemCode.OperationInterrupted ? style.failure(glyphs.failure, capabilities) : style.warning(glyphs.warning, capabilities);
|
|
55001
56674
|
const lines = [
|
|
55002
|
-
`${marker} ${style.strong(clean(
|
|
55003
|
-
` ${clean(
|
|
56675
|
+
`${marker} ${style.strong(clean(problem4.summary), capabilities)}`,
|
|
56676
|
+
` ${clean(problem4.detail)}`
|
|
55004
56677
|
];
|
|
55005
|
-
const action =
|
|
56678
|
+
const action = problem4.actions[0];
|
|
55006
56679
|
if (action !== undefined) {
|
|
55007
56680
|
lines.push(` ${style.accent("Next:", capabilities)} ${clean(action.title)}`);
|
|
55008
56681
|
}
|
|
55009
56682
|
if (verbose) {
|
|
55010
|
-
for (const evidence of
|
|
56683
|
+
for (const evidence of problem4.evidence) {
|
|
55011
56684
|
const field = evidence.field === undefined ? evidence.source : `${evidence.source}.${evidence.field}`;
|
|
55012
56685
|
lines.push(` ${style.dim(`${clean(field)}: ${clean(JSON.stringify(evidence.value))}`, capabilities)}`);
|
|
55013
56686
|
}
|
|
@@ -55069,6 +56742,60 @@ function renderHuman(result, options) {
|
|
|
55069
56742
|
if (result.command === "run" && isAutomationRunData(result.data)) {
|
|
55070
56743
|
lines.push(...automationHumanLines(result.data, capabilities));
|
|
55071
56744
|
}
|
|
56745
|
+
if (result.command === "test gradle" && isGradleManagedTestData(result.data)) {
|
|
56746
|
+
const data = result.data;
|
|
56747
|
+
if (data.status === "discovered") {
|
|
56748
|
+
const tasks = data.tasks ?? [];
|
|
56749
|
+
lines.push(`${style.success(glyphs.success, capabilities)} Gradle ${clean(data.wrapper)}`, "", style.strong(`Managed-device tasks (${String(tasks.length)})`, capabilities), ...tasks.length === 0 ? [`${style.dim(glyphs.end, capabilities)} None declared`] : tasks.map((task, index) => {
|
|
56750
|
+
const branch = index === tasks.length - 1 ? glyphs.end : glyphs.branch;
|
|
56751
|
+
return `${style.dim(branch, capabilities)} ${clean(task)}`;
|
|
56752
|
+
}));
|
|
56753
|
+
} else {
|
|
56754
|
+
const marker = data.outcome === undefined || data.outcome === "passed" ? style.success(glyphs.success, capabilities) : style.failure(glyphs.failure, capabilities);
|
|
56755
|
+
lines.push(`${style.success(glyphs.success, capabilities)} Gradle ${clean(data.wrapper)}`, `${marker} Task ${clean(data.task ?? "unresolved")}${data.outcome === undefined ? " · planned" : ` · ${clean(data.outcome)}`}`);
|
|
56756
|
+
if (data.tests !== undefined) {
|
|
56757
|
+
lines.push(`${data.tests.failed + data.tests.errors === 0 ? style.success(glyphs.success, capabilities) : style.failure(glyphs.failure, capabilities)} Tests ${String(data.tests.total)} total · ${String(data.tests.failed)} failed · ${String(data.tests.errors)} errors · ${String(data.tests.skipped)} skipped`);
|
|
56758
|
+
}
|
|
56759
|
+
if (data.evidence !== undefined) {
|
|
56760
|
+
lines.push(`${style.accent(glyphs.active, capabilities)} Evidence ${clean(data.evidence.path)} · ${String(data.evidence.nativeFiles)} native files · ${clean(data.evidence.provenance)}`);
|
|
56761
|
+
}
|
|
56762
|
+
}
|
|
56763
|
+
}
|
|
56764
|
+
if (result.command === "test firebase" && isFirebaseTestLabData(result.data)) {
|
|
56765
|
+
const data = result.data;
|
|
56766
|
+
const marker = data.outcome === "flaky" ? style.warning(glyphs.warning, capabilities) : data.outcome === undefined || data.outcome === "passed" || data.status === "catalogued" ? style.success(glyphs.success, capabilities) : style.failure(glyphs.failure, capabilities);
|
|
56767
|
+
lines.push(`${marker} Firebase ${clean(data.action)} · ${clean(data.status)}${data.outcome === undefined ? "" : ` · ${clean(data.outcome)}`}`);
|
|
56768
|
+
if (data.status === "catalogued") {
|
|
56769
|
+
const devices = data.devices ?? [];
|
|
56770
|
+
lines.push(`${style.accent(glyphs.active, capabilities)} Catalog ${String(devices.length)} compatible model/version choices`, ...devices.slice(0, 20).map((device, index) => {
|
|
56771
|
+
const branch = index === Math.min(devices.length, 20) - 1 ? glyphs.end : glyphs.branch;
|
|
56772
|
+
return `${style.dim(branch, capabilities)} ${clean(device.model)}/${clean(device.version)} · ${clean(device.form.toLowerCase())} · ${clean(device.capacity)} capacity${device.tags.length === 0 ? "" : ` · ${device.tags.map(clean).join(", ")}`}`;
|
|
56773
|
+
}), ...devices.length > 20 ? [
|
|
56774
|
+
style.dim(` ${String(devices.length - 20)} more · use --json for the full catalog`, capabilities)
|
|
56775
|
+
] : []);
|
|
56776
|
+
}
|
|
56777
|
+
if (data.matrixId !== undefined) {
|
|
56778
|
+
lines.push(`${style.accent(glyphs.active, capabilities)} Matrix ${clean(data.matrixId)}`);
|
|
56779
|
+
}
|
|
56780
|
+
if (data.consoleUrl !== undefined) {
|
|
56781
|
+
lines.push(`${style.accent(glyphs.active, capabilities)} Results ${clean(data.consoleUrl)}`);
|
|
56782
|
+
}
|
|
56783
|
+
if (data.remoteContinues === true) {
|
|
56784
|
+
lines.push(`${style.warning(glyphs.warning, capabilities)} Remote still running · explicit cancel required`);
|
|
56785
|
+
}
|
|
56786
|
+
if (data.evidence !== undefined) {
|
|
56787
|
+
lines.push(`${style.accent(glyphs.active, capabilities)} Evidence ${clean(data.evidence.path)} · ${String(data.evidence.providerFiles)} provider files · ${clean(data.evidence.collection)}`);
|
|
56788
|
+
}
|
|
56789
|
+
}
|
|
56790
|
+
if ((result.command === "run pool" || result.command === "test firebase pool") && isTargetPoolData(result.data)) {
|
|
56791
|
+
const data = result.data;
|
|
56792
|
+
lines.push(`${style.accent(glyphs.active, capabilities)} Pool ${clean(data.pool)} · concurrency ${String(data.maxConcurrency)} · ${data.failFast ? "fail fast" : "run all"}`, `${style.success(glyphs.success, capabilities)} Summary ${String(data.summary.passed)} passed · ${String(data.summary.failed)} failed · ${String(data.summary.cancelled)} cancelled · ${String(data.summary.skipped)} skipped`, `${style.dim(glyphs.branch, capabilities)} Coordination ${clean(data.coordination)}`, "", style.strong(`Members (${String(data.members.length)})`, capabilities));
|
|
56793
|
+
data.members.forEach((member, index) => {
|
|
56794
|
+
const branch = index === data.members.length - 1 ? glyphs.end : glyphs.branch;
|
|
56795
|
+
const marker = member.status === "passed" ? style.success(glyphs.success, capabilities) : member.status === "failed" ? style.failure(glyphs.failure, capabilities) : member.status === "cancelled" ? style.warning(glyphs.warning, capabilities) : style.dim(glyphs.skipped, capabilities);
|
|
56796
|
+
lines.push(`${style.dim(branch, capabilities)} ${marker} ${clean(member.id)} · ${clean(member.kind)} · ${clean(member.status)}${member.required ? "" : " · optional"}`);
|
|
56797
|
+
});
|
|
56798
|
+
}
|
|
55072
56799
|
if (result.command === "logs" && isLogsData(result.data)) {
|
|
55073
56800
|
const data = result.data;
|
|
55074
56801
|
lines.push(`${style.success(glyphs.success, capabilities)} Target ${clean(data.selected.target.name)} · ${clean(data.selected.transport.serial)}`, `${style.success(glyphs.success, capabilities)} Filter ${data.filters.map(clean).join(" ")}${data.packageName === undefined ? "" : ` · ${clean(data.packageName)}`}${data.uid === undefined ? "" : ` · UID ${String(data.uid)}`}${data.pid === undefined ? "" : ` · PID ${String(data.pid)}`}`, `${style.success(glyphs.success, capabilities)} Buffers ${data.buffers.length === 0 ? "device default" : data.buffers.map(clean).join(", ")}`, `${style.success(glyphs.success, capabilities)} Records ${String(data.records.length)} retained${data.dropped === 0 ? "" : ` · ${String(data.dropped)} dropped`}`);
|
|
@@ -55224,9 +56951,9 @@ function renderHuman(result, options) {
|
|
|
55224
56951
|
if (data.problems.length === 0) {
|
|
55225
56952
|
lines.push(`${style.dim(glyphs.end, capabilities)} No recorded problems`);
|
|
55226
56953
|
} else {
|
|
55227
|
-
data.problems.forEach((
|
|
56954
|
+
data.problems.forEach((problem4, index) => {
|
|
55228
56955
|
const branch = index === data.problems.length - 1 ? glyphs.end : glyphs.branch;
|
|
55229
|
-
lines.push(`${style.dim(branch, capabilities)} ${style.failure(clean(
|
|
56956
|
+
lines.push(`${style.dim(branch, capabilities)} ${style.failure(clean(problem4.code), capabilities)} · ${clean(problem4.summary)}`, ` ${clean(problem4.detail)}`);
|
|
55230
56957
|
});
|
|
55231
56958
|
}
|
|
55232
56959
|
}
|
|
@@ -55313,8 +57040,8 @@ function renderHuman(result, options) {
|
|
|
55313
57040
|
}
|
|
55314
57041
|
if (result.problems.length > 0) {
|
|
55315
57042
|
lines.push("", style.strong("Diagnostics", capabilities));
|
|
55316
|
-
for (const
|
|
55317
|
-
lines.push(...problemLines(
|
|
57043
|
+
for (const problem4 of result.problems) {
|
|
57044
|
+
lines.push(...problemLines(problem4, capabilities, options.verbose ?? false));
|
|
55318
57045
|
}
|
|
55319
57046
|
}
|
|
55320
57047
|
const interrupted = result.problems.some(({ code }) => code === ProblemCode.OperationInterrupted) && !result.problems.some(({ code, severity }) => severity === "error" && code !== ProblemCode.OperationInterrupted);
|
|
@@ -55343,6 +57070,26 @@ function renderPlain(result, sink) {
|
|
|
55343
57070
|
if (isVersionData(result.data))
|
|
55344
57071
|
sink.write(`version=${clean(result.data.version)}
|
|
55345
57072
|
`);
|
|
57073
|
+
if (isTargetPoolData(result.data)) {
|
|
57074
|
+
sink.write(`pool=${clean(result.data.pool)}
|
|
57075
|
+
`);
|
|
57076
|
+
sink.write(`max_concurrency=${String(result.data.maxConcurrency)}
|
|
57077
|
+
`);
|
|
57078
|
+
sink.write(`fail_fast=${String(result.data.failFast)}
|
|
57079
|
+
`);
|
|
57080
|
+
sink.write(`passed=${String(result.data.summary.passed)}
|
|
57081
|
+
`);
|
|
57082
|
+
sink.write(`failed=${String(result.data.summary.failed)}
|
|
57083
|
+
`);
|
|
57084
|
+
sink.write(`cancelled=${String(result.data.summary.cancelled)}
|
|
57085
|
+
`);
|
|
57086
|
+
sink.write(`skipped=${String(result.data.summary.skipped)}
|
|
57087
|
+
`);
|
|
57088
|
+
result.data.members.forEach((member, index) => {
|
|
57089
|
+
sink.write(`member_${String(index)}=${clean(member.id)}:${clean(member.kind)}:${clean(member.status)}:${String(member.exitCode)}
|
|
57090
|
+
`);
|
|
57091
|
+
});
|
|
57092
|
+
}
|
|
55346
57093
|
if (isConnectData(result.data)) {
|
|
55347
57094
|
sink.write(`endpoint=${clean(result.data.endpoint)}
|
|
55348
57095
|
`);
|
|
@@ -55429,6 +57176,81 @@ function renderPlain(result, sink) {
|
|
|
55429
57176
|
}
|
|
55430
57177
|
if (isAutomationRunData(result.data))
|
|
55431
57178
|
renderAutomationPlain(result.data, sink);
|
|
57179
|
+
if (isGradleManagedTestData(result.data)) {
|
|
57180
|
+
sink.write(`status=${clean(result.data.status)}
|
|
57181
|
+
`);
|
|
57182
|
+
sink.write(`gradle=${clean(result.data.wrapper)}
|
|
57183
|
+
`);
|
|
57184
|
+
if (result.data.task !== undefined)
|
|
57185
|
+
sink.write(`task=${clean(result.data.task)}
|
|
57186
|
+
`);
|
|
57187
|
+
if (result.data.tasks !== undefined) {
|
|
57188
|
+
sink.write(`task_count=${String(result.data.tasks.length)}
|
|
57189
|
+
`);
|
|
57190
|
+
result.data.tasks.forEach((task, index) => {
|
|
57191
|
+
sink.write(`task_${String(index)}=${clean(task)}
|
|
57192
|
+
`);
|
|
57193
|
+
});
|
|
57194
|
+
}
|
|
57195
|
+
if (result.data.outcome !== undefined)
|
|
57196
|
+
sink.write(`outcome=${clean(result.data.outcome)}
|
|
57197
|
+
`);
|
|
57198
|
+
if (result.data.tests !== undefined) {
|
|
57199
|
+
sink.write(`tests=${String(result.data.tests.total)}
|
|
57200
|
+
`);
|
|
57201
|
+
sink.write(`test_failures=${String(result.data.tests.failed)}
|
|
57202
|
+
`);
|
|
57203
|
+
sink.write(`test_errors=${String(result.data.tests.errors)}
|
|
57204
|
+
`);
|
|
57205
|
+
sink.write(`test_skipped=${String(result.data.tests.skipped)}
|
|
57206
|
+
`);
|
|
57207
|
+
}
|
|
57208
|
+
if (result.data.evidence !== undefined) {
|
|
57209
|
+
sink.write(`evidence=${clean(result.data.evidence.path)}
|
|
57210
|
+
`);
|
|
57211
|
+
sink.write(`native_file_count=${String(result.data.evidence.nativeFiles)}
|
|
57212
|
+
`);
|
|
57213
|
+
sink.write(`evidence_provenance=${clean(result.data.evidence.provenance)}
|
|
57214
|
+
`);
|
|
57215
|
+
}
|
|
57216
|
+
}
|
|
57217
|
+
if (isFirebaseTestLabData(result.data)) {
|
|
57218
|
+
sink.write(`status=${clean(result.data.status)}
|
|
57219
|
+
`);
|
|
57220
|
+
sink.write(`action=${clean(result.data.action)}
|
|
57221
|
+
`);
|
|
57222
|
+
if (result.data.project !== undefined)
|
|
57223
|
+
sink.write(`project=${clean(result.data.project)}
|
|
57224
|
+
`);
|
|
57225
|
+
if (result.data.outcome !== undefined)
|
|
57226
|
+
sink.write(`outcome=${clean(result.data.outcome)}
|
|
57227
|
+
`);
|
|
57228
|
+
if (result.data.matrixId !== undefined)
|
|
57229
|
+
sink.write(`matrix_id=${clean(result.data.matrixId)}
|
|
57230
|
+
`);
|
|
57231
|
+
if (result.data.consoleUrl !== undefined)
|
|
57232
|
+
sink.write(`console_url=${clean(result.data.consoleUrl)}
|
|
57233
|
+
`);
|
|
57234
|
+
if (result.data.devices !== undefined)
|
|
57235
|
+
sink.write(`device_count=${String(result.data.devices.length)}
|
|
57236
|
+
`);
|
|
57237
|
+
if (result.data.evidence !== undefined) {
|
|
57238
|
+
sink.write(`evidence=${clean(result.data.evidence.path)}
|
|
57239
|
+
`);
|
|
57240
|
+
sink.write(`provider_file_count=${String(result.data.evidence.providerFiles)}
|
|
57241
|
+
`);
|
|
57242
|
+
sink.write(`provider_file_omitted=${String(result.data.evidence.omittedFiles)}
|
|
57243
|
+
`);
|
|
57244
|
+
sink.write(`evidence_collection=${clean(result.data.evidence.collection)}
|
|
57245
|
+
`);
|
|
57246
|
+
if (result.data.evidence.remotePrefix !== undefined)
|
|
57247
|
+
sink.write(`provider_result_prefix=${clean(result.data.evidence.remotePrefix)}
|
|
57248
|
+
`);
|
|
57249
|
+
}
|
|
57250
|
+
if (result.data.remoteContinues !== undefined)
|
|
57251
|
+
sink.write(`remote_continues=${String(result.data.remoteContinues)}
|
|
57252
|
+
`);
|
|
57253
|
+
}
|
|
55432
57254
|
if (isLogsData(result.data)) {
|
|
55433
57255
|
sink.write(`serial=${clean(result.data.selected.transport.serial)}
|
|
55434
57256
|
`);
|
|
@@ -55621,8 +57443,8 @@ function renderPlain(result, sink) {
|
|
|
55621
57443
|
`);
|
|
55622
57444
|
sink.write(`problem_count=${String(result.data.problems.length)}
|
|
55623
57445
|
`);
|
|
55624
|
-
result.data.problems.forEach((
|
|
55625
|
-
sink.write(`problem=${clean(
|
|
57446
|
+
result.data.problems.forEach((problem4) => {
|
|
57447
|
+
sink.write(`problem=${clean(problem4.code)}:${clean(problem4.summary)}
|
|
55626
57448
|
`);
|
|
55627
57449
|
});
|
|
55628
57450
|
}
|
|
@@ -55740,8 +57562,8 @@ function renderPlain(result, sink) {
|
|
|
55740
57562
|
`);
|
|
55741
57563
|
});
|
|
55742
57564
|
}
|
|
55743
|
-
for (const
|
|
55744
|
-
sink.write(`problem=${clean(
|
|
57565
|
+
for (const problem4 of result.problems) {
|
|
57566
|
+
sink.write(`problem=${clean(problem4.code)}:${clean(problem4.summary)}
|
|
55745
57567
|
`);
|
|
55746
57568
|
}
|
|
55747
57569
|
}
|
|
@@ -55910,12 +57732,259 @@ function resolveTerminalCapabilities(preferences) {
|
|
|
55910
57732
|
};
|
|
55911
57733
|
}
|
|
55912
57734
|
|
|
57735
|
+
// src/cli/completion.ts
|
|
57736
|
+
var COMPLETION_SHELLS = ["bash", "zsh", "fish", "powershell", "nushell"];
|
|
57737
|
+
var COMMANDS = [
|
|
57738
|
+
{ name: "agent", description: "Configure a project-local AI agent bridge", values: ["setup"] },
|
|
57739
|
+
{
|
|
57740
|
+
name: "app",
|
|
57741
|
+
description: "Resolve, inspect, install, or control one app",
|
|
57742
|
+
values: ["resolve", "info", "install", "launch", "stop", "restart", "clear-data", "uninstall"]
|
|
57743
|
+
},
|
|
57744
|
+
{ name: "apps", description: "List packages on one Android target", values: ["list"] },
|
|
57745
|
+
{
|
|
57746
|
+
name: "capture",
|
|
57747
|
+
description: "Capture verified device evidence",
|
|
57748
|
+
values: ["screenshot", "screen-record"]
|
|
57749
|
+
},
|
|
57750
|
+
{ name: "completion", description: "Generate shell completion", values: COMPLETION_SHELLS },
|
|
57751
|
+
{
|
|
57752
|
+
name: "config",
|
|
57753
|
+
description: "Validate or explain configuration",
|
|
57754
|
+
values: ["validate", "explain"]
|
|
57755
|
+
},
|
|
57756
|
+
{ name: "connect", description: "Connect a wireless Android target" },
|
|
57757
|
+
{ name: "context", description: "Export bounded AI-ready context" },
|
|
57758
|
+
{ name: "dev", description: "Start a target-bound development session" },
|
|
57759
|
+
{ name: "devices", description: "List visible Android targets" },
|
|
57760
|
+
{ name: "doctor", description: "Inspect the local ADB environment" },
|
|
57761
|
+
{ name: "help", description: "Show command help" },
|
|
57762
|
+
{ name: "init", description: "Create detected project configuration" },
|
|
57763
|
+
{
|
|
57764
|
+
name: "inspect",
|
|
57765
|
+
description: "Build bounded device evidence",
|
|
57766
|
+
values: ["app", "failures", "ui"]
|
|
57767
|
+
},
|
|
57768
|
+
{ name: "logs", description: "Stream focused target logs" },
|
|
57769
|
+
{ name: "mcp", description: "Serve typed tools over local stdio" },
|
|
57770
|
+
{ name: "open", description: "Open a deep link or web URL" },
|
|
57771
|
+
{ name: "pair", description: "Pair through Wireless debugging" },
|
|
57772
|
+
{ name: "ports", description: "Manage verified TCP mappings", values: ["forward", "reverse"] },
|
|
57773
|
+
{ name: "problems", description: "Show saved session problems" },
|
|
57774
|
+
{ name: "run", description: "Run one bounded verification job" },
|
|
57775
|
+
{ name: "sessions", description: "Inspect saved sessions", values: ["list", "show", "events"] },
|
|
57776
|
+
{ name: "test", description: "Run managed Android tests", values: ["gradle", "firebase"] },
|
|
57777
|
+
{
|
|
57778
|
+
name: "ui",
|
|
57779
|
+
description: "Perform a bounded verified UI action",
|
|
57780
|
+
values: [
|
|
57781
|
+
"audit",
|
|
57782
|
+
"find",
|
|
57783
|
+
"get",
|
|
57784
|
+
"assert",
|
|
57785
|
+
"compare",
|
|
57786
|
+
"tap",
|
|
57787
|
+
"long-press",
|
|
57788
|
+
"scroll",
|
|
57789
|
+
"swipe",
|
|
57790
|
+
"fill",
|
|
57791
|
+
"clear",
|
|
57792
|
+
"type",
|
|
57793
|
+
"press",
|
|
57794
|
+
"keyboard",
|
|
57795
|
+
"permission",
|
|
57796
|
+
"wait"
|
|
57797
|
+
]
|
|
57798
|
+
},
|
|
57799
|
+
{ name: "version", description: "Show version" }
|
|
57800
|
+
];
|
|
57801
|
+
var GLOBAL_OPTIONS = [
|
|
57802
|
+
"--help",
|
|
57803
|
+
"-h",
|
|
57804
|
+
"--version",
|
|
57805
|
+
"-V",
|
|
57806
|
+
"--format",
|
|
57807
|
+
"--json",
|
|
57808
|
+
"--quiet",
|
|
57809
|
+
"--verbose",
|
|
57810
|
+
"--color",
|
|
57811
|
+
"--no-color",
|
|
57812
|
+
"--unicode",
|
|
57813
|
+
"--no-unicode",
|
|
57814
|
+
"--animation",
|
|
57815
|
+
"--no-animation",
|
|
57816
|
+
"--non-interactive",
|
|
57817
|
+
"--timeout",
|
|
57818
|
+
"--adb",
|
|
57819
|
+
"--adb-host",
|
|
57820
|
+
"--adb-port",
|
|
57821
|
+
"--config",
|
|
57822
|
+
"--profile",
|
|
57823
|
+
"--select",
|
|
57824
|
+
"--device",
|
|
57825
|
+
"--serial",
|
|
57826
|
+
"-s",
|
|
57827
|
+
"--transport-id",
|
|
57828
|
+
"--last",
|
|
57829
|
+
"--dry-run"
|
|
57830
|
+
];
|
|
57831
|
+
var commandNames = () => COMMANDS.map(({ name }) => name).join(" ");
|
|
57832
|
+
var optionNames = () => GLOBAL_OPTIONS.join(" ");
|
|
57833
|
+
function bash() {
|
|
57834
|
+
const cases = COMMANDS.filter(({ values: values2 }) => values2 !== undefined).map(({ name, values: values2 }) => ` ${name}) candidates="${values2?.join(" ")}" ;;`).join(`
|
|
57835
|
+
`);
|
|
57836
|
+
return `# Generated by adb-ready. Source this file from ~/.bashrc.
|
|
57837
|
+
_adb_ready_complete() {
|
|
57838
|
+
local current command candidates
|
|
57839
|
+
COMPREPLY=()
|
|
57840
|
+
current="\${COMP_WORDS[COMP_CWORD]}"
|
|
57841
|
+
command="\${COMP_WORDS[1]}"
|
|
57842
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
57843
|
+
candidates="${commandNames()} ${optionNames()}"
|
|
57844
|
+
elif [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
57845
|
+
case "\${command}" in
|
|
57846
|
+
${cases}
|
|
57847
|
+
help) candidates="${commandNames()}" ;;
|
|
57848
|
+
*) candidates="${optionNames()}" ;;
|
|
57849
|
+
esac
|
|
57850
|
+
else
|
|
57851
|
+
candidates="${optionNames()}"
|
|
57852
|
+
fi
|
|
57853
|
+
COMPREPLY=( $(compgen -W "\${candidates}" -- "\${current}") )
|
|
57854
|
+
}
|
|
57855
|
+
complete -F _adb_ready_complete adb-ready adbr
|
|
57856
|
+
`;
|
|
57857
|
+
}
|
|
57858
|
+
function zsh() {
|
|
57859
|
+
const commands = COMMANDS.map(({ name, description }) => ` '${name}:${description.replaceAll("'", "'\\''")}'`).join(`
|
|
57860
|
+
`);
|
|
57861
|
+
const cases = COMMANDS.filter(({ values: values2 }) => values2 !== undefined).map(({ name, values: values2 }) => ` ${name}) _values 'action' ${values2?.join(" ")} ;;`).join(`
|
|
57862
|
+
`);
|
|
57863
|
+
return `#compdef adb-ready adbr
|
|
57864
|
+
# Generated by adb-ready. Add its directory to fpath, then run compinit.
|
|
57865
|
+
_adb_ready() {
|
|
57866
|
+
local -a commands
|
|
57867
|
+
commands=(
|
|
57868
|
+
${commands}
|
|
57869
|
+
)
|
|
57870
|
+
_arguments -C \\
|
|
57871
|
+
'(-h --help)'{-h,--help}'[show help]' \\
|
|
57872
|
+
'(-V --version)'{-V,--version}'[show version]' \\
|
|
57873
|
+
'--format[select output format]:format:(human plain json ndjson)' \\
|
|
57874
|
+
'--json[emit JSON]' \\
|
|
57875
|
+
'--non-interactive[never prompt]' \\
|
|
57876
|
+
'1:command:->command' \\
|
|
57877
|
+
'*::argument:->argument'
|
|
57878
|
+
case $state in
|
|
57879
|
+
command) _describe 'command' commands ;;
|
|
57880
|
+
argument)
|
|
57881
|
+
case $words[2] in
|
|
57882
|
+
${cases}
|
|
57883
|
+
help) _describe 'command' commands ;;
|
|
57884
|
+
esac
|
|
57885
|
+
;;
|
|
57886
|
+
esac
|
|
57887
|
+
}
|
|
57888
|
+
_adb_ready "$@"
|
|
57889
|
+
`;
|
|
57890
|
+
}
|
|
57891
|
+
function fish() {
|
|
57892
|
+
const lines = [
|
|
57893
|
+
"# Generated by adb-ready. Save as ~/.config/fish/completions/adb-ready.fish."
|
|
57894
|
+
];
|
|
57895
|
+
for (const binary of ["adb-ready", "adbr"]) {
|
|
57896
|
+
for (const { name, description } of COMMANDS) {
|
|
57897
|
+
const safeDescription = description.replaceAll("'", "\\'");
|
|
57898
|
+
lines.push(`complete -c ${binary} -f -n '__fish_use_subcommand' -a '${name}' -d '${safeDescription}'`);
|
|
57899
|
+
}
|
|
57900
|
+
for (const { name, values: values2 } of COMMANDS) {
|
|
57901
|
+
if (values2 === undefined)
|
|
57902
|
+
continue;
|
|
57903
|
+
lines.push(`complete -c ${binary} -f -n '__fish_seen_subcommand_from ${name}' -a '${values2.join(" ")}'`);
|
|
57904
|
+
}
|
|
57905
|
+
lines.push(`complete -c ${binary} -s h -l help -d 'Show help'`, `complete -c ${binary} -s V -l version -d 'Show version'`, `complete -c ${binary} -l json -d 'Emit JSON'`, `complete -c ${binary} -l non-interactive -d 'Never prompt'`, `complete -c ${binary} -l dry-run -d 'Plan without mutation'`);
|
|
57906
|
+
}
|
|
57907
|
+
return `${lines.join(`
|
|
57908
|
+
`)}
|
|
57909
|
+
`;
|
|
57910
|
+
}
|
|
57911
|
+
function powershell() {
|
|
57912
|
+
const map = COMMANDS.filter(({ values: values2 }) => values2 !== undefined).map(({ name, values: values2 }) => ` '${name}' = @('${values2?.join("','")}')`).join(`
|
|
57913
|
+
`);
|
|
57914
|
+
return `# Generated by adb-ready. Add this script to your PowerShell profile.
|
|
57915
|
+
$AdbReadyCommands = @('${commandNames().replaceAll(" ", "','")}')
|
|
57916
|
+
$AdbReadyOptions = @('${optionNames().replaceAll(" ", "','")}')
|
|
57917
|
+
$AdbReadyActions = @{
|
|
57918
|
+
${map}
|
|
57919
|
+
}
|
|
57920
|
+
Register-ArgumentCompleter -Native -CommandName adb-ready,adbr -ScriptBlock {
|
|
57921
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
57922
|
+
$words = $commandAst.CommandElements | ForEach-Object { $_.Extent.Text }
|
|
57923
|
+
$candidates = if ($words.Count -le 1) {
|
|
57924
|
+
$AdbReadyCommands + $AdbReadyOptions
|
|
57925
|
+
} elseif ($words.Count -eq 2 -and $AdbReadyActions.ContainsKey($words[1])) {
|
|
57926
|
+
$AdbReadyActions[$words[1]] + $AdbReadyOptions
|
|
57927
|
+
} elseif ($words[1] -eq 'help') {
|
|
57928
|
+
$AdbReadyCommands
|
|
57929
|
+
} else {
|
|
57930
|
+
$AdbReadyOptions
|
|
57931
|
+
}
|
|
57932
|
+
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
57933
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
57934
|
+
}
|
|
57935
|
+
}
|
|
57936
|
+
`;
|
|
57937
|
+
}
|
|
57938
|
+
function nushellExtern(binary) {
|
|
57939
|
+
return `export extern "${binary}" [
|
|
57940
|
+
command?: string@"nu-complete adb-ready commands"
|
|
57941
|
+
...args: string
|
|
57942
|
+
--help(-h)
|
|
57943
|
+
--version(-V)
|
|
57944
|
+
--json
|
|
57945
|
+
--quiet
|
|
57946
|
+
--verbose
|
|
57947
|
+
--non-interactive
|
|
57948
|
+
--dry-run
|
|
57949
|
+
]`;
|
|
57950
|
+
}
|
|
57951
|
+
function nushell() {
|
|
57952
|
+
const records = COMMANDS.map(({ name, description }) => ` { value: "${name}", description: "${description}" }`).join(`
|
|
57953
|
+
`);
|
|
57954
|
+
return `# Generated by adb-ready. Source this file from config.nu.
|
|
57955
|
+
def "nu-complete adb-ready commands" [] {
|
|
57956
|
+
[
|
|
57957
|
+
${records}
|
|
57958
|
+
]
|
|
57959
|
+
}
|
|
57960
|
+
|
|
57961
|
+
${nushellExtern("adb-ready")}
|
|
57962
|
+
|
|
57963
|
+
${nushellExtern("adbr")}
|
|
57964
|
+
`;
|
|
57965
|
+
}
|
|
57966
|
+
function generateCompletion(shell) {
|
|
57967
|
+
switch (shell) {
|
|
57968
|
+
case "bash":
|
|
57969
|
+
return bash();
|
|
57970
|
+
case "zsh":
|
|
57971
|
+
return zsh();
|
|
57972
|
+
case "fish":
|
|
57973
|
+
return fish();
|
|
57974
|
+
case "powershell":
|
|
57975
|
+
return powershell();
|
|
57976
|
+
case "nushell":
|
|
57977
|
+
return nushell();
|
|
57978
|
+
}
|
|
57979
|
+
}
|
|
57980
|
+
|
|
55913
57981
|
// src/cli/arguments.ts
|
|
55914
|
-
var
|
|
57982
|
+
var COMMANDS2 = new Set([
|
|
55915
57983
|
"agent",
|
|
55916
57984
|
"app",
|
|
55917
57985
|
"apps",
|
|
55918
57986
|
"capture",
|
|
57987
|
+
"completion",
|
|
55919
57988
|
"config",
|
|
55920
57989
|
"connect",
|
|
55921
57990
|
"context",
|
|
@@ -55933,6 +58002,7 @@ var COMMANDS = new Set([
|
|
|
55933
58002
|
"problems",
|
|
55934
58003
|
"run",
|
|
55935
58004
|
"sessions",
|
|
58005
|
+
"test",
|
|
55936
58006
|
"ui",
|
|
55937
58007
|
"version"
|
|
55938
58008
|
]);
|
|
@@ -56008,6 +58078,12 @@ var BOOLEAN_OPTIONS = new Set([
|
|
|
56008
58078
|
"--all",
|
|
56009
58079
|
"--all-projects",
|
|
56010
58080
|
"--interactive-only",
|
|
58081
|
+
"--software-rendering",
|
|
58082
|
+
"--allow-deprecated",
|
|
58083
|
+
"--allow-reduced-stability",
|
|
58084
|
+
"--allow-low-capacity",
|
|
58085
|
+
"--fail-fast",
|
|
58086
|
+
"--no-fail-fast",
|
|
56011
58087
|
"--submit",
|
|
56012
58088
|
"--secret-stdin"
|
|
56013
58089
|
]);
|
|
@@ -56058,6 +58134,7 @@ function splitLongOption(argument) {
|
|
|
56058
58134
|
function parseArguments(argv) {
|
|
56059
58135
|
let command;
|
|
56060
58136
|
let helpTarget;
|
|
58137
|
+
let completionShell;
|
|
56061
58138
|
let format = "human";
|
|
56062
58139
|
let quiet = false;
|
|
56063
58140
|
let verbose = false;
|
|
@@ -56079,6 +58156,10 @@ function parseArguments(argv) {
|
|
|
56079
58156
|
let pairingCodeStdin = false;
|
|
56080
58157
|
let remembered = false;
|
|
56081
58158
|
let dryRun = false;
|
|
58159
|
+
let poolName;
|
|
58160
|
+
let maxConcurrency;
|
|
58161
|
+
let failFast;
|
|
58162
|
+
let leaseWaitMs;
|
|
56082
58163
|
let allProjects = false;
|
|
56083
58164
|
let portDirection;
|
|
56084
58165
|
let portAction;
|
|
@@ -56093,6 +58174,24 @@ function parseArguments(argv) {
|
|
|
56093
58174
|
let customCommand;
|
|
56094
58175
|
let runCommand;
|
|
56095
58176
|
let runTimeoutMs;
|
|
58177
|
+
let testKind;
|
|
58178
|
+
let gradleTask;
|
|
58179
|
+
let gradlePath;
|
|
58180
|
+
let gradleShards;
|
|
58181
|
+
let gradleSoftwareRendering = false;
|
|
58182
|
+
let firebaseAction;
|
|
58183
|
+
let firebaseMatrixId;
|
|
58184
|
+
const firebaseDevices = [];
|
|
58185
|
+
let gcloudPath;
|
|
58186
|
+
let cloudProject;
|
|
58187
|
+
let firebaseApp;
|
|
58188
|
+
let firebaseTest;
|
|
58189
|
+
let firebaseResultsBucket;
|
|
58190
|
+
let firebaseResultsDir;
|
|
58191
|
+
let firebaseTestTimeout;
|
|
58192
|
+
let firebaseAllowDeprecated = false;
|
|
58193
|
+
let firebaseAllowReducedStability = false;
|
|
58194
|
+
let firebaseAllowLowCapacity = false;
|
|
56096
58195
|
let avdName;
|
|
56097
58196
|
let deployArtifact = false;
|
|
56098
58197
|
const runArtifactPaths = [];
|
|
@@ -56173,17 +58272,17 @@ function parseArguments(argv) {
|
|
|
56173
58272
|
}
|
|
56174
58273
|
if (!argument.startsWith("-")) {
|
|
56175
58274
|
if (command === undefined) {
|
|
56176
|
-
if (!
|
|
56177
|
-
const hint = suggestion(argument,
|
|
58275
|
+
if (!COMMANDS2.has(argument)) {
|
|
58276
|
+
const hint = suggestion(argument, COMMANDS2);
|
|
56178
58277
|
return failure4("CLI_USAGE", `Unknown command: ${argument}${hint === "" ? "" : `. ${hint}`}`);
|
|
56179
58278
|
}
|
|
56180
58279
|
const candidate = argument;
|
|
56181
58280
|
command = candidate;
|
|
56182
58281
|
continue;
|
|
56183
58282
|
}
|
|
56184
|
-
if (command === "help" &&
|
|
58283
|
+
if (command === "help" && COMMANDS2.has(argument)) {
|
|
56185
58284
|
const candidate = argument;
|
|
56186
|
-
if (candidate === "agent" || candidate === "app" || candidate === "apps" || candidate === "capture" || candidate === "connect" || candidate === "config" || candidate === "context" || candidate === "dev" || candidate === "devices" || candidate === "doctor" || candidate === "logs" || candidate === "mcp" || candidate === "open" || candidate === "init" || candidate === "inspect" || candidate === "pair" || candidate === "ports" || candidate === "problems" || candidate === "run" || candidate === "sessions" || candidate === "ui" || candidate === "version") {
|
|
58285
|
+
if (candidate === "agent" || candidate === "app" || candidate === "apps" || candidate === "capture" || candidate === "completion" || candidate === "connect" || candidate === "config" || candidate === "context" || candidate === "dev" || candidate === "devices" || candidate === "doctor" || candidate === "logs" || candidate === "mcp" || candidate === "open" || candidate === "init" || candidate === "inspect" || candidate === "pair" || candidate === "ports" || candidate === "problems" || candidate === "run" || candidate === "sessions" || candidate === "test" || candidate === "ui" || candidate === "version") {
|
|
56187
58286
|
helpTarget = candidate;
|
|
56188
58287
|
continue;
|
|
56189
58288
|
}
|
|
@@ -56192,10 +58291,35 @@ function parseArguments(argv) {
|
|
|
56192
58291
|
endpoint = argument;
|
|
56193
58292
|
continue;
|
|
56194
58293
|
}
|
|
58294
|
+
if (command === "completion" && completionShell === undefined) {
|
|
58295
|
+
if (!COMPLETION_SHELLS.includes(argument)) {
|
|
58296
|
+
return failure4("CLI_INVALID_VALUE", `Invalid completion shell: ${argument}. Expected bash, zsh, fish, powershell, or nushell.`);
|
|
58297
|
+
}
|
|
58298
|
+
completionShell = argument;
|
|
58299
|
+
continue;
|
|
58300
|
+
}
|
|
56195
58301
|
if (command === "agent" && argument === "setup" && !agentSetupSeen) {
|
|
56196
58302
|
agentSetupSeen = true;
|
|
56197
58303
|
continue;
|
|
56198
58304
|
}
|
|
58305
|
+
if (command === "test") {
|
|
58306
|
+
if (testKind === undefined && (argument === "firebase" || argument === "gradle")) {
|
|
58307
|
+
testKind = argument;
|
|
58308
|
+
continue;
|
|
58309
|
+
}
|
|
58310
|
+
if (testKind === "gradle" && gradleTask === undefined) {
|
|
58311
|
+
gradleTask = argument;
|
|
58312
|
+
continue;
|
|
58313
|
+
}
|
|
58314
|
+
if (testKind === "firebase" && firebaseAction === undefined && (argument === "cancel" || argument === "devices" || argument === "instrumentation" || argument === "robo")) {
|
|
58315
|
+
firebaseAction = argument;
|
|
58316
|
+
continue;
|
|
58317
|
+
}
|
|
58318
|
+
if (testKind === "firebase" && firebaseAction === "cancel" && firebaseMatrixId === undefined) {
|
|
58319
|
+
firebaseMatrixId = argument;
|
|
58320
|
+
continue;
|
|
58321
|
+
}
|
|
58322
|
+
}
|
|
56199
58323
|
if (command === "agent" && agentSetupSeen && agentClient === undefined) {
|
|
56200
58324
|
if (!AGENT_CLIENTS.has(argument)) {
|
|
56201
58325
|
return failure4("CLI_INVALID_VALUE", `Invalid agent client: ${argument}. Expected codex, claude-code, cursor, vscode, windsurf, or generic.`);
|
|
@@ -56373,6 +58497,35 @@ function parseArguments(argv) {
|
|
|
56373
58497
|
remembered = true;
|
|
56374
58498
|
} else if (option === "--dry-run") {
|
|
56375
58499
|
dryRun = true;
|
|
58500
|
+
} else if (option === "--pool") {
|
|
58501
|
+
const value = readValue();
|
|
58502
|
+
if (typeof value !== "string")
|
|
58503
|
+
return value;
|
|
58504
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/u.test(value)) {
|
|
58505
|
+
return failure4("CLI_INVALID_VALUE", `Invalid target pool name: ${value}.`, option);
|
|
58506
|
+
}
|
|
58507
|
+
poolName = value;
|
|
58508
|
+
} else if (option === "--max-concurrency") {
|
|
58509
|
+
const value = readValue();
|
|
58510
|
+
if (typeof value !== "string")
|
|
58511
|
+
return value;
|
|
58512
|
+
const parsed = Number(value);
|
|
58513
|
+
if (!/^\d+$/u.test(value) || !Number.isSafeInteger(parsed) || parsed < 1 || parsed > 32) {
|
|
58514
|
+
return failure4("CLI_INVALID_VALUE", "--max-concurrency must be an integer from 1 to 32.", option);
|
|
58515
|
+
}
|
|
58516
|
+
maxConcurrency = parsed;
|
|
58517
|
+
} else if (option === "--fail-fast") {
|
|
58518
|
+
failFast = true;
|
|
58519
|
+
} else if (option === "--no-fail-fast") {
|
|
58520
|
+
failFast = false;
|
|
58521
|
+
} else if (option === "--lease-wait") {
|
|
58522
|
+
const value = readValue();
|
|
58523
|
+
if (typeof value !== "string")
|
|
58524
|
+
return value;
|
|
58525
|
+
leaseWaitMs = parseDuration(value);
|
|
58526
|
+
if (leaseWaitMs === undefined || leaseWaitMs > 3600000) {
|
|
58527
|
+
return failure4("CLI_INVALID_VALUE", "--lease-wait must be a positive duration up to 1h.", option);
|
|
58528
|
+
}
|
|
56376
58529
|
} else if (option === "--deploy") {
|
|
56377
58530
|
deployArtifact = true;
|
|
56378
58531
|
} else if (option === "--all-projects") {
|
|
@@ -56692,6 +58845,96 @@ function parseArguments(argv) {
|
|
|
56692
58845
|
if (runTimeoutMs === undefined) {
|
|
56693
58846
|
return failure4("CLI_INVALID_VALUE", `Invalid run timeout: ${value}. Use a positive duration such as 30s or 10m.`, option);
|
|
56694
58847
|
}
|
|
58848
|
+
} else if (option === "--gradle") {
|
|
58849
|
+
const value = readValue();
|
|
58850
|
+
if (typeof value !== "string")
|
|
58851
|
+
return value;
|
|
58852
|
+
if (value.trim() !== value || value.length === 0 || !printableValue(value)) {
|
|
58853
|
+
return failure4("CLI_INVALID_VALUE", "--gradle must be a non-empty printable path.", option);
|
|
58854
|
+
}
|
|
58855
|
+
gradlePath = value;
|
|
58856
|
+
} else if (option === "--shards") {
|
|
58857
|
+
const value = readValue();
|
|
58858
|
+
if (typeof value !== "string")
|
|
58859
|
+
return value;
|
|
58860
|
+
const parsed = Number(value);
|
|
58861
|
+
if (!/^\d+$/u.test(value) || !Number.isSafeInteger(parsed) || parsed < 1 || parsed > 256) {
|
|
58862
|
+
return failure4("CLI_INVALID_VALUE", "--shards must be an integer from 1 to 256.", option);
|
|
58863
|
+
}
|
|
58864
|
+
gradleShards = parsed;
|
|
58865
|
+
} else if (option === "--software-rendering") {
|
|
58866
|
+
gradleSoftwareRendering = true;
|
|
58867
|
+
} else if (option === "--allow-deprecated") {
|
|
58868
|
+
firebaseAllowDeprecated = true;
|
|
58869
|
+
} else if (option === "--allow-reduced-stability") {
|
|
58870
|
+
firebaseAllowReducedStability = true;
|
|
58871
|
+
} else if (option === "--allow-low-capacity") {
|
|
58872
|
+
firebaseAllowLowCapacity = true;
|
|
58873
|
+
} else if (option === "--gcloud") {
|
|
58874
|
+
const value = readValue();
|
|
58875
|
+
if (typeof value !== "string")
|
|
58876
|
+
return value;
|
|
58877
|
+
if (value.trim() !== value || value.length === 0 || !printableValue(value)) {
|
|
58878
|
+
return failure4("CLI_INVALID_VALUE", "--gcloud must be a non-empty printable path.", option);
|
|
58879
|
+
}
|
|
58880
|
+
gcloudPath = value;
|
|
58881
|
+
} else if (option === "--project") {
|
|
58882
|
+
const value = readValue();
|
|
58883
|
+
if (typeof value !== "string")
|
|
58884
|
+
return value;
|
|
58885
|
+
if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/u.test(value)) {
|
|
58886
|
+
return failure4("CLI_INVALID_VALUE", "--project must be an explicit Google Cloud project ID.", option);
|
|
58887
|
+
}
|
|
58888
|
+
cloudProject = value;
|
|
58889
|
+
} else if (option === "--app") {
|
|
58890
|
+
const value = readValue();
|
|
58891
|
+
if (typeof value !== "string")
|
|
58892
|
+
return value;
|
|
58893
|
+
if (value.trim() !== value || value.length === 0 || !printableValue(value) || value.startsWith("gs://") && !/^gs:\/\/[a-z0-9][a-z0-9._-]{1,221}[a-z0-9]\/.+/u.test(value) || value.includes("?")) {
|
|
58894
|
+
return failure4("CLI_INVALID_VALUE", "--app must be a non-empty printable artifact path.", option);
|
|
58895
|
+
}
|
|
58896
|
+
firebaseApp = value;
|
|
58897
|
+
} else if (option === "--test-apk") {
|
|
58898
|
+
const value = readValue();
|
|
58899
|
+
if (typeof value !== "string")
|
|
58900
|
+
return value;
|
|
58901
|
+
if (value.trim() !== value || value.length === 0 || !printableValue(value) || value.startsWith("gs://") && !/^gs:\/\/[a-z0-9][a-z0-9._-]{1,221}[a-z0-9]\/.+/u.test(value) || value.includes("?")) {
|
|
58902
|
+
return failure4("CLI_INVALID_VALUE", "--test-apk must be a non-empty printable path.", option);
|
|
58903
|
+
}
|
|
58904
|
+
firebaseTest = value;
|
|
58905
|
+
} else if (option === "--test-device") {
|
|
58906
|
+
const value = readValue();
|
|
58907
|
+
if (typeof value !== "string")
|
|
58908
|
+
return value;
|
|
58909
|
+
const parsed = parseFirebaseDevice(value);
|
|
58910
|
+
if (parsed === undefined) {
|
|
58911
|
+
return failure4("CLI_INVALID_VALUE", "--test-device requires model=ID,version=ID with optional locale and orientation.", option);
|
|
58912
|
+
}
|
|
58913
|
+
firebaseDevices.push(parsed);
|
|
58914
|
+
} else if (option === "--results-bucket") {
|
|
58915
|
+
const value = readValue();
|
|
58916
|
+
if (typeof value !== "string")
|
|
58917
|
+
return value;
|
|
58918
|
+
if (!/^gs:\/\/[a-z0-9][a-z0-9._-]{1,221}[a-z0-9]$/u.test(value)) {
|
|
58919
|
+
return failure4("CLI_INVALID_VALUE", "--results-bucket must be a gs:// bucket URI.", option);
|
|
58920
|
+
}
|
|
58921
|
+
firebaseResultsBucket = value;
|
|
58922
|
+
} else if (option === "--results-dir") {
|
|
58923
|
+
const value = readValue();
|
|
58924
|
+
if (typeof value !== "string")
|
|
58925
|
+
return value;
|
|
58926
|
+
if (value.length > 512 || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/u.test(value) || value.trim() !== value || value.startsWith("/") || value.endsWith("/") || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..") || !printableValue(value)) {
|
|
58927
|
+
return failure4("CLI_INVALID_VALUE", "--results-dir must be a safe unique bucket-relative path.", option);
|
|
58928
|
+
}
|
|
58929
|
+
firebaseResultsDir = value;
|
|
58930
|
+
} else if (option === "--test-timeout") {
|
|
58931
|
+
const value = readValue();
|
|
58932
|
+
if (typeof value !== "string")
|
|
58933
|
+
return value;
|
|
58934
|
+
if (parseDuration(value) === undefined) {
|
|
58935
|
+
return failure4("CLI_INVALID_VALUE", "--test-timeout must be a positive duration such as 5m.", option);
|
|
58936
|
+
}
|
|
58937
|
+
firebaseTestTimeout = value;
|
|
56695
58938
|
} else if (option === "--avd") {
|
|
56696
58939
|
const value = readValue();
|
|
56697
58940
|
if (typeof value !== "string")
|
|
@@ -56797,6 +59040,9 @@ function parseArguments(argv) {
|
|
|
56797
59040
|
"--adb-port",
|
|
56798
59041
|
"--config",
|
|
56799
59042
|
"--profile",
|
|
59043
|
+
"--pool",
|
|
59044
|
+
"--max-concurrency",
|
|
59045
|
+
"--lease-wait",
|
|
56800
59046
|
"--mcp-profile",
|
|
56801
59047
|
"--device",
|
|
56802
59048
|
"--serial",
|
|
@@ -56827,6 +59073,16 @@ function parseArguments(argv) {
|
|
|
56827
59073
|
"--max-records",
|
|
56828
59074
|
"--budget",
|
|
56829
59075
|
"--run-timeout",
|
|
59076
|
+
"--gradle",
|
|
59077
|
+
"--shards",
|
|
59078
|
+
"--gcloud",
|
|
59079
|
+
"--project",
|
|
59080
|
+
"--app",
|
|
59081
|
+
"--test-apk",
|
|
59082
|
+
"--test-device",
|
|
59083
|
+
"--results-bucket",
|
|
59084
|
+
"--results-dir",
|
|
59085
|
+
"--test-timeout",
|
|
56830
59086
|
"--avd",
|
|
56831
59087
|
"--artifact",
|
|
56832
59088
|
"--variant",
|
|
@@ -56891,8 +59147,8 @@ function parseArguments(argv) {
|
|
|
56891
59147
|
if (remembered && (select || device !== undefined || transportId !== undefined)) {
|
|
56892
59148
|
return failure4("CLI_USAGE", "--last cannot be combined with another target selector.");
|
|
56893
59149
|
}
|
|
56894
|
-
if (dryRun && command !== "connect" && command !== "dev" && command !== "run" && command !== "app" && command !== "open" && command !== "pair" && command !== "ports" && command !== "init" && command !== "agent" && command !== "ui") {
|
|
56895
|
-
return failure4("CLI_USAGE", "--dry-run can only be used with app, connect, dev, run, init, open, pair, or
|
|
59150
|
+
if (dryRun && command !== "connect" && command !== "dev" && command !== "run" && command !== "app" && command !== "open" && command !== "pair" && command !== "ports" && command !== "init" && command !== "agent" && command !== "test" && command !== "ui") {
|
|
59151
|
+
return failure4("CLI_USAGE", "--dry-run can only be used with app, connect, dev, run, init, open, pair, ports, test, or agent setup.", "--dry-run");
|
|
56896
59152
|
}
|
|
56897
59153
|
if (command === "ports") {
|
|
56898
59154
|
if (portDirection === undefined) {
|
|
@@ -56926,6 +59182,12 @@ function parseArguments(argv) {
|
|
|
56926
59182
|
if (command === "capture" && captureKind === undefined) {
|
|
56927
59183
|
return failure4("CLI_USAGE", "capture requires screenshot or screen-record.");
|
|
56928
59184
|
}
|
|
59185
|
+
if (command === "completion" && completionShell === undefined) {
|
|
59186
|
+
return failure4("CLI_USAGE", "completion requires bash, zsh, fish, powershell, or nushell.");
|
|
59187
|
+
}
|
|
59188
|
+
if (command === "completion" && argv.some((argument) => argument !== "completion" && argument !== completionShell)) {
|
|
59189
|
+
return failure4("CLI_USAGE", "completion accepts only one shell name; redirect its exact script output to a file.");
|
|
59190
|
+
}
|
|
56929
59191
|
if (command === "inspect" && inspectKind === undefined) {
|
|
56930
59192
|
return failure4("CLI_USAGE", "inspect requires app, failures, or ui.");
|
|
56931
59193
|
}
|
|
@@ -57122,8 +59384,56 @@ function parseArguments(argv) {
|
|
|
57122
59384
|
if (command === "run" && runCommand === undefined) {
|
|
57123
59385
|
return failure4("CLI_USAGE", "run requires a bounded command after --.");
|
|
57124
59386
|
}
|
|
59387
|
+
const poolCommand = command === "run" || command === "test" && testKind === "firebase" && (firebaseAction === "instrumentation" || firebaseAction === "robo");
|
|
59388
|
+
if (poolName !== undefined && !poolCommand) {
|
|
59389
|
+
return failure4("CLI_USAGE", "--pool can only be used with run or a Firebase instrumentation/Robo workflow.", "--pool");
|
|
59390
|
+
}
|
|
59391
|
+
if (poolName !== undefined && (select || remembered || device !== undefined || transportId !== undefined || avdName !== undefined)) {
|
|
59392
|
+
return failure4("CLI_USAGE", "--pool cannot be combined with another target selector.", "--pool");
|
|
59393
|
+
}
|
|
59394
|
+
if (poolName !== undefined && firebaseDevices.length > 0) {
|
|
59395
|
+
return failure4("CLI_USAGE", "--pool cannot be combined with --test-device.", "--pool");
|
|
59396
|
+
}
|
|
59397
|
+
if ((maxConcurrency !== undefined || failFast !== undefined || leaseWaitMs !== undefined) && poolName === undefined) {
|
|
59398
|
+
return failure4("CLI_USAGE", "--max-concurrency, --fail-fast, --no-fail-fast, and --lease-wait require --pool.");
|
|
59399
|
+
}
|
|
59400
|
+
if (leaseWaitMs !== undefined && command !== "run") {
|
|
59401
|
+
return failure4("CLI_USAGE", "--lease-wait only applies to local run pools.", "--lease-wait");
|
|
59402
|
+
}
|
|
57125
59403
|
if (runTimeoutMs !== undefined && command !== "run") {
|
|
57126
|
-
|
|
59404
|
+
if (command !== "test") {
|
|
59405
|
+
return failure4("CLI_USAGE", "--run-timeout can only be used with run or a supported test workflow.", "--run-timeout");
|
|
59406
|
+
}
|
|
59407
|
+
}
|
|
59408
|
+
if (command === "test" && testKind === undefined) {
|
|
59409
|
+
return failure4("CLI_USAGE", "test requires a workflow: adb-ready test gradle [TASK] or adb-ready test firebase ACTION.");
|
|
59410
|
+
}
|
|
59411
|
+
if (testKind !== "gradle" && (gradlePath !== undefined || gradleShards !== undefined || gradleSoftwareRendering)) {
|
|
59412
|
+
return failure4("CLI_USAGE", "--gradle, --shards, and --software-rendering can only be used with test gradle.");
|
|
59413
|
+
}
|
|
59414
|
+
const firebaseOptions = firebaseAction !== undefined || firebaseMatrixId !== undefined || firebaseDevices.length > 0 || gcloudPath !== undefined || cloudProject !== undefined || firebaseApp !== undefined || firebaseTest !== undefined || firebaseResultsBucket !== undefined || firebaseResultsDir !== undefined || firebaseTestTimeout !== undefined || firebaseAllowDeprecated || firebaseAllowReducedStability || firebaseAllowLowCapacity;
|
|
59415
|
+
if (firebaseOptions && (command !== "test" || testKind !== "firebase")) {
|
|
59416
|
+
return failure4("CLI_USAGE", "Firebase options can only be used with adb-ready test firebase.");
|
|
59417
|
+
}
|
|
59418
|
+
if (testKind === "firebase") {
|
|
59419
|
+
if (firebaseAction === undefined) {
|
|
59420
|
+
return failure4("CLI_USAGE", "test firebase requires devices, instrumentation, robo, or cancel.");
|
|
59421
|
+
}
|
|
59422
|
+
if (firebaseResultsDir !== undefined && firebaseResultsBucket === undefined) {
|
|
59423
|
+
return failure4("CLI_USAGE", "--results-dir requires --results-bucket.");
|
|
59424
|
+
}
|
|
59425
|
+
if (firebaseAction === "instrumentation" && firebaseTest === undefined) {
|
|
59426
|
+
return failure4("CLI_USAGE", "Firebase instrumentation requires --test-apk PATH.");
|
|
59427
|
+
}
|
|
59428
|
+
if ((firebaseAction === "instrumentation" || firebaseAction === "robo") && (firebaseApp === undefined || firebaseDevices.length === 0 && poolName === undefined)) {
|
|
59429
|
+
return failure4("CLI_USAGE", `Firebase ${firebaseAction} requires --app PATH and at least one --test-device.`);
|
|
59430
|
+
}
|
|
59431
|
+
if (firebaseAction === "cancel" && (firebaseMatrixId === undefined || cloudProject === undefined)) {
|
|
59432
|
+
return failure4("CLI_USAGE", "Firebase cancellation requires MATRIX_ID and --project PROJECT.");
|
|
59433
|
+
}
|
|
59434
|
+
if ((firebaseAction === "cancel" || firebaseAction === "devices") && (firebaseApp !== undefined || firebaseTest !== undefined || firebaseDevices.length > 0 || firebaseResultsBucket !== undefined || firebaseResultsDir !== undefined || firebaseTestTimeout !== undefined)) {
|
|
59435
|
+
return failure4("CLI_USAGE", `Firebase ${firebaseAction} does not accept test artifacts, dimensions, or result storage options.`);
|
|
59436
|
+
}
|
|
57127
59437
|
}
|
|
57128
59438
|
const autonomousRunOptions = avdName !== undefined || deployArtifact || runArtifactPaths.length > 0 || runVariant !== undefined || javaPath !== undefined || bundletoolPath !== undefined;
|
|
57129
59439
|
if (autonomousRunOptions && command !== "run") {
|
|
@@ -57183,6 +59493,7 @@ function parseArguments(argv) {
|
|
|
57183
59493
|
options: {
|
|
57184
59494
|
command,
|
|
57185
59495
|
...helpTarget === undefined ? {} : { helpTarget },
|
|
59496
|
+
...completionShell === undefined ? {} : { completionShell },
|
|
57186
59497
|
format,
|
|
57187
59498
|
quiet,
|
|
57188
59499
|
verbose,
|
|
@@ -57204,6 +59515,10 @@ function parseArguments(argv) {
|
|
|
57204
59515
|
pairingCodeStdin,
|
|
57205
59516
|
remembered,
|
|
57206
59517
|
dryRun,
|
|
59518
|
+
...poolName === undefined ? {} : { poolName },
|
|
59519
|
+
...maxConcurrency === undefined ? {} : { maxConcurrency },
|
|
59520
|
+
...failFast === undefined ? {} : { failFast },
|
|
59521
|
+
...leaseWaitMs === undefined ? {} : { leaseWaitMs },
|
|
57207
59522
|
allProjects,
|
|
57208
59523
|
...portDirection === undefined ? {} : { portDirection },
|
|
57209
59524
|
...portAction === undefined ? {} : { portAction },
|
|
@@ -57218,6 +59533,24 @@ function parseArguments(argv) {
|
|
|
57218
59533
|
...customCommand === undefined ? {} : { customCommand },
|
|
57219
59534
|
...runCommand === undefined ? {} : { runCommand },
|
|
57220
59535
|
...runTimeoutMs === undefined ? {} : { runTimeoutMs },
|
|
59536
|
+
...testKind === undefined ? {} : { testKind },
|
|
59537
|
+
...gradleTask === undefined ? {} : { gradleTask },
|
|
59538
|
+
...gradlePath === undefined ? {} : { gradlePath },
|
|
59539
|
+
...gradleShards === undefined ? {} : { gradleShards },
|
|
59540
|
+
...gradleSoftwareRendering ? { gradleSoftwareRendering: true } : {},
|
|
59541
|
+
...firebaseAction === undefined ? {} : { firebaseAction },
|
|
59542
|
+
...firebaseMatrixId === undefined ? {} : { firebaseMatrixId },
|
|
59543
|
+
...firebaseDevices.length === 0 ? {} : { firebaseDevices },
|
|
59544
|
+
...gcloudPath === undefined ? {} : { gcloudPath },
|
|
59545
|
+
...cloudProject === undefined ? {} : { cloudProject },
|
|
59546
|
+
...firebaseApp === undefined ? {} : { firebaseApp },
|
|
59547
|
+
...firebaseTest === undefined ? {} : { firebaseTest },
|
|
59548
|
+
...firebaseResultsBucket === undefined ? {} : { firebaseResultsBucket },
|
|
59549
|
+
...firebaseResultsDir === undefined ? {} : { firebaseResultsDir },
|
|
59550
|
+
...firebaseTestTimeout === undefined ? {} : { firebaseTestTimeout },
|
|
59551
|
+
...firebaseAllowDeprecated ? { firebaseAllowDeprecated: true } : {},
|
|
59552
|
+
...firebaseAllowReducedStability ? { firebaseAllowReducedStability: true } : {},
|
|
59553
|
+
...firebaseAllowLowCapacity ? { firebaseAllowLowCapacity: true } : {},
|
|
57221
59554
|
...avdName === undefined ? {} : { avdName },
|
|
57222
59555
|
...deployArtifact ? { deployArtifact: true } : {},
|
|
57223
59556
|
...runArtifactPaths.length === 0 ? {} : { runArtifactPaths },
|
|
@@ -57289,6 +59622,7 @@ Commands:
|
|
|
57289
59622
|
app ACTION [APP_ID] Resolve, inspect, install, or control one app
|
|
57290
59623
|
apps list List packages on one Android target
|
|
57291
59624
|
capture ACTION Save a verified screenshot or bounded screen recording
|
|
59625
|
+
completion SHELL Generate completion for bash, zsh, fish, PowerShell, or Nushell
|
|
57292
59626
|
config ACTION Validate or explain resolved configuration
|
|
57293
59627
|
connect [HOST:PORT] Connect and verify a wireless Android target
|
|
57294
59628
|
context [SESSION] Export bounded AI-ready diagnostic context
|
|
@@ -57304,6 +59638,8 @@ Commands:
|
|
|
57304
59638
|
pair [HOST:PORT] Pair using Android's six-digit pairing code
|
|
57305
59639
|
ports DIRECTION ACTION Manage verified TCP forward/reverse mappings
|
|
57306
59640
|
sessions [ACTION] Inspect saved development sessions
|
|
59641
|
+
test gradle [TASK] Discover or run a Gradle managed-device test
|
|
59642
|
+
test firebase ACTION Run or manage a Firebase Test Lab matrix
|
|
57307
59643
|
ui ACTION Perform a bounded, verified Android UI action
|
|
57308
59644
|
problems [SESSION] Show problems from a saved session
|
|
57309
59645
|
help [COMMAND] Show help
|
|
@@ -57393,6 +59729,12 @@ Writes verified binary evidence inside the current project. Existing files are
|
|
|
57393
59729
|
never replaced unless --force is explicit. Recordings are bounded to 180s.
|
|
57394
59730
|
Screenshot crops use source pixels. Dimension limits preserve aspect ratio and
|
|
57395
59731
|
never upscale the image. The CLI keeps full resolution unless limits are set.
|
|
59732
|
+
`,
|
|
59733
|
+
completion: `Usage: adb-ready completion bash|zsh|fish|powershell|nushell
|
|
59734
|
+
|
|
59735
|
+
Prints a static completion script for both adb-ready and adbr. Generation is
|
|
59736
|
+
offline, deterministic, and never loads project configuration or contacts ADB.
|
|
59737
|
+
See docs/completions.md for one-time installation instructions.
|
|
57396
59738
|
`,
|
|
57397
59739
|
config: `Usage:
|
|
57398
59740
|
adb-ready config validate [options]
|
|
@@ -57446,6 +59788,10 @@ waits for every readiness assertion, runs one bounded verification command,
|
|
|
57446
59788
|
then cleans owned resources. The verification exit code is preserved.
|
|
57447
59789
|
|
|
57448
59790
|
Run options:
|
|
59791
|
+
--pool NAME Fan out through one explicit project target pool
|
|
59792
|
+
--max-concurrency N Override the pool's bounded concurrency (1-32)
|
|
59793
|
+
--[no-]fail-fast Override whether a required failure stops queued work
|
|
59794
|
+
--lease-wait DURATION Bound host-local target queueing (maximum: 1h)
|
|
57449
59795
|
--avd NAME Reuse or start this exact existing AVD
|
|
57450
59796
|
--deploy Discover and deploy one compatible Android artifact
|
|
57451
59797
|
--artifact PATH Deploy this project-local APK, APK Set, or AAB; repeat for splits
|
|
@@ -57462,6 +59808,8 @@ Verification processes also receive ANDROID_SERIAL, ADB_READY_TARGET_SERIAL, and
|
|
|
57462
59808
|
ADB_READY_VERIFIER_OUTPUT_DIR. Direct Maestro test and Android CLI layout/screen-capture commands
|
|
57463
59809
|
are pinned to the selected target and their native reports are retained automatically.
|
|
57464
59810
|
AVDs must already exist. ADB Ready never creates or upgrades an SDK or AVD implicitly.
|
|
59811
|
+
Pool members each retain an isolated session and result. Pool mode never changes the
|
|
59812
|
+
single-target invariant inside a run.
|
|
57465
59813
|
`,
|
|
57466
59814
|
doctor: `Usage: adb-ready doctor [options]
|
|
57467
59815
|
|
|
@@ -57611,6 +59959,50 @@ List filters:
|
|
|
57611
59959
|
--since DURATION Keep sessions updated in the final window
|
|
57612
59960
|
--preset NAME Keep sessions using one development preset
|
|
57613
59961
|
--limit COUNT Return at most 1-100 sessions
|
|
59962
|
+
`,
|
|
59963
|
+
test: `Usage:
|
|
59964
|
+
adb-ready test gradle
|
|
59965
|
+
adb-ready test gradle TASK [options]
|
|
59966
|
+
adb-ready test firebase devices [options]
|
|
59967
|
+
adb-ready test firebase instrumentation [options]
|
|
59968
|
+
adb-ready test firebase robo [options]
|
|
59969
|
+
adb-ready test firebase cancel MATRIX_ID --project PROJECT [options]
|
|
59970
|
+
|
|
59971
|
+
Discovers declared Gradle Managed Device tasks or hands one exact task to the
|
|
59972
|
+
project's checked-in Gradle Wrapper. Gradle owns provisioning, test execution,
|
|
59973
|
+
sharding, and teardown; ADB Ready never leases or controls its transient target.
|
|
59974
|
+
|
|
59975
|
+
Gradle managed-device options:
|
|
59976
|
+
--gradle PATH Use an explicit Gradle Wrapper or executable
|
|
59977
|
+
--shards COUNT Request 1-256 uniform managed-device shards
|
|
59978
|
+
--software-rendering Request Gradle's SwiftShader server rendering mode
|
|
59979
|
+
--run-timeout DURATION Bound task execution (default: 15m)
|
|
59980
|
+
--dry-run Print the exact task plan without starting Gradle
|
|
59981
|
+
|
|
59982
|
+
Native JUnit XML and HTML reports are retained with bounded Gradle output under
|
|
59983
|
+
.adb-ready/artifacts/. Product assertion failures, infrastructure failures,
|
|
59984
|
+
timeouts, cancellation, and missing tasks remain distinct structured outcomes.
|
|
59985
|
+
|
|
59986
|
+
Firebase Test Lab options:
|
|
59987
|
+
--pool NAME Run one matrix per named Firebase dimension
|
|
59988
|
+
--max-concurrency N Bound simultaneously observed matrices (1-32)
|
|
59989
|
+
--[no-]fail-fast Stop submitting queued dimensions after a required failure
|
|
59990
|
+
--project PROJECT Use one explicit Google Cloud project
|
|
59991
|
+
--gcloud PATH Use an explicit gcloud executable
|
|
59992
|
+
--app PATH App APK or supported app bundle
|
|
59993
|
+
--test-apk PATH Instrumentation test APK
|
|
59994
|
+
--test-device DIMENSIONS Exact model=ID,version=ID[,locale=..,orientation=..]
|
|
59995
|
+
--test-timeout DURATION Provider-side timeout for each execution
|
|
59996
|
+
--run-timeout DURATION Bound local observation (default: 30m)
|
|
59997
|
+
--results-bucket GS_URI Store provider artifacts in an owned GCS bucket
|
|
59998
|
+
--results-dir PATH Unique bucket-relative result directory
|
|
59999
|
+
--allow-deprecated Explicitly accept a deprecated catalog dimension
|
|
60000
|
+
--allow-reduced-stability Explicitly accept a reduced-stability dimension
|
|
60001
|
+
--allow-low-capacity Explicitly accept a low-capacity dimension
|
|
60002
|
+
|
|
60003
|
+
Every run validates dimensions against the live catalog before upload. Stopping
|
|
60004
|
+
local observation leaves the remote matrix running; only the explicit cancel
|
|
60005
|
+
action cancels it.
|
|
57614
60006
|
`,
|
|
57615
60007
|
version: `Usage: adb-ready version
|
|
57616
60008
|
|
|
@@ -57619,6 +60011,92 @@ configuration or ADB. Human output and --version remain one bare version line;
|
|
|
57619
60011
|
--format plain, --json, and --format ndjson use the global automation contract.
|
|
57620
60012
|
`
|
|
57621
60013
|
};
|
|
60014
|
+
|
|
60015
|
+
class CapturedOutput {
|
|
60016
|
+
isTTY = false;
|
|
60017
|
+
columns = 120;
|
|
60018
|
+
rows = 40;
|
|
60019
|
+
#chunks = [];
|
|
60020
|
+
write(value) {
|
|
60021
|
+
this.#chunks.push(value);
|
|
60022
|
+
}
|
|
60023
|
+
text() {
|
|
60024
|
+
return this.#chunks.join("");
|
|
60025
|
+
}
|
|
60026
|
+
}
|
|
60027
|
+
function parsedResultEnvelope(value) {
|
|
60028
|
+
try {
|
|
60029
|
+
const parsed = JSON.parse(value.trim());
|
|
60030
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
60031
|
+
return;
|
|
60032
|
+
const result = parsed;
|
|
60033
|
+
return result.schemaVersion === SCHEMA_VERSION && typeof result.command === "string" && typeof result.commandId === "string" && typeof result.ok === "boolean" && typeof result.startedAt === "string" && typeof result.finishedAt === "string" && typeof result.durationMs === "number" && Array.isArray(result.problems) ? result : undefined;
|
|
60034
|
+
} catch {
|
|
60035
|
+
return;
|
|
60036
|
+
}
|
|
60037
|
+
}
|
|
60038
|
+
function targetPoolProblem(code, summary2, detail2, commandId = "target-pool", retryable = false) {
|
|
60039
|
+
return {
|
|
60040
|
+
code,
|
|
60041
|
+
category: "automation.pool",
|
|
60042
|
+
severity: "error",
|
|
60043
|
+
summary: summary2,
|
|
60044
|
+
detail: detail2,
|
|
60045
|
+
retryable,
|
|
60046
|
+
evidence: [],
|
|
60047
|
+
actions: [],
|
|
60048
|
+
correlation: { commandId }
|
|
60049
|
+
};
|
|
60050
|
+
}
|
|
60051
|
+
function targetPoolProgress(options, io) {
|
|
60052
|
+
if (options.format !== "human" || options.quiet)
|
|
60053
|
+
return {};
|
|
60054
|
+
return {
|
|
60055
|
+
onMemberStart: (member) => io.error.write(`pool ${member.id} running
|
|
60056
|
+
`),
|
|
60057
|
+
onMemberFinish: (member) => io.error.write(`pool ${member.id} ${member.status} · exit ${String(member.exitCode)}
|
|
60058
|
+
`)
|
|
60059
|
+
};
|
|
60060
|
+
}
|
|
60061
|
+
async function runLocalTargetPool(argv, options, pool, io, dependencies, signal) {
|
|
60062
|
+
return await runTargetPool({
|
|
60063
|
+
name: options.poolName ?? "",
|
|
60064
|
+
pool,
|
|
60065
|
+
...options.maxConcurrency === undefined ? {} : { maxConcurrency: options.maxConcurrency },
|
|
60066
|
+
...options.failFast === undefined ? {} : { failFast: options.failFast },
|
|
60067
|
+
...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
|
|
60068
|
+
...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
|
|
60069
|
+
...signal === undefined ? {} : { signal },
|
|
60070
|
+
...targetPoolProgress(options, io)
|
|
60071
|
+
}, async (member, memberSignal) => {
|
|
60072
|
+
if (member.kind === "firebase") {
|
|
60073
|
+
throw new Error("Firebase dimensions cannot execute through a local run pool.");
|
|
60074
|
+
}
|
|
60075
|
+
const output = new CapturedOutput;
|
|
60076
|
+
const error5 = new CapturedOutput;
|
|
60077
|
+
const memberDependencies = {
|
|
60078
|
+
...dependencies,
|
|
60079
|
+
targetLease: dependencies.targetLease === false ? false : {
|
|
60080
|
+
...dependencies.targetLease ?? {},
|
|
60081
|
+
waitTimeoutMs: options.leaseWaitMs ?? pool.leaseWaitMs,
|
|
60082
|
+
signal: memberSignal
|
|
60083
|
+
},
|
|
60084
|
+
writeRememberedTarget: async () => ({ ok: true, path: "target-pool" })
|
|
60085
|
+
};
|
|
60086
|
+
const exitCode2 = await runCli(targetPoolMemberArguments(argv, member), {
|
|
60087
|
+
input: io.input,
|
|
60088
|
+
output,
|
|
60089
|
+
error: error5,
|
|
60090
|
+
cwd: io.cwd,
|
|
60091
|
+
env: io.env
|
|
60092
|
+
}, memberDependencies, memberSignal);
|
|
60093
|
+
const result = parsedResultEnvelope(output.text());
|
|
60094
|
+
if (result === undefined) {
|
|
60095
|
+
throw new Error(`Pool member ${member.id} did not return one JSON result envelope.`);
|
|
60096
|
+
}
|
|
60097
|
+
return { exitCode: exitCode2, result };
|
|
60098
|
+
});
|
|
60099
|
+
}
|
|
57622
60100
|
function requiresTargetLease(options) {
|
|
57623
60101
|
if (options.dryRun)
|
|
57624
60102
|
return false;
|
|
@@ -57694,7 +60172,7 @@ function capabilities(options, values2, io, target, fallbackFormat) {
|
|
|
57694
60172
|
function failureResult(command, problems, dependencies) {
|
|
57695
60173
|
const clock = dependencies.clock ?? (() => new Date);
|
|
57696
60174
|
const now = clock();
|
|
57697
|
-
const commandId = (dependencies.idFactory ??
|
|
60175
|
+
const commandId = (dependencies.idFactory ?? randomUUID17)();
|
|
57698
60176
|
return {
|
|
57699
60177
|
schemaVersion: SCHEMA_VERSION,
|
|
57700
60178
|
command,
|
|
@@ -57704,9 +60182,9 @@ function failureResult(command, problems, dependencies) {
|
|
|
57704
60182
|
finishedAt: now.toISOString(),
|
|
57705
60183
|
durationMs: 0,
|
|
57706
60184
|
data: null,
|
|
57707
|
-
problems: problems.map((
|
|
57708
|
-
...
|
|
57709
|
-
correlation: { ...
|
|
60185
|
+
problems: problems.map((problem4) => ({
|
|
60186
|
+
...problem4,
|
|
60187
|
+
correlation: { ...problem4.correlation, commandId }
|
|
57710
60188
|
}))
|
|
57711
60189
|
};
|
|
57712
60190
|
}
|
|
@@ -57880,6 +60358,8 @@ async function runInteractiveSession(io, dependencies, terminal2, signal) {
|
|
|
57880
60358
|
pair: ["pair"],
|
|
57881
60359
|
"run-help": ["help", "run"],
|
|
57882
60360
|
sessions: ["sessions"],
|
|
60361
|
+
"test-firebase": ["test", "firebase", "devices"],
|
|
60362
|
+
"test-gradle": ["test", "gradle"],
|
|
57883
60363
|
version: ["version"]
|
|
57884
60364
|
};
|
|
57885
60365
|
clearInteractiveScreen(io.error, terminal2);
|
|
@@ -57909,10 +60389,10 @@ async function selectDevice(execution2, io, terminal2, remembered, signal) {
|
|
|
57909
60389
|
}
|
|
57910
60390
|
const selectable = data.targets.filter((target) => target.transports.some(({ stable, state }) => stable && state === "device"));
|
|
57911
60391
|
if (selectable.length === 0) {
|
|
57912
|
-
const
|
|
60392
|
+
const problem4 = inputProblem(ProblemCode.NoSelectableTarget, "No ready Android target can be selected.", "Connect or recover a target until ADB reports the device state.", execution2.result.commandId);
|
|
57913
60393
|
return {
|
|
57914
60394
|
exitCode: 20 /* Target */,
|
|
57915
|
-
result: { ...execution2.result, ok: false, problems: [...execution2.result.problems,
|
|
60395
|
+
result: { ...execution2.result, ok: false, problems: [...execution2.result.problems, problem4] }
|
|
57916
60396
|
};
|
|
57917
60397
|
}
|
|
57918
60398
|
const selection2 = await selectOne({
|
|
@@ -58014,10 +60494,14 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58014
60494
|
});
|
|
58015
60495
|
return 0 /* Success */;
|
|
58016
60496
|
}
|
|
60497
|
+
if (options.command === "completion") {
|
|
60498
|
+
io.output.write(generateCompletion(options.completionShell ?? "bash"));
|
|
60499
|
+
return 0 /* Success */;
|
|
60500
|
+
}
|
|
58017
60501
|
if (options.command === "mcp") {
|
|
58018
60502
|
const explicitRoot = io.env.ADB_READY_MCP_PROJECT_ROOT?.trim();
|
|
58019
60503
|
await runMcpStdio({
|
|
58020
|
-
cwd: explicitRoot === undefined || explicitRoot === "" ? io.cwd :
|
|
60504
|
+
cwd: explicitRoot === undefined || explicitRoot === "" ? io.cwd : path26.resolve(explicitRoot),
|
|
58021
60505
|
env: io.env,
|
|
58022
60506
|
dependencies,
|
|
58023
60507
|
version: VERSION,
|
|
@@ -58060,6 +60544,119 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58060
60544
|
});
|
|
58061
60545
|
return execution3.exitCode;
|
|
58062
60546
|
}
|
|
60547
|
+
if (options.command === "test") {
|
|
60548
|
+
if (options.testKind === "firebase" && options.firebaseAction !== undefined) {
|
|
60549
|
+
const firebaseAction = options.firebaseAction;
|
|
60550
|
+
const executeFirebase = async (devices, memberSignal, resultsDirSuffix) => await runFirebaseTestLab({
|
|
60551
|
+
cwd: io.cwd,
|
|
60552
|
+
action: firebaseAction,
|
|
60553
|
+
...options.gcloudPath === undefined ? {} : { gcloudPath: options.gcloudPath },
|
|
60554
|
+
...options.cloudProject === undefined ? {} : { project: options.cloudProject },
|
|
60555
|
+
...options.firebaseApp === undefined ? {} : { app: options.firebaseApp },
|
|
60556
|
+
...options.firebaseTest === undefined ? {} : { test: options.firebaseTest },
|
|
60557
|
+
...devices === undefined ? {} : { devices },
|
|
60558
|
+
...options.firebaseMatrixId === undefined ? {} : { matrixId: options.firebaseMatrixId },
|
|
60559
|
+
...options.firebaseResultsBucket === undefined ? {} : { resultsBucket: options.firebaseResultsBucket },
|
|
60560
|
+
...options.firebaseResultsDir === undefined ? {} : {
|
|
60561
|
+
resultsDir: resultsDirSuffix === undefined ? options.firebaseResultsDir : `${options.firebaseResultsDir}/${resultsDirSuffix}`
|
|
60562
|
+
},
|
|
60563
|
+
...options.firebaseTestTimeout === undefined ? {} : { testTimeout: options.firebaseTestTimeout },
|
|
60564
|
+
...options.runTimeoutMs === undefined ? {} : { observationTimeoutMs: options.runTimeoutMs },
|
|
60565
|
+
...options.firebaseAllowDeprecated === true ? { allowDeprecated: true } : {},
|
|
60566
|
+
...options.firebaseAllowReducedStability === true ? { allowReducedStability: true } : {},
|
|
60567
|
+
...options.firebaseAllowLowCapacity === true ? { allowLowCapacity: true } : {},
|
|
60568
|
+
...options.dryRun ? { dryRun: true } : {}
|
|
60569
|
+
}, {
|
|
60570
|
+
...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
|
|
60571
|
+
...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
|
|
60572
|
+
...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
|
|
60573
|
+
...dependencies.firebaseFetch === undefined ? {} : { fetch: dependencies.firebaseFetch }
|
|
60574
|
+
}, memberSignal);
|
|
60575
|
+
let execution4;
|
|
60576
|
+
if (options.poolName === undefined) {
|
|
60577
|
+
execution4 = await executeFirebase(options.firebaseDevices, signal);
|
|
60578
|
+
} else {
|
|
60579
|
+
const loadedPool = await (dependencies.loadConfig ?? loadConfig)({
|
|
60580
|
+
cwd: io.cwd,
|
|
60581
|
+
env: io.env,
|
|
60582
|
+
...options.configPath === undefined ? {} : { projectConfigPath: options.configPath },
|
|
60583
|
+
explicitProjectConfig: options.configPath !== undefined,
|
|
60584
|
+
...options.profileName === undefined ? {} : { profileName: options.profileName },
|
|
60585
|
+
cli: cliConfig(options)
|
|
60586
|
+
});
|
|
60587
|
+
if (!loadedPool.ok) {
|
|
60588
|
+
const failed2 = failureResult("test firebase pool", configProblems(loadedPool.errors), dependencies);
|
|
60589
|
+
renderFailure(failed2, options.format, io);
|
|
60590
|
+
return 2 /* InvalidInput */;
|
|
60591
|
+
}
|
|
60592
|
+
const pool = loadedPool.config.values.targetPools?.[options.poolName];
|
|
60593
|
+
if (pool === undefined) {
|
|
60594
|
+
const failed2 = failureResult("test firebase pool", [
|
|
60595
|
+
targetPoolProblem("TARGET_POOL_NOT_FOUND", `Target pool ${options.poolName} was not found.`, "Define the named pool under targets.pools in the project configuration.")
|
|
60596
|
+
], dependencies);
|
|
60597
|
+
renderFailure(failed2, options.format, io);
|
|
60598
|
+
return 2 /* InvalidInput */;
|
|
60599
|
+
}
|
|
60600
|
+
if (pool.members.some((member) => member.kind !== "firebase")) {
|
|
60601
|
+
const failed2 = failureResult("test firebase pool", [
|
|
60602
|
+
targetPoolProblem("TARGET_POOL_KIND_MISMATCH", `Target pool ${options.poolName} contains a non-Firebase member.`, "Firebase pool runs require every member to declare kind firebase.")
|
|
60603
|
+
], dependencies);
|
|
60604
|
+
renderFailure(failed2, options.format, io);
|
|
60605
|
+
return 2 /* InvalidInput */;
|
|
60606
|
+
}
|
|
60607
|
+
execution4 = await runTargetPool({
|
|
60608
|
+
name: options.poolName,
|
|
60609
|
+
command: "test firebase pool",
|
|
60610
|
+
pool,
|
|
60611
|
+
coordination: "provider-managed",
|
|
60612
|
+
cancelRunningOnFailFast: false,
|
|
60613
|
+
...options.maxConcurrency === undefined ? {} : { maxConcurrency: options.maxConcurrency },
|
|
60614
|
+
...options.failFast === undefined ? {} : { failFast: options.failFast },
|
|
60615
|
+
...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
|
|
60616
|
+
...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
|
|
60617
|
+
...signal === undefined ? {} : { signal },
|
|
60618
|
+
...targetPoolProgress(options, io)
|
|
60619
|
+
}, async (member, memberSignal) => {
|
|
60620
|
+
const firebase = member;
|
|
60621
|
+
return await executeFirebase([
|
|
60622
|
+
{
|
|
60623
|
+
model: firebase.model,
|
|
60624
|
+
version: firebase.version,
|
|
60625
|
+
locale: firebase.locale,
|
|
60626
|
+
orientation: firebase.orientation
|
|
60627
|
+
}
|
|
60628
|
+
], memberSignal, firebase.id);
|
|
60629
|
+
});
|
|
60630
|
+
}
|
|
60631
|
+
renderResult(execution4.result, {
|
|
60632
|
+
format: options.format,
|
|
60633
|
+
capabilities: capabilities(options, cliConfig(options), io, "output", options.format),
|
|
60634
|
+
sink: options.format === "human" ? io.error : io.output,
|
|
60635
|
+
verbose: options.verbose
|
|
60636
|
+
});
|
|
60637
|
+
return execution4.exitCode;
|
|
60638
|
+
}
|
|
60639
|
+
const execution3 = await runGradleManagedTest({
|
|
60640
|
+
cwd: io.cwd,
|
|
60641
|
+
...options.gradleTask === undefined ? {} : { task: options.gradleTask },
|
|
60642
|
+
...options.gradlePath === undefined ? {} : { gradlePath: options.gradlePath },
|
|
60643
|
+
...options.gradleShards === undefined ? {} : { shards: options.gradleShards },
|
|
60644
|
+
...options.gradleSoftwareRendering === true ? { softwareRendering: true } : {},
|
|
60645
|
+
...options.runTimeoutMs === undefined ? {} : { timeoutMs: options.runTimeoutMs },
|
|
60646
|
+
...options.dryRun ? { dryRun: true } : {}
|
|
60647
|
+
}, {
|
|
60648
|
+
...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
|
|
60649
|
+
...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
|
|
60650
|
+
...dependencies.runner === undefined ? {} : { runner: dependencies.runner }
|
|
60651
|
+
}, signal);
|
|
60652
|
+
renderResult(execution3.result, {
|
|
60653
|
+
format: options.format,
|
|
60654
|
+
capabilities: capabilities(options, cliConfig(options), io, "output", options.format),
|
|
60655
|
+
sink: options.format === "human" ? io.error : io.output,
|
|
60656
|
+
verbose: options.verbose
|
|
60657
|
+
});
|
|
60658
|
+
return execution3.exitCode;
|
|
60659
|
+
}
|
|
58063
60660
|
if (options.command === "context" || options.command === "sessions" || options.command === "problems") {
|
|
58064
60661
|
const storeOptions = dependencies.sessionStore === false ? { env: io.env, projectRoot: io.cwd, allProjects: options.allProjects } : {
|
|
58065
60662
|
...dependencies.sessionStore ?? { env: io.env },
|
|
@@ -58107,6 +60704,31 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58107
60704
|
return execution3.exitCode;
|
|
58108
60705
|
}
|
|
58109
60706
|
const values2 = loaded.config.values;
|
|
60707
|
+
if (options.command === "run" && options.poolName !== undefined) {
|
|
60708
|
+
const pool = values2.targetPools?.[options.poolName];
|
|
60709
|
+
if (pool === undefined) {
|
|
60710
|
+
const failed2 = failureResult("run pool", [
|
|
60711
|
+
targetPoolProblem("TARGET_POOL_NOT_FOUND", `Target pool ${options.poolName} was not found.`, "Define the named pool under targets.pools in the project configuration.")
|
|
60712
|
+
], dependencies);
|
|
60713
|
+
renderFailure(failed2, options.format, io);
|
|
60714
|
+
return 2 /* InvalidInput */;
|
|
60715
|
+
}
|
|
60716
|
+
if (pool.members.some((member) => member.kind === "firebase")) {
|
|
60717
|
+
const failed2 = failureResult("run pool", [
|
|
60718
|
+
targetPoolProblem("TARGET_POOL_KIND_MISMATCH", `Target pool ${options.poolName} contains a Firebase member.`, "Local run pools accept adb, avd, and remote-adb members only. Use test firebase with a Firebase-only pool.")
|
|
60719
|
+
], dependencies);
|
|
60720
|
+
renderFailure(failed2, options.format, io);
|
|
60721
|
+
return 2 /* InvalidInput */;
|
|
60722
|
+
}
|
|
60723
|
+
const execution3 = await runLocalTargetPool(argv, options, pool, io, dependencies, signal);
|
|
60724
|
+
renderResult(execution3.result, {
|
|
60725
|
+
format: options.format,
|
|
60726
|
+
capabilities: capabilities(options, values2, io, "output", options.format),
|
|
60727
|
+
sink: options.format === "human" ? io.error : io.output,
|
|
60728
|
+
verbose: options.verbose
|
|
60729
|
+
});
|
|
60730
|
+
return execution3.exitCode;
|
|
60731
|
+
}
|
|
58110
60732
|
const errorCapabilities = capabilities(options, values2, io, "error", options.format);
|
|
58111
60733
|
const outputCapabilities = capabilities(options, values2, io, "output", options.format);
|
|
58112
60734
|
let lastTarget;
|
|
@@ -58118,7 +60740,7 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58118
60740
|
...values2.adbPort === undefined ? {} : { adbPort: values2.adbPort }
|
|
58119
60741
|
});
|
|
58120
60742
|
if (!state.ok) {
|
|
58121
|
-
const
|
|
60743
|
+
const problem4 = {
|
|
58122
60744
|
code: state.code,
|
|
58123
60745
|
category: options.remembered ? "environment.state" : "state.persistence",
|
|
58124
60746
|
severity: options.remembered ? "error" : "warning",
|
|
@@ -58130,11 +60752,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58130
60752
|
correlation: { commandId: "state" }
|
|
58131
60753
|
};
|
|
58132
60754
|
if (options.remembered) {
|
|
58133
|
-
const failure5 = failureResult(options.command, [
|
|
60755
|
+
const failure5 = failureResult(options.command, [problem4], dependencies);
|
|
58134
60756
|
renderFailure(failure5, options.format, io);
|
|
58135
60757
|
return 10 /* Environment */;
|
|
58136
60758
|
}
|
|
58137
|
-
stateWarning =
|
|
60759
|
+
stateWarning = problem4;
|
|
58138
60760
|
}
|
|
58139
60761
|
if (state.ok) {
|
|
58140
60762
|
lastTarget = rememberedTarget(state.document, {
|
|
@@ -58162,10 +60784,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58162
60784
|
};
|
|
58163
60785
|
let androidCapabilities;
|
|
58164
60786
|
let preparedAvd;
|
|
60787
|
+
let avdResourceLease;
|
|
58165
60788
|
let autonomousEvidence;
|
|
58166
60789
|
let autonomousAdb;
|
|
58167
60790
|
const autonomousRun = options.command === "run" && (options.avdName !== undefined || options.deployArtifact === true);
|
|
58168
|
-
const runPreparationCommandId = autonomousRun ? (dependencies.idFactory ??
|
|
60791
|
+
const runPreparationCommandId = autonomousRun ? (dependencies.idFactory ?? randomUUID17)() : "run-preparation";
|
|
58169
60792
|
const finishAutonomousFailure = async (problems, requestedExitCode) => {
|
|
58170
60793
|
let exitCode2 = requestedExitCode;
|
|
58171
60794
|
if (preparedAvd !== undefined) {
|
|
@@ -58178,6 +60801,8 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58178
60801
|
}
|
|
58179
60802
|
preparedAvd = undefined;
|
|
58180
60803
|
}
|
|
60804
|
+
await avdResourceLease?.release();
|
|
60805
|
+
avdResourceLease = undefined;
|
|
58181
60806
|
const failed2 = {
|
|
58182
60807
|
result: { ...failureResult("run", problems, dependencies), data: null },
|
|
58183
60808
|
exitCode: exitCode2
|
|
@@ -58226,6 +60851,19 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58226
60851
|
});
|
|
58227
60852
|
}
|
|
58228
60853
|
if (options.avdName !== undefined && !options.dryRun) {
|
|
60854
|
+
if (dependencies.targetLease !== false) {
|
|
60855
|
+
const acquired = await acquireTargetLease({
|
|
60856
|
+
targetIdentity: `avd:${options.avdName}`,
|
|
60857
|
+
projectRoot: io.cwd,
|
|
60858
|
+
purpose: `run avd ${options.avdName}`
|
|
60859
|
+
}, { env: io.env, ...dependencies.targetLease ?? {} });
|
|
60860
|
+
if (!acquired.ok) {
|
|
60861
|
+
return await finishAutonomousFailure([
|
|
60862
|
+
targetPoolProblem(acquired.code, acquired.message, acquired.code === "TARGET_LEASE_CANCELLED" ? "The queued AVD was not started or changed." : "Wait for the owning workflow to finish or choose another AVD.", "target-pool", true)
|
|
60863
|
+
], acquired.code === "TARGET_LEASE_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */);
|
|
60864
|
+
}
|
|
60865
|
+
avdResourceLease = acquired.lease;
|
|
60866
|
+
}
|
|
58229
60867
|
const emulator = capability(androidCapabilities, "emulator");
|
|
58230
60868
|
if (emulator.status !== "supported" || emulator.invocation === undefined) {
|
|
58231
60869
|
return await finishAutonomousFailure([
|
|
@@ -58256,16 +60894,27 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58256
60894
|
return await finishAutonomousFailure([autonomousProblem(preparation.failure, "target.avd")], preparation.failure.code === "AVD_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */);
|
|
58257
60895
|
}
|
|
58258
60896
|
const avd = preparation.avd;
|
|
58259
|
-
|
|
60897
|
+
const releaseAvd = avd.release.bind(avd);
|
|
60898
|
+
preparedAvd = {
|
|
60899
|
+
...avd,
|
|
60900
|
+
release: async (releaseSignal) => {
|
|
60901
|
+
try {
|
|
60902
|
+
return await releaseAvd(releaseSignal);
|
|
60903
|
+
} finally {
|
|
60904
|
+
await avdResourceLease?.release();
|
|
60905
|
+
avdResourceLease = undefined;
|
|
60906
|
+
}
|
|
60907
|
+
}
|
|
60908
|
+
};
|
|
58260
60909
|
autonomousEvidence = {
|
|
58261
60910
|
avd: {
|
|
58262
|
-
name:
|
|
58263
|
-
serial:
|
|
58264
|
-
ownership:
|
|
58265
|
-
readiness:
|
|
60911
|
+
name: preparedAvd.name,
|
|
60912
|
+
serial: preparedAvd.serial,
|
|
60913
|
+
ownership: preparedAvd.ownership,
|
|
60914
|
+
readiness: preparedAvd.readiness
|
|
58266
60915
|
}
|
|
58267
60916
|
};
|
|
58268
|
-
config4.targetSelector =
|
|
60917
|
+
config4.targetSelector = preparedAvd.serial;
|
|
58269
60918
|
delete config4.targetTransportId;
|
|
58270
60919
|
delete config4.rememberedSerial;
|
|
58271
60920
|
delete config4.rememberedHardwareSerial;
|
|
@@ -58438,11 +61087,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58438
61087
|
if (!acquired.ok) {
|
|
58439
61088
|
const failure5 = failureResult(options.command, [
|
|
58440
61089
|
{
|
|
58441
|
-
code: acquired.code === "TARGET_BUSY" ? ProblemCode.TargetBusy : ProblemCode.TargetLeaseUnavailable,
|
|
61090
|
+
code: acquired.code === "TARGET_BUSY" ? ProblemCode.TargetBusy : acquired.code === "TARGET_LEASE_CANCELLED" ? ProblemCode.OperationInterrupted : ProblemCode.TargetLeaseUnavailable,
|
|
58442
61091
|
category: "target.lease",
|
|
58443
61092
|
severity: "error",
|
|
58444
61093
|
summary: acquired.message,
|
|
58445
|
-
detail: acquired.code === "TARGET_BUSY" ? "Wait for the owning workflow to finish, or select another Android target. Expired leases are recovered automatically." : "Check the per-user state directory permissions, then retry.",
|
|
61094
|
+
detail: acquired.code === "TARGET_BUSY" ? "Wait for the owning workflow to finish, or select another Android target. Expired leases are recovered automatically." : acquired.code === "TARGET_LEASE_CANCELLED" ? "The queued target was not mutated. Re-run the pool when the target is available." : "Check the per-user state directory permissions, then retry.",
|
|
58446
61095
|
retryable: true,
|
|
58447
61096
|
evidence: acquired.owner === undefined ? [] : [
|
|
58448
61097
|
{ source: "target.lease", field: "purpose", value: acquired.owner.purpose },
|
|
@@ -58461,13 +61110,14 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
|
|
|
58461
61110
|
correlation: { commandId: "target-lease", targetId: preparedSelection.target.id }
|
|
58462
61111
|
}
|
|
58463
61112
|
], dependencies);
|
|
61113
|
+
const leaseExitCode = acquired.code === "TARGET_LEASE_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */;
|
|
58464
61114
|
if (autonomousRun) {
|
|
58465
|
-
return await finishAutonomousFailure([...failure5.problems],
|
|
61115
|
+
return await finishAutonomousFailure([...failure5.problems], leaseExitCode);
|
|
58466
61116
|
}
|
|
58467
61117
|
await preparedAvd?.release(signal);
|
|
58468
61118
|
preparedAvd = undefined;
|
|
58469
61119
|
renderFailure(failure5, options.format, io);
|
|
58470
|
-
return
|
|
61120
|
+
return leaseExitCode;
|
|
58471
61121
|
}
|
|
58472
61122
|
targetLease = acquired.lease;
|
|
58473
61123
|
}
|
|
@@ -58939,7 +61589,7 @@ async function runCli(argv, io, dependencies = {}, signal) {
|
|
|
58939
61589
|
return await runCliInternal(argv, io, dependencies, signal);
|
|
58940
61590
|
} catch (caught) {
|
|
58941
61591
|
const message = redactText(caught instanceof Error ? caught.message : String(caught)).value;
|
|
58942
|
-
const
|
|
61592
|
+
const problem4 = {
|
|
58943
61593
|
code: "INTERNAL_ERROR",
|
|
58944
61594
|
category: "internal.unexpected",
|
|
58945
61595
|
severity: "error",
|
|
@@ -58950,7 +61600,7 @@ async function runCli(argv, io, dependencies = {}, signal) {
|
|
|
58950
61600
|
actions: [],
|
|
58951
61601
|
correlation: { commandId: "cli" }
|
|
58952
61602
|
};
|
|
58953
|
-
renderFailure(failureResult("cli", [
|
|
61603
|
+
renderFailure(failureResult("cli", [problem4], dependencies), inferredFormat(argv), io);
|
|
58954
61604
|
return 70 /* Internal */;
|
|
58955
61605
|
}
|
|
58956
61606
|
}
|
|
@@ -58978,4 +61628,4 @@ try {
|
|
|
58978
61628
|
process16.removeListener("SIGTERM", abort);
|
|
58979
61629
|
}
|
|
58980
61630
|
|
|
58981
|
-
//# debugId=
|
|
61631
|
+
//# debugId=2AD0AEC355B9989464756E2164756E21
|