adb-ready 0.6.0 → 0.7.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/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 randomUUID14 } from "node:crypto";
3302
- import path24 from "node:path";
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.6.0",
3307
+ version: "0.7.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",
@@ -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
- child.kill(killSignal);
37593
+ signalOwnedProcess(killSignal);
37573
37594
  if (killSignal !== "SIGKILL" && escalationTimer === undefined) {
37574
37595
  escalationTimer = setTimeout(() => {
37575
- if (child.exitCode === null && child.signalCode === null) {
37596
+ if (!closed) {
37576
37597
  killEscalated = true;
37577
- child.kill("SIGKILL");
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
- return await this.#observe("version", "Checking ADB version", ["version"], parseAdbVersion, signal, { useServerArguments: false });
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 acquireTargetLease(request, options = {}) {
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 MAX_RESPONSE_BYTES = 16 * 1024;
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 > MAX_RESPONSE_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 wait2 = dependencies.sleep ?? (async (milliseconds) => {
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 wait2(150, signal ?? new AbortController().signal))
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 randomUUID13 } from "node:crypto";
53346
- import { mkdir as mkdir8, readFile as readFile14, rename as rename8, rm as rm9, writeFile as writeFile8 } from "node:fs/promises";
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 path23 from "node:path";
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" ? path23.win32 : path23.posix;
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 = path23.dirname(statePath);
53453
- const temporary = path23.join(directory, `.state-${String(process14.pid)}-${randomUUID13()}.tmp`);
55103
+ const directory = path25.dirname(statePath);
55104
+ const temporary = path25.join(directory, `.state-${String(process14.pid)}-${randomUUID16()}.tmp`);
53454
55105
  try {
53455
- await mkdir8(directory, { recursive: true, mode: 448 });
53456
- await writeFile8(temporary, `${JSON.stringify(document2, null, 2)}
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 rename8(temporary, statePath);
55113
+ await rename10(temporary, statePath);
53463
55114
  return { ok: true, path: statePath };
53464
55115
  } catch {
53465
- await rm9(temporary, { force: true }).catch(() => {
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(problem2, capabilities, verbose) {
56671
+ function problemLines(problem4, capabilities, verbose) {
54999
56672
  const glyphs = symbols(capabilities);
55000
- const marker = problem2.severity === "error" && problem2.code !== ProblemCode.OperationInterrupted ? style.failure(glyphs.failure, capabilities) : style.warning(glyphs.warning, capabilities);
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(problem2.summary), capabilities)}`,
55003
- ` ${clean(problem2.detail)}`
56675
+ `${marker} ${style.strong(clean(problem4.summary), capabilities)}`,
56676
+ ` ${clean(problem4.detail)}`
55004
56677
  ];
55005
- const action = problem2.actions[0];
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 problem2.evidence) {
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((problem2, index) => {
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(problem2.code), capabilities)} · ${clean(problem2.summary)}`, ` ${clean(problem2.detail)}`);
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 problem2 of result.problems) {
55317
- lines.push(...problemLines(problem2, capabilities, options.verbose ?? false));
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((problem2) => {
55625
- sink.write(`problem=${clean(problem2.code)}:${clean(problem2.summary)}
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 problem2 of result.problems) {
55744
- sink.write(`problem=${clean(problem2.code)}:${clean(problem2.summary)}
57565
+ for (const problem4 of result.problems) {
57566
+ sink.write(`problem=${clean(problem4.code)}:${clean(problem4.summary)}
55745
57567
  `);
55746
57568
  }
55747
57569
  }
@@ -55933,6 +57755,7 @@ var COMMANDS = new Set([
55933
57755
  "problems",
55934
57756
  "run",
55935
57757
  "sessions",
57758
+ "test",
55936
57759
  "ui",
55937
57760
  "version"
55938
57761
  ]);
@@ -56008,6 +57831,12 @@ var BOOLEAN_OPTIONS = new Set([
56008
57831
  "--all",
56009
57832
  "--all-projects",
56010
57833
  "--interactive-only",
57834
+ "--software-rendering",
57835
+ "--allow-deprecated",
57836
+ "--allow-reduced-stability",
57837
+ "--allow-low-capacity",
57838
+ "--fail-fast",
57839
+ "--no-fail-fast",
56011
57840
  "--submit",
56012
57841
  "--secret-stdin"
56013
57842
  ]);
@@ -56079,6 +57908,10 @@ function parseArguments(argv) {
56079
57908
  let pairingCodeStdin = false;
56080
57909
  let remembered = false;
56081
57910
  let dryRun = false;
57911
+ let poolName;
57912
+ let maxConcurrency;
57913
+ let failFast;
57914
+ let leaseWaitMs;
56082
57915
  let allProjects = false;
56083
57916
  let portDirection;
56084
57917
  let portAction;
@@ -56093,6 +57926,24 @@ function parseArguments(argv) {
56093
57926
  let customCommand;
56094
57927
  let runCommand;
56095
57928
  let runTimeoutMs;
57929
+ let testKind;
57930
+ let gradleTask;
57931
+ let gradlePath;
57932
+ let gradleShards;
57933
+ let gradleSoftwareRendering = false;
57934
+ let firebaseAction;
57935
+ let firebaseMatrixId;
57936
+ const firebaseDevices = [];
57937
+ let gcloudPath;
57938
+ let cloudProject;
57939
+ let firebaseApp;
57940
+ let firebaseTest;
57941
+ let firebaseResultsBucket;
57942
+ let firebaseResultsDir;
57943
+ let firebaseTestTimeout;
57944
+ let firebaseAllowDeprecated = false;
57945
+ let firebaseAllowReducedStability = false;
57946
+ let firebaseAllowLowCapacity = false;
56096
57947
  let avdName;
56097
57948
  let deployArtifact = false;
56098
57949
  const runArtifactPaths = [];
@@ -56183,7 +58034,7 @@ function parseArguments(argv) {
56183
58034
  }
56184
58035
  if (command === "help" && COMMANDS.has(argument)) {
56185
58036
  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") {
58037
+ 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 === "test" || candidate === "ui" || candidate === "version") {
56187
58038
  helpTarget = candidate;
56188
58039
  continue;
56189
58040
  }
@@ -56196,6 +58047,24 @@ function parseArguments(argv) {
56196
58047
  agentSetupSeen = true;
56197
58048
  continue;
56198
58049
  }
58050
+ if (command === "test") {
58051
+ if (testKind === undefined && (argument === "firebase" || argument === "gradle")) {
58052
+ testKind = argument;
58053
+ continue;
58054
+ }
58055
+ if (testKind === "gradle" && gradleTask === undefined) {
58056
+ gradleTask = argument;
58057
+ continue;
58058
+ }
58059
+ if (testKind === "firebase" && firebaseAction === undefined && (argument === "cancel" || argument === "devices" || argument === "instrumentation" || argument === "robo")) {
58060
+ firebaseAction = argument;
58061
+ continue;
58062
+ }
58063
+ if (testKind === "firebase" && firebaseAction === "cancel" && firebaseMatrixId === undefined) {
58064
+ firebaseMatrixId = argument;
58065
+ continue;
58066
+ }
58067
+ }
56199
58068
  if (command === "agent" && agentSetupSeen && agentClient === undefined) {
56200
58069
  if (!AGENT_CLIENTS.has(argument)) {
56201
58070
  return failure4("CLI_INVALID_VALUE", `Invalid agent client: ${argument}. Expected codex, claude-code, cursor, vscode, windsurf, or generic.`);
@@ -56373,6 +58242,35 @@ function parseArguments(argv) {
56373
58242
  remembered = true;
56374
58243
  } else if (option === "--dry-run") {
56375
58244
  dryRun = true;
58245
+ } else if (option === "--pool") {
58246
+ const value = readValue();
58247
+ if (typeof value !== "string")
58248
+ return value;
58249
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/u.test(value)) {
58250
+ return failure4("CLI_INVALID_VALUE", `Invalid target pool name: ${value}.`, option);
58251
+ }
58252
+ poolName = value;
58253
+ } else if (option === "--max-concurrency") {
58254
+ const value = readValue();
58255
+ if (typeof value !== "string")
58256
+ return value;
58257
+ const parsed = Number(value);
58258
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(parsed) || parsed < 1 || parsed > 32) {
58259
+ return failure4("CLI_INVALID_VALUE", "--max-concurrency must be an integer from 1 to 32.", option);
58260
+ }
58261
+ maxConcurrency = parsed;
58262
+ } else if (option === "--fail-fast") {
58263
+ failFast = true;
58264
+ } else if (option === "--no-fail-fast") {
58265
+ failFast = false;
58266
+ } else if (option === "--lease-wait") {
58267
+ const value = readValue();
58268
+ if (typeof value !== "string")
58269
+ return value;
58270
+ leaseWaitMs = parseDuration(value);
58271
+ if (leaseWaitMs === undefined || leaseWaitMs > 3600000) {
58272
+ return failure4("CLI_INVALID_VALUE", "--lease-wait must be a positive duration up to 1h.", option);
58273
+ }
56376
58274
  } else if (option === "--deploy") {
56377
58275
  deployArtifact = true;
56378
58276
  } else if (option === "--all-projects") {
@@ -56692,6 +58590,96 @@ function parseArguments(argv) {
56692
58590
  if (runTimeoutMs === undefined) {
56693
58591
  return failure4("CLI_INVALID_VALUE", `Invalid run timeout: ${value}. Use a positive duration such as 30s or 10m.`, option);
56694
58592
  }
58593
+ } else if (option === "--gradle") {
58594
+ const value = readValue();
58595
+ if (typeof value !== "string")
58596
+ return value;
58597
+ if (value.trim() !== value || value.length === 0 || !printableValue(value)) {
58598
+ return failure4("CLI_INVALID_VALUE", "--gradle must be a non-empty printable path.", option);
58599
+ }
58600
+ gradlePath = value;
58601
+ } else if (option === "--shards") {
58602
+ const value = readValue();
58603
+ if (typeof value !== "string")
58604
+ return value;
58605
+ const parsed = Number(value);
58606
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(parsed) || parsed < 1 || parsed > 256) {
58607
+ return failure4("CLI_INVALID_VALUE", "--shards must be an integer from 1 to 256.", option);
58608
+ }
58609
+ gradleShards = parsed;
58610
+ } else if (option === "--software-rendering") {
58611
+ gradleSoftwareRendering = true;
58612
+ } else if (option === "--allow-deprecated") {
58613
+ firebaseAllowDeprecated = true;
58614
+ } else if (option === "--allow-reduced-stability") {
58615
+ firebaseAllowReducedStability = true;
58616
+ } else if (option === "--allow-low-capacity") {
58617
+ firebaseAllowLowCapacity = true;
58618
+ } else if (option === "--gcloud") {
58619
+ const value = readValue();
58620
+ if (typeof value !== "string")
58621
+ return value;
58622
+ if (value.trim() !== value || value.length === 0 || !printableValue(value)) {
58623
+ return failure4("CLI_INVALID_VALUE", "--gcloud must be a non-empty printable path.", option);
58624
+ }
58625
+ gcloudPath = value;
58626
+ } else if (option === "--project") {
58627
+ const value = readValue();
58628
+ if (typeof value !== "string")
58629
+ return value;
58630
+ if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/u.test(value)) {
58631
+ return failure4("CLI_INVALID_VALUE", "--project must be an explicit Google Cloud project ID.", option);
58632
+ }
58633
+ cloudProject = value;
58634
+ } else if (option === "--app") {
58635
+ const value = readValue();
58636
+ if (typeof value !== "string")
58637
+ return value;
58638
+ 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("?")) {
58639
+ return failure4("CLI_INVALID_VALUE", "--app must be a non-empty printable artifact path.", option);
58640
+ }
58641
+ firebaseApp = value;
58642
+ } else if (option === "--test-apk") {
58643
+ const value = readValue();
58644
+ if (typeof value !== "string")
58645
+ return value;
58646
+ 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("?")) {
58647
+ return failure4("CLI_INVALID_VALUE", "--test-apk must be a non-empty printable path.", option);
58648
+ }
58649
+ firebaseTest = value;
58650
+ } else if (option === "--test-device") {
58651
+ const value = readValue();
58652
+ if (typeof value !== "string")
58653
+ return value;
58654
+ const parsed = parseFirebaseDevice(value);
58655
+ if (parsed === undefined) {
58656
+ return failure4("CLI_INVALID_VALUE", "--test-device requires model=ID,version=ID with optional locale and orientation.", option);
58657
+ }
58658
+ firebaseDevices.push(parsed);
58659
+ } else if (option === "--results-bucket") {
58660
+ const value = readValue();
58661
+ if (typeof value !== "string")
58662
+ return value;
58663
+ if (!/^gs:\/\/[a-z0-9][a-z0-9._-]{1,221}[a-z0-9]$/u.test(value)) {
58664
+ return failure4("CLI_INVALID_VALUE", "--results-bucket must be a gs:// bucket URI.", option);
58665
+ }
58666
+ firebaseResultsBucket = value;
58667
+ } else if (option === "--results-dir") {
58668
+ const value = readValue();
58669
+ if (typeof value !== "string")
58670
+ return value;
58671
+ 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)) {
58672
+ return failure4("CLI_INVALID_VALUE", "--results-dir must be a safe unique bucket-relative path.", option);
58673
+ }
58674
+ firebaseResultsDir = value;
58675
+ } else if (option === "--test-timeout") {
58676
+ const value = readValue();
58677
+ if (typeof value !== "string")
58678
+ return value;
58679
+ if (parseDuration(value) === undefined) {
58680
+ return failure4("CLI_INVALID_VALUE", "--test-timeout must be a positive duration such as 5m.", option);
58681
+ }
58682
+ firebaseTestTimeout = value;
56695
58683
  } else if (option === "--avd") {
56696
58684
  const value = readValue();
56697
58685
  if (typeof value !== "string")
@@ -56797,6 +58785,9 @@ function parseArguments(argv) {
56797
58785
  "--adb-port",
56798
58786
  "--config",
56799
58787
  "--profile",
58788
+ "--pool",
58789
+ "--max-concurrency",
58790
+ "--lease-wait",
56800
58791
  "--mcp-profile",
56801
58792
  "--device",
56802
58793
  "--serial",
@@ -56827,6 +58818,16 @@ function parseArguments(argv) {
56827
58818
  "--max-records",
56828
58819
  "--budget",
56829
58820
  "--run-timeout",
58821
+ "--gradle",
58822
+ "--shards",
58823
+ "--gcloud",
58824
+ "--project",
58825
+ "--app",
58826
+ "--test-apk",
58827
+ "--test-device",
58828
+ "--results-bucket",
58829
+ "--results-dir",
58830
+ "--test-timeout",
56830
58831
  "--avd",
56831
58832
  "--artifact",
56832
58833
  "--variant",
@@ -56891,8 +58892,8 @@ function parseArguments(argv) {
56891
58892
  if (remembered && (select || device !== undefined || transportId !== undefined)) {
56892
58893
  return failure4("CLI_USAGE", "--last cannot be combined with another target selector.");
56893
58894
  }
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 ports.", "--dry-run");
58895
+ if (dryRun && command !== "connect" && command !== "dev" && command !== "run" && command !== "app" && command !== "open" && command !== "pair" && command !== "ports" && command !== "init" && command !== "agent" && command !== "test" && command !== "ui") {
58896
+ 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
58897
  }
56897
58898
  if (command === "ports") {
56898
58899
  if (portDirection === undefined) {
@@ -57122,8 +59123,56 @@ function parseArguments(argv) {
57122
59123
  if (command === "run" && runCommand === undefined) {
57123
59124
  return failure4("CLI_USAGE", "run requires a bounded command after --.");
57124
59125
  }
59126
+ const poolCommand = command === "run" || command === "test" && testKind === "firebase" && (firebaseAction === "instrumentation" || firebaseAction === "robo");
59127
+ if (poolName !== undefined && !poolCommand) {
59128
+ return failure4("CLI_USAGE", "--pool can only be used with run or a Firebase instrumentation/Robo workflow.", "--pool");
59129
+ }
59130
+ if (poolName !== undefined && (select || remembered || device !== undefined || transportId !== undefined || avdName !== undefined)) {
59131
+ return failure4("CLI_USAGE", "--pool cannot be combined with another target selector.", "--pool");
59132
+ }
59133
+ if (poolName !== undefined && firebaseDevices.length > 0) {
59134
+ return failure4("CLI_USAGE", "--pool cannot be combined with --test-device.", "--pool");
59135
+ }
59136
+ if ((maxConcurrency !== undefined || failFast !== undefined || leaseWaitMs !== undefined) && poolName === undefined) {
59137
+ return failure4("CLI_USAGE", "--max-concurrency, --fail-fast, --no-fail-fast, and --lease-wait require --pool.");
59138
+ }
59139
+ if (leaseWaitMs !== undefined && command !== "run") {
59140
+ return failure4("CLI_USAGE", "--lease-wait only applies to local run pools.", "--lease-wait");
59141
+ }
57125
59142
  if (runTimeoutMs !== undefined && command !== "run") {
57126
- return failure4("CLI_USAGE", "--run-timeout can only be used with run.", "--run-timeout");
59143
+ if (command !== "test") {
59144
+ return failure4("CLI_USAGE", "--run-timeout can only be used with run or a supported test workflow.", "--run-timeout");
59145
+ }
59146
+ }
59147
+ if (command === "test" && testKind === undefined) {
59148
+ return failure4("CLI_USAGE", "test requires a workflow: adb-ready test gradle [TASK] or adb-ready test firebase ACTION.");
59149
+ }
59150
+ if (testKind !== "gradle" && (gradlePath !== undefined || gradleShards !== undefined || gradleSoftwareRendering)) {
59151
+ return failure4("CLI_USAGE", "--gradle, --shards, and --software-rendering can only be used with test gradle.");
59152
+ }
59153
+ 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;
59154
+ if (firebaseOptions && (command !== "test" || testKind !== "firebase")) {
59155
+ return failure4("CLI_USAGE", "Firebase options can only be used with adb-ready test firebase.");
59156
+ }
59157
+ if (testKind === "firebase") {
59158
+ if (firebaseAction === undefined) {
59159
+ return failure4("CLI_USAGE", "test firebase requires devices, instrumentation, robo, or cancel.");
59160
+ }
59161
+ if (firebaseResultsDir !== undefined && firebaseResultsBucket === undefined) {
59162
+ return failure4("CLI_USAGE", "--results-dir requires --results-bucket.");
59163
+ }
59164
+ if (firebaseAction === "instrumentation" && firebaseTest === undefined) {
59165
+ return failure4("CLI_USAGE", "Firebase instrumentation requires --test-apk PATH.");
59166
+ }
59167
+ if ((firebaseAction === "instrumentation" || firebaseAction === "robo") && (firebaseApp === undefined || firebaseDevices.length === 0 && poolName === undefined)) {
59168
+ return failure4("CLI_USAGE", `Firebase ${firebaseAction} requires --app PATH and at least one --test-device.`);
59169
+ }
59170
+ if (firebaseAction === "cancel" && (firebaseMatrixId === undefined || cloudProject === undefined)) {
59171
+ return failure4("CLI_USAGE", "Firebase cancellation requires MATRIX_ID and --project PROJECT.");
59172
+ }
59173
+ if ((firebaseAction === "cancel" || firebaseAction === "devices") && (firebaseApp !== undefined || firebaseTest !== undefined || firebaseDevices.length > 0 || firebaseResultsBucket !== undefined || firebaseResultsDir !== undefined || firebaseTestTimeout !== undefined)) {
59174
+ return failure4("CLI_USAGE", `Firebase ${firebaseAction} does not accept test artifacts, dimensions, or result storage options.`);
59175
+ }
57127
59176
  }
57128
59177
  const autonomousRunOptions = avdName !== undefined || deployArtifact || runArtifactPaths.length > 0 || runVariant !== undefined || javaPath !== undefined || bundletoolPath !== undefined;
57129
59178
  if (autonomousRunOptions && command !== "run") {
@@ -57204,6 +59253,10 @@ function parseArguments(argv) {
57204
59253
  pairingCodeStdin,
57205
59254
  remembered,
57206
59255
  dryRun,
59256
+ ...poolName === undefined ? {} : { poolName },
59257
+ ...maxConcurrency === undefined ? {} : { maxConcurrency },
59258
+ ...failFast === undefined ? {} : { failFast },
59259
+ ...leaseWaitMs === undefined ? {} : { leaseWaitMs },
57207
59260
  allProjects,
57208
59261
  ...portDirection === undefined ? {} : { portDirection },
57209
59262
  ...portAction === undefined ? {} : { portAction },
@@ -57218,6 +59271,24 @@ function parseArguments(argv) {
57218
59271
  ...customCommand === undefined ? {} : { customCommand },
57219
59272
  ...runCommand === undefined ? {} : { runCommand },
57220
59273
  ...runTimeoutMs === undefined ? {} : { runTimeoutMs },
59274
+ ...testKind === undefined ? {} : { testKind },
59275
+ ...gradleTask === undefined ? {} : { gradleTask },
59276
+ ...gradlePath === undefined ? {} : { gradlePath },
59277
+ ...gradleShards === undefined ? {} : { gradleShards },
59278
+ ...gradleSoftwareRendering ? { gradleSoftwareRendering: true } : {},
59279
+ ...firebaseAction === undefined ? {} : { firebaseAction },
59280
+ ...firebaseMatrixId === undefined ? {} : { firebaseMatrixId },
59281
+ ...firebaseDevices.length === 0 ? {} : { firebaseDevices },
59282
+ ...gcloudPath === undefined ? {} : { gcloudPath },
59283
+ ...cloudProject === undefined ? {} : { cloudProject },
59284
+ ...firebaseApp === undefined ? {} : { firebaseApp },
59285
+ ...firebaseTest === undefined ? {} : { firebaseTest },
59286
+ ...firebaseResultsBucket === undefined ? {} : { firebaseResultsBucket },
59287
+ ...firebaseResultsDir === undefined ? {} : { firebaseResultsDir },
59288
+ ...firebaseTestTimeout === undefined ? {} : { firebaseTestTimeout },
59289
+ ...firebaseAllowDeprecated ? { firebaseAllowDeprecated: true } : {},
59290
+ ...firebaseAllowReducedStability ? { firebaseAllowReducedStability: true } : {},
59291
+ ...firebaseAllowLowCapacity ? { firebaseAllowLowCapacity: true } : {},
57221
59292
  ...avdName === undefined ? {} : { avdName },
57222
59293
  ...deployArtifact ? { deployArtifact: true } : {},
57223
59294
  ...runArtifactPaths.length === 0 ? {} : { runArtifactPaths },
@@ -57304,6 +59375,8 @@ Commands:
57304
59375
  pair [HOST:PORT] Pair using Android's six-digit pairing code
57305
59376
  ports DIRECTION ACTION Manage verified TCP forward/reverse mappings
57306
59377
  sessions [ACTION] Inspect saved development sessions
59378
+ test gradle [TASK] Discover or run a Gradle managed-device test
59379
+ test firebase ACTION Run or manage a Firebase Test Lab matrix
57307
59380
  ui ACTION Perform a bounded, verified Android UI action
57308
59381
  problems [SESSION] Show problems from a saved session
57309
59382
  help [COMMAND] Show help
@@ -57446,6 +59519,10 @@ waits for every readiness assertion, runs one bounded verification command,
57446
59519
  then cleans owned resources. The verification exit code is preserved.
57447
59520
 
57448
59521
  Run options:
59522
+ --pool NAME Fan out through one explicit project target pool
59523
+ --max-concurrency N Override the pool's bounded concurrency (1-32)
59524
+ --[no-]fail-fast Override whether a required failure stops queued work
59525
+ --lease-wait DURATION Bound host-local target queueing (maximum: 1h)
57449
59526
  --avd NAME Reuse or start this exact existing AVD
57450
59527
  --deploy Discover and deploy one compatible Android artifact
57451
59528
  --artifact PATH Deploy this project-local APK, APK Set, or AAB; repeat for splits
@@ -57462,6 +59539,8 @@ Verification processes also receive ANDROID_SERIAL, ADB_READY_TARGET_SERIAL, and
57462
59539
  ADB_READY_VERIFIER_OUTPUT_DIR. Direct Maestro test and Android CLI layout/screen-capture commands
57463
59540
  are pinned to the selected target and their native reports are retained automatically.
57464
59541
  AVDs must already exist. ADB Ready never creates or upgrades an SDK or AVD implicitly.
59542
+ Pool members each retain an isolated session and result. Pool mode never changes the
59543
+ single-target invariant inside a run.
57465
59544
  `,
57466
59545
  doctor: `Usage: adb-ready doctor [options]
57467
59546
 
@@ -57611,6 +59690,50 @@ List filters:
57611
59690
  --since DURATION Keep sessions updated in the final window
57612
59691
  --preset NAME Keep sessions using one development preset
57613
59692
  --limit COUNT Return at most 1-100 sessions
59693
+ `,
59694
+ test: `Usage:
59695
+ adb-ready test gradle
59696
+ adb-ready test gradle TASK [options]
59697
+ adb-ready test firebase devices [options]
59698
+ adb-ready test firebase instrumentation [options]
59699
+ adb-ready test firebase robo [options]
59700
+ adb-ready test firebase cancel MATRIX_ID --project PROJECT [options]
59701
+
59702
+ Discovers declared Gradle Managed Device tasks or hands one exact task to the
59703
+ project's checked-in Gradle Wrapper. Gradle owns provisioning, test execution,
59704
+ sharding, and teardown; ADB Ready never leases or controls its transient target.
59705
+
59706
+ Gradle managed-device options:
59707
+ --gradle PATH Use an explicit Gradle Wrapper or executable
59708
+ --shards COUNT Request 1-256 uniform managed-device shards
59709
+ --software-rendering Request Gradle's SwiftShader server rendering mode
59710
+ --run-timeout DURATION Bound task execution (default: 15m)
59711
+ --dry-run Print the exact task plan without starting Gradle
59712
+
59713
+ Native JUnit XML and HTML reports are retained with bounded Gradle output under
59714
+ .adb-ready/artifacts/. Product assertion failures, infrastructure failures,
59715
+ timeouts, cancellation, and missing tasks remain distinct structured outcomes.
59716
+
59717
+ Firebase Test Lab options:
59718
+ --pool NAME Run one matrix per named Firebase dimension
59719
+ --max-concurrency N Bound simultaneously observed matrices (1-32)
59720
+ --[no-]fail-fast Stop submitting queued dimensions after a required failure
59721
+ --project PROJECT Use one explicit Google Cloud project
59722
+ --gcloud PATH Use an explicit gcloud executable
59723
+ --app PATH App APK or supported app bundle
59724
+ --test-apk PATH Instrumentation test APK
59725
+ --test-device DIMENSIONS Exact model=ID,version=ID[,locale=..,orientation=..]
59726
+ --test-timeout DURATION Provider-side timeout for each execution
59727
+ --run-timeout DURATION Bound local observation (default: 30m)
59728
+ --results-bucket GS_URI Store provider artifacts in an owned GCS bucket
59729
+ --results-dir PATH Unique bucket-relative result directory
59730
+ --allow-deprecated Explicitly accept a deprecated catalog dimension
59731
+ --allow-reduced-stability Explicitly accept a reduced-stability dimension
59732
+ --allow-low-capacity Explicitly accept a low-capacity dimension
59733
+
59734
+ Every run validates dimensions against the live catalog before upload. Stopping
59735
+ local observation leaves the remote matrix running; only the explicit cancel
59736
+ action cancels it.
57614
59737
  `,
57615
59738
  version: `Usage: adb-ready version
57616
59739
 
@@ -57619,6 +59742,92 @@ configuration or ADB. Human output and --version remain one bare version line;
57619
59742
  --format plain, --json, and --format ndjson use the global automation contract.
57620
59743
  `
57621
59744
  };
59745
+
59746
+ class CapturedOutput {
59747
+ isTTY = false;
59748
+ columns = 120;
59749
+ rows = 40;
59750
+ #chunks = [];
59751
+ write(value) {
59752
+ this.#chunks.push(value);
59753
+ }
59754
+ text() {
59755
+ return this.#chunks.join("");
59756
+ }
59757
+ }
59758
+ function parsedResultEnvelope(value) {
59759
+ try {
59760
+ const parsed = JSON.parse(value.trim());
59761
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
59762
+ return;
59763
+ const result = parsed;
59764
+ 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;
59765
+ } catch {
59766
+ return;
59767
+ }
59768
+ }
59769
+ function targetPoolProblem(code, summary2, detail2, commandId = "target-pool", retryable = false) {
59770
+ return {
59771
+ code,
59772
+ category: "automation.pool",
59773
+ severity: "error",
59774
+ summary: summary2,
59775
+ detail: detail2,
59776
+ retryable,
59777
+ evidence: [],
59778
+ actions: [],
59779
+ correlation: { commandId }
59780
+ };
59781
+ }
59782
+ function targetPoolProgress(options, io) {
59783
+ if (options.format !== "human" || options.quiet)
59784
+ return {};
59785
+ return {
59786
+ onMemberStart: (member) => io.error.write(`pool ${member.id} running
59787
+ `),
59788
+ onMemberFinish: (member) => io.error.write(`pool ${member.id} ${member.status} · exit ${String(member.exitCode)}
59789
+ `)
59790
+ };
59791
+ }
59792
+ async function runLocalTargetPool(argv, options, pool, io, dependencies, signal) {
59793
+ return await runTargetPool({
59794
+ name: options.poolName ?? "",
59795
+ pool,
59796
+ ...options.maxConcurrency === undefined ? {} : { maxConcurrency: options.maxConcurrency },
59797
+ ...options.failFast === undefined ? {} : { failFast: options.failFast },
59798
+ ...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
59799
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
59800
+ ...signal === undefined ? {} : { signal },
59801
+ ...targetPoolProgress(options, io)
59802
+ }, async (member, memberSignal) => {
59803
+ if (member.kind === "firebase") {
59804
+ throw new Error("Firebase dimensions cannot execute through a local run pool.");
59805
+ }
59806
+ const output = new CapturedOutput;
59807
+ const error5 = new CapturedOutput;
59808
+ const memberDependencies = {
59809
+ ...dependencies,
59810
+ targetLease: dependencies.targetLease === false ? false : {
59811
+ ...dependencies.targetLease ?? {},
59812
+ waitTimeoutMs: options.leaseWaitMs ?? pool.leaseWaitMs,
59813
+ signal: memberSignal
59814
+ },
59815
+ writeRememberedTarget: async () => ({ ok: true, path: "target-pool" })
59816
+ };
59817
+ const exitCode2 = await runCli(targetPoolMemberArguments(argv, member), {
59818
+ input: io.input,
59819
+ output,
59820
+ error: error5,
59821
+ cwd: io.cwd,
59822
+ env: io.env
59823
+ }, memberDependencies, memberSignal);
59824
+ const result = parsedResultEnvelope(output.text());
59825
+ if (result === undefined) {
59826
+ throw new Error(`Pool member ${member.id} did not return one JSON result envelope.`);
59827
+ }
59828
+ return { exitCode: exitCode2, result };
59829
+ });
59830
+ }
57622
59831
  function requiresTargetLease(options) {
57623
59832
  if (options.dryRun)
57624
59833
  return false;
@@ -57694,7 +59903,7 @@ function capabilities(options, values2, io, target, fallbackFormat) {
57694
59903
  function failureResult(command, problems, dependencies) {
57695
59904
  const clock = dependencies.clock ?? (() => new Date);
57696
59905
  const now = clock();
57697
- const commandId = (dependencies.idFactory ?? randomUUID14)();
59906
+ const commandId = (dependencies.idFactory ?? randomUUID17)();
57698
59907
  return {
57699
59908
  schemaVersion: SCHEMA_VERSION,
57700
59909
  command,
@@ -57704,9 +59913,9 @@ function failureResult(command, problems, dependencies) {
57704
59913
  finishedAt: now.toISOString(),
57705
59914
  durationMs: 0,
57706
59915
  data: null,
57707
- problems: problems.map((problem2) => ({
57708
- ...problem2,
57709
- correlation: { ...problem2.correlation, commandId }
59916
+ problems: problems.map((problem4) => ({
59917
+ ...problem4,
59918
+ correlation: { ...problem4.correlation, commandId }
57710
59919
  }))
57711
59920
  };
57712
59921
  }
@@ -57880,6 +60089,8 @@ async function runInteractiveSession(io, dependencies, terminal2, signal) {
57880
60089
  pair: ["pair"],
57881
60090
  "run-help": ["help", "run"],
57882
60091
  sessions: ["sessions"],
60092
+ "test-firebase": ["test", "firebase", "devices"],
60093
+ "test-gradle": ["test", "gradle"],
57883
60094
  version: ["version"]
57884
60095
  };
57885
60096
  clearInteractiveScreen(io.error, terminal2);
@@ -57909,10 +60120,10 @@ async function selectDevice(execution2, io, terminal2, remembered, signal) {
57909
60120
  }
57910
60121
  const selectable = data.targets.filter((target) => target.transports.some(({ stable, state }) => stable && state === "device"));
57911
60122
  if (selectable.length === 0) {
57912
- const problem2 = inputProblem(ProblemCode.NoSelectableTarget, "No ready Android target can be selected.", "Connect or recover a target until ADB reports the device state.", execution2.result.commandId);
60123
+ 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
60124
  return {
57914
60125
  exitCode: 20 /* Target */,
57915
- result: { ...execution2.result, ok: false, problems: [...execution2.result.problems, problem2] }
60126
+ result: { ...execution2.result, ok: false, problems: [...execution2.result.problems, problem4] }
57916
60127
  };
57917
60128
  }
57918
60129
  const selection2 = await selectOne({
@@ -58017,7 +60228,7 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58017
60228
  if (options.command === "mcp") {
58018
60229
  const explicitRoot = io.env.ADB_READY_MCP_PROJECT_ROOT?.trim();
58019
60230
  await runMcpStdio({
58020
- cwd: explicitRoot === undefined || explicitRoot === "" ? io.cwd : path24.resolve(explicitRoot),
60231
+ cwd: explicitRoot === undefined || explicitRoot === "" ? io.cwd : path26.resolve(explicitRoot),
58021
60232
  env: io.env,
58022
60233
  dependencies,
58023
60234
  version: VERSION,
@@ -58060,6 +60271,119 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58060
60271
  });
58061
60272
  return execution3.exitCode;
58062
60273
  }
60274
+ if (options.command === "test") {
60275
+ if (options.testKind === "firebase" && options.firebaseAction !== undefined) {
60276
+ const firebaseAction = options.firebaseAction;
60277
+ const executeFirebase = async (devices, memberSignal, resultsDirSuffix) => await runFirebaseTestLab({
60278
+ cwd: io.cwd,
60279
+ action: firebaseAction,
60280
+ ...options.gcloudPath === undefined ? {} : { gcloudPath: options.gcloudPath },
60281
+ ...options.cloudProject === undefined ? {} : { project: options.cloudProject },
60282
+ ...options.firebaseApp === undefined ? {} : { app: options.firebaseApp },
60283
+ ...options.firebaseTest === undefined ? {} : { test: options.firebaseTest },
60284
+ ...devices === undefined ? {} : { devices },
60285
+ ...options.firebaseMatrixId === undefined ? {} : { matrixId: options.firebaseMatrixId },
60286
+ ...options.firebaseResultsBucket === undefined ? {} : { resultsBucket: options.firebaseResultsBucket },
60287
+ ...options.firebaseResultsDir === undefined ? {} : {
60288
+ resultsDir: resultsDirSuffix === undefined ? options.firebaseResultsDir : `${options.firebaseResultsDir}/${resultsDirSuffix}`
60289
+ },
60290
+ ...options.firebaseTestTimeout === undefined ? {} : { testTimeout: options.firebaseTestTimeout },
60291
+ ...options.runTimeoutMs === undefined ? {} : { observationTimeoutMs: options.runTimeoutMs },
60292
+ ...options.firebaseAllowDeprecated === true ? { allowDeprecated: true } : {},
60293
+ ...options.firebaseAllowReducedStability === true ? { allowReducedStability: true } : {},
60294
+ ...options.firebaseAllowLowCapacity === true ? { allowLowCapacity: true } : {},
60295
+ ...options.dryRun ? { dryRun: true } : {}
60296
+ }, {
60297
+ ...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
60298
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
60299
+ ...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
60300
+ ...dependencies.firebaseFetch === undefined ? {} : { fetch: dependencies.firebaseFetch }
60301
+ }, memberSignal);
60302
+ let execution4;
60303
+ if (options.poolName === undefined) {
60304
+ execution4 = await executeFirebase(options.firebaseDevices, signal);
60305
+ } else {
60306
+ const loadedPool = await (dependencies.loadConfig ?? loadConfig)({
60307
+ cwd: io.cwd,
60308
+ env: io.env,
60309
+ ...options.configPath === undefined ? {} : { projectConfigPath: options.configPath },
60310
+ explicitProjectConfig: options.configPath !== undefined,
60311
+ ...options.profileName === undefined ? {} : { profileName: options.profileName },
60312
+ cli: cliConfig(options)
60313
+ });
60314
+ if (!loadedPool.ok) {
60315
+ const failed2 = failureResult("test firebase pool", configProblems(loadedPool.errors), dependencies);
60316
+ renderFailure(failed2, options.format, io);
60317
+ return 2 /* InvalidInput */;
60318
+ }
60319
+ const pool = loadedPool.config.values.targetPools?.[options.poolName];
60320
+ if (pool === undefined) {
60321
+ const failed2 = failureResult("test firebase pool", [
60322
+ targetPoolProblem("TARGET_POOL_NOT_FOUND", `Target pool ${options.poolName} was not found.`, "Define the named pool under targets.pools in the project configuration.")
60323
+ ], dependencies);
60324
+ renderFailure(failed2, options.format, io);
60325
+ return 2 /* InvalidInput */;
60326
+ }
60327
+ if (pool.members.some((member) => member.kind !== "firebase")) {
60328
+ const failed2 = failureResult("test firebase pool", [
60329
+ targetPoolProblem("TARGET_POOL_KIND_MISMATCH", `Target pool ${options.poolName} contains a non-Firebase member.`, "Firebase pool runs require every member to declare kind firebase.")
60330
+ ], dependencies);
60331
+ renderFailure(failed2, options.format, io);
60332
+ return 2 /* InvalidInput */;
60333
+ }
60334
+ execution4 = await runTargetPool({
60335
+ name: options.poolName,
60336
+ command: "test firebase pool",
60337
+ pool,
60338
+ coordination: "provider-managed",
60339
+ cancelRunningOnFailFast: false,
60340
+ ...options.maxConcurrency === undefined ? {} : { maxConcurrency: options.maxConcurrency },
60341
+ ...options.failFast === undefined ? {} : { failFast: options.failFast },
60342
+ ...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
60343
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
60344
+ ...signal === undefined ? {} : { signal },
60345
+ ...targetPoolProgress(options, io)
60346
+ }, async (member, memberSignal) => {
60347
+ const firebase = member;
60348
+ return await executeFirebase([
60349
+ {
60350
+ model: firebase.model,
60351
+ version: firebase.version,
60352
+ locale: firebase.locale,
60353
+ orientation: firebase.orientation
60354
+ }
60355
+ ], memberSignal, firebase.id);
60356
+ });
60357
+ }
60358
+ renderResult(execution4.result, {
60359
+ format: options.format,
60360
+ capabilities: capabilities(options, cliConfig(options), io, "output", options.format),
60361
+ sink: options.format === "human" ? io.error : io.output,
60362
+ verbose: options.verbose
60363
+ });
60364
+ return execution4.exitCode;
60365
+ }
60366
+ const execution3 = await runGradleManagedTest({
60367
+ cwd: io.cwd,
60368
+ ...options.gradleTask === undefined ? {} : { task: options.gradleTask },
60369
+ ...options.gradlePath === undefined ? {} : { gradlePath: options.gradlePath },
60370
+ ...options.gradleShards === undefined ? {} : { shards: options.gradleShards },
60371
+ ...options.gradleSoftwareRendering === true ? { softwareRendering: true } : {},
60372
+ ...options.runTimeoutMs === undefined ? {} : { timeoutMs: options.runTimeoutMs },
60373
+ ...options.dryRun ? { dryRun: true } : {}
60374
+ }, {
60375
+ ...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
60376
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
60377
+ ...dependencies.runner === undefined ? {} : { runner: dependencies.runner }
60378
+ }, signal);
60379
+ renderResult(execution3.result, {
60380
+ format: options.format,
60381
+ capabilities: capabilities(options, cliConfig(options), io, "output", options.format),
60382
+ sink: options.format === "human" ? io.error : io.output,
60383
+ verbose: options.verbose
60384
+ });
60385
+ return execution3.exitCode;
60386
+ }
58063
60387
  if (options.command === "context" || options.command === "sessions" || options.command === "problems") {
58064
60388
  const storeOptions = dependencies.sessionStore === false ? { env: io.env, projectRoot: io.cwd, allProjects: options.allProjects } : {
58065
60389
  ...dependencies.sessionStore ?? { env: io.env },
@@ -58107,6 +60431,31 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58107
60431
  return execution3.exitCode;
58108
60432
  }
58109
60433
  const values2 = loaded.config.values;
60434
+ if (options.command === "run" && options.poolName !== undefined) {
60435
+ const pool = values2.targetPools?.[options.poolName];
60436
+ if (pool === undefined) {
60437
+ const failed2 = failureResult("run pool", [
60438
+ targetPoolProblem("TARGET_POOL_NOT_FOUND", `Target pool ${options.poolName} was not found.`, "Define the named pool under targets.pools in the project configuration.")
60439
+ ], dependencies);
60440
+ renderFailure(failed2, options.format, io);
60441
+ return 2 /* InvalidInput */;
60442
+ }
60443
+ if (pool.members.some((member) => member.kind === "firebase")) {
60444
+ const failed2 = failureResult("run pool", [
60445
+ 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.")
60446
+ ], dependencies);
60447
+ renderFailure(failed2, options.format, io);
60448
+ return 2 /* InvalidInput */;
60449
+ }
60450
+ const execution3 = await runLocalTargetPool(argv, options, pool, io, dependencies, signal);
60451
+ renderResult(execution3.result, {
60452
+ format: options.format,
60453
+ capabilities: capabilities(options, values2, io, "output", options.format),
60454
+ sink: options.format === "human" ? io.error : io.output,
60455
+ verbose: options.verbose
60456
+ });
60457
+ return execution3.exitCode;
60458
+ }
58110
60459
  const errorCapabilities = capabilities(options, values2, io, "error", options.format);
58111
60460
  const outputCapabilities = capabilities(options, values2, io, "output", options.format);
58112
60461
  let lastTarget;
@@ -58118,7 +60467,7 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58118
60467
  ...values2.adbPort === undefined ? {} : { adbPort: values2.adbPort }
58119
60468
  });
58120
60469
  if (!state.ok) {
58121
- const problem2 = {
60470
+ const problem4 = {
58122
60471
  code: state.code,
58123
60472
  category: options.remembered ? "environment.state" : "state.persistence",
58124
60473
  severity: options.remembered ? "error" : "warning",
@@ -58130,11 +60479,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58130
60479
  correlation: { commandId: "state" }
58131
60480
  };
58132
60481
  if (options.remembered) {
58133
- const failure5 = failureResult(options.command, [problem2], dependencies);
60482
+ const failure5 = failureResult(options.command, [problem4], dependencies);
58134
60483
  renderFailure(failure5, options.format, io);
58135
60484
  return 10 /* Environment */;
58136
60485
  }
58137
- stateWarning = problem2;
60486
+ stateWarning = problem4;
58138
60487
  }
58139
60488
  if (state.ok) {
58140
60489
  lastTarget = rememberedTarget(state.document, {
@@ -58162,10 +60511,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58162
60511
  };
58163
60512
  let androidCapabilities;
58164
60513
  let preparedAvd;
60514
+ let avdResourceLease;
58165
60515
  let autonomousEvidence;
58166
60516
  let autonomousAdb;
58167
60517
  const autonomousRun = options.command === "run" && (options.avdName !== undefined || options.deployArtifact === true);
58168
- const runPreparationCommandId = autonomousRun ? (dependencies.idFactory ?? randomUUID14)() : "run-preparation";
60518
+ const runPreparationCommandId = autonomousRun ? (dependencies.idFactory ?? randomUUID17)() : "run-preparation";
58169
60519
  const finishAutonomousFailure = async (problems, requestedExitCode) => {
58170
60520
  let exitCode2 = requestedExitCode;
58171
60521
  if (preparedAvd !== undefined) {
@@ -58178,6 +60528,8 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58178
60528
  }
58179
60529
  preparedAvd = undefined;
58180
60530
  }
60531
+ await avdResourceLease?.release();
60532
+ avdResourceLease = undefined;
58181
60533
  const failed2 = {
58182
60534
  result: { ...failureResult("run", problems, dependencies), data: null },
58183
60535
  exitCode: exitCode2
@@ -58226,6 +60578,19 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58226
60578
  });
58227
60579
  }
58228
60580
  if (options.avdName !== undefined && !options.dryRun) {
60581
+ if (dependencies.targetLease !== false) {
60582
+ const acquired = await acquireTargetLease({
60583
+ targetIdentity: `avd:${options.avdName}`,
60584
+ projectRoot: io.cwd,
60585
+ purpose: `run avd ${options.avdName}`
60586
+ }, { env: io.env, ...dependencies.targetLease ?? {} });
60587
+ if (!acquired.ok) {
60588
+ return await finishAutonomousFailure([
60589
+ 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)
60590
+ ], acquired.code === "TARGET_LEASE_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */);
60591
+ }
60592
+ avdResourceLease = acquired.lease;
60593
+ }
58229
60594
  const emulator = capability(androidCapabilities, "emulator");
58230
60595
  if (emulator.status !== "supported" || emulator.invocation === undefined) {
58231
60596
  return await finishAutonomousFailure([
@@ -58256,16 +60621,27 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58256
60621
  return await finishAutonomousFailure([autonomousProblem(preparation.failure, "target.avd")], preparation.failure.code === "AVD_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */);
58257
60622
  }
58258
60623
  const avd = preparation.avd;
58259
- preparedAvd = avd;
60624
+ const releaseAvd = avd.release.bind(avd);
60625
+ preparedAvd = {
60626
+ ...avd,
60627
+ release: async (releaseSignal) => {
60628
+ try {
60629
+ return await releaseAvd(releaseSignal);
60630
+ } finally {
60631
+ await avdResourceLease?.release();
60632
+ avdResourceLease = undefined;
60633
+ }
60634
+ }
60635
+ };
58260
60636
  autonomousEvidence = {
58261
60637
  avd: {
58262
- name: avd.name,
58263
- serial: avd.serial,
58264
- ownership: avd.ownership,
58265
- readiness: avd.readiness
60638
+ name: preparedAvd.name,
60639
+ serial: preparedAvd.serial,
60640
+ ownership: preparedAvd.ownership,
60641
+ readiness: preparedAvd.readiness
58266
60642
  }
58267
60643
  };
58268
- config4.targetSelector = avd.serial;
60644
+ config4.targetSelector = preparedAvd.serial;
58269
60645
  delete config4.targetTransportId;
58270
60646
  delete config4.rememberedSerial;
58271
60647
  delete config4.rememberedHardwareSerial;
@@ -58438,11 +60814,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58438
60814
  if (!acquired.ok) {
58439
60815
  const failure5 = failureResult(options.command, [
58440
60816
  {
58441
- code: acquired.code === "TARGET_BUSY" ? ProblemCode.TargetBusy : ProblemCode.TargetLeaseUnavailable,
60817
+ code: acquired.code === "TARGET_BUSY" ? ProblemCode.TargetBusy : acquired.code === "TARGET_LEASE_CANCELLED" ? ProblemCode.OperationInterrupted : ProblemCode.TargetLeaseUnavailable,
58442
60818
  category: "target.lease",
58443
60819
  severity: "error",
58444
60820
  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.",
60821
+ 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
60822
  retryable: true,
58447
60823
  evidence: acquired.owner === undefined ? [] : [
58448
60824
  { source: "target.lease", field: "purpose", value: acquired.owner.purpose },
@@ -58461,13 +60837,14 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58461
60837
  correlation: { commandId: "target-lease", targetId: preparedSelection.target.id }
58462
60838
  }
58463
60839
  ], dependencies);
60840
+ const leaseExitCode = acquired.code === "TARGET_LEASE_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */;
58464
60841
  if (autonomousRun) {
58465
- return await finishAutonomousFailure([...failure5.problems], 20 /* Target */);
60842
+ return await finishAutonomousFailure([...failure5.problems], leaseExitCode);
58466
60843
  }
58467
60844
  await preparedAvd?.release(signal);
58468
60845
  preparedAvd = undefined;
58469
60846
  renderFailure(failure5, options.format, io);
58470
- return 20 /* Target */;
60847
+ return leaseExitCode;
58471
60848
  }
58472
60849
  targetLease = acquired.lease;
58473
60850
  }
@@ -58939,7 +61316,7 @@ async function runCli(argv, io, dependencies = {}, signal) {
58939
61316
  return await runCliInternal(argv, io, dependencies, signal);
58940
61317
  } catch (caught) {
58941
61318
  const message = redactText(caught instanceof Error ? caught.message : String(caught)).value;
58942
- const problem2 = {
61319
+ const problem4 = {
58943
61320
  code: "INTERNAL_ERROR",
58944
61321
  category: "internal.unexpected",
58945
61322
  severity: "error",
@@ -58950,7 +61327,7 @@ async function runCli(argv, io, dependencies = {}, signal) {
58950
61327
  actions: [],
58951
61328
  correlation: { commandId: "cli" }
58952
61329
  };
58953
- renderFailure(failureResult("cli", [problem2], dependencies), inferredFormat(argv), io);
61330
+ renderFailure(failureResult("cli", [problem4], dependencies), inferredFormat(argv), io);
58954
61331
  return 70 /* Internal */;
58955
61332
  }
58956
61333
  }
@@ -58978,4 +61355,4 @@ try {
58978
61355
  process16.removeListener("SIGTERM", abort);
58979
61356
  }
58980
61357
 
58981
- //# debugId=7127D338B7D94B7264756E2164756E21
61358
+ //# debugId=8986C9014073E12364756E2164756E21