@delacour/warden-e2e 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/{chunk-yb1hcjqm.mjs → chunk-vs5npy59.mjs} +3370 -218
  2. package/index.mjs +247 -16
  3. package/package.json +2 -2
  4. package/preload.mjs +8264 -2387
  5. package/types/core/src/a11y/a11y.config.d.ts +47 -0
  6. package/types/core/src/affected/affected.schema.d.ts +142 -1
  7. package/types/core/src/allocate.d.ts +2 -1
  8. package/types/core/src/android-keyguard.d.ts +36 -0
  9. package/types/core/src/batch/preset.schema.d.ts +13 -1
  10. package/types/core/src/biometrics.d.ts +15 -0
  11. package/types/core/src/builds/app-option.schema.d.ts +1 -1
  12. package/types/core/src/builds/builds.defaults.d.ts +15 -0
  13. package/types/core/src/builds/config.d.ts +5 -1
  14. package/types/core/src/builds/config.schema.d.ts +275 -2
  15. package/types/core/src/builds/define-config.d.ts +159 -0
  16. package/types/core/src/builds/install-device.d.ts +37 -0
  17. package/types/core/src/builds/install.d.ts +17 -3
  18. package/types/core/src/builds/kinds.d.ts +21 -3
  19. package/types/core/src/builds/lock.d.ts +38 -0
  20. package/types/core/src/builds/resolve.d.ts +12 -2
  21. package/types/core/src/camera-mode.d.ts +17 -0
  22. package/types/core/src/camera-placeholder.d.ts +9 -0
  23. package/types/core/src/camera-profile.d.ts +12 -0
  24. package/types/core/src/claim-errors.d.ts +34 -0
  25. package/types/core/src/conditions/a11y-keys.d.ts +45 -0
  26. package/types/core/src/conditions/camera-media.d.ts +30 -0
  27. package/types/core/src/conditions/conditions.config.d.ts +141 -0
  28. package/types/core/src/conditions/conditions.d.ts +22 -0
  29. package/types/core/src/conditions/conditions.types.d.ts +171 -0
  30. package/types/core/src/conditions/inject-state.d.ts +13 -0
  31. package/types/core/src/conditions/launch-settings.d.ts +14 -0
  32. package/types/core/src/conditions/network.presets.d.ts +42 -0
  33. package/types/core/src/conditions/text-size.d.ts +19 -0
  34. package/types/core/src/golden/android-biometrics.d.ts +35 -0
  35. package/types/core/src/ios-runner/build.d.ts +28 -0
  36. package/types/core/src/ios-runner/cache.d.ts +44 -0
  37. package/types/core/src/ios-runner/classify.d.ts +7 -0
  38. package/types/core/src/ios-runner/pack.d.ts +17 -0
  39. package/types/core/src/ios-runner/runner-build.types.d.ts +76 -0
  40. package/types/core/src/ios-runner/signing.d.ts +40 -0
  41. package/types/core/src/ios-runner/sim-build.d.ts +36 -0
  42. package/types/core/src/ios-runner/stage.d.ts +26 -0
  43. package/types/core/src/lease-events.d.ts +126 -0
  44. package/types/core/src/netproxy/control.d.ts +75 -0
  45. package/types/core/src/netproxy/decide.d.ts +22 -0
  46. package/types/core/src/netproxy/handle-file.d.ts +12 -0
  47. package/types/core/src/netproxy/lease-proxy.d.ts +53 -0
  48. package/types/core/src/netproxy/netproxy.main.d.ts +40 -0
  49. package/types/core/src/netproxy/pipe.d.ts +37 -0
  50. package/types/core/src/netproxy/proxy.d.ts +48 -0
  51. package/types/core/src/netproxy/shaping.d.ts +43 -0
  52. package/types/core/src/ownership.d.ts +44 -0
  53. package/types/core/src/providers/android.d.ts +46 -2
  54. package/types/core/src/providers/provider.types.d.ts +9 -0
  55. package/types/core/src/queue.d.ts +43 -0
  56. package/types/core/src/store.d.ts +61 -5
  57. package/types/core/src/types.d.ts +57 -0
  58. package/types/core/src/xcode.d.ts +22 -0
  59. package/types/e2e/src/a11y/a11y-target.d.ts +23 -0
  60. package/types/e2e/src/a11y/a11y.types.d.ts +81 -0
  61. package/types/e2e/src/a11y/announcements.d.ts +24 -0
  62. package/types/e2e/src/a11y/audit.d.ts +22 -0
  63. package/types/e2e/src/a11y/native/atf.d.ts +51 -0
  64. package/types/e2e/src/a11y/native/xcuitest.d.ts +60 -0
  65. package/types/e2e/src/a11y/order.d.ts +5 -0
  66. package/types/e2e/src/a11y/rules/duplicate-name.d.ts +6 -0
  67. package/types/e2e/src/a11y/rules/hidden-focusable.d.ts +3 -0
  68. package/types/e2e/src/a11y/rules/image-no-name.d.ts +6 -0
  69. package/types/e2e/src/a11y/rules/label-in-name.d.ts +7 -0
  70. package/types/e2e/src/a11y/rules/missing-name.d.ts +3 -0
  71. package/types/e2e/src/a11y/rules/role-in-name.d.ts +3 -0
  72. package/types/e2e/src/a11y/rules/rule.context.d.ts +22 -0
  73. package/types/e2e/src/a11y/rules/target-size.d.ts +21 -0
  74. package/types/e2e/src/a11y/rules/text-clipped.d.ts +9 -0
  75. package/types/e2e/src/a11y/speech.d.ts +26 -0
  76. package/types/e2e/src/a11y/talkback-log.d.ts +13 -0
  77. package/types/e2e/src/alert-buttons.d.ts +9 -0
  78. package/types/e2e/src/app.d.ts +3 -2
  79. package/types/e2e/src/conditions/condition-events.d.ts +68 -0
  80. package/types/e2e/src/conditions/device-conditions.d.ts +198 -0
  81. package/types/e2e/src/conditions/geo.d.ts +15 -0
  82. package/types/e2e/src/conditions/gpx.d.ts +8 -0
  83. package/types/e2e/src/conditions/lease-recorder.d.ts +27 -0
  84. package/types/e2e/src/conditions/lease-store.d.ts +9 -0
  85. package/types/e2e/src/conditions/location-plan.d.ts +17 -0
  86. package/types/e2e/src/conditions/location-scenarios.data.d.ts +17 -0
  87. package/types/e2e/src/conditions/route-ticker.d.ts +53 -0
  88. package/types/e2e/src/conditions/scheduler.d.ts +83 -0
  89. package/types/e2e/src/device-a11y.d.ts +97 -0
  90. package/types/e2e/src/device.d.ts +67 -4
  91. package/types/e2e/src/driver/a11y-events.d.ts +30 -0
  92. package/types/e2e/src/driver/a11y-read.d.ts +12 -0
  93. package/types/e2e/src/driver/android/android-animations.d.ts +7 -0
  94. package/types/e2e/src/driver/android/android-app.d.ts +4 -0
  95. package/types/e2e/src/driver/android/android-argv.d.ts +21 -0
  96. package/types/e2e/src/driver/android/android-camera-host.d.ts +23 -0
  97. package/types/e2e/src/driver/android/android-camera.d.ts +28 -0
  98. package/types/e2e/src/driver/android/android-clock.d.ts +28 -0
  99. package/types/e2e/src/driver/android/android-device.d.ts +8 -0
  100. package/types/e2e/src/driver/android/android-driver.d.ts +22 -0
  101. package/types/e2e/src/driver/android/android-network.d.ts +66 -0
  102. package/types/e2e/src/driver/android/android-phone.d.ts +47 -0
  103. package/types/e2e/src/driver/android/android-screen-reader.d.ts +23 -0
  104. package/types/e2e/src/driver/android/android-settings.d.ts +41 -0
  105. package/types/e2e/src/driver/android/android-spoken.d.ts +18 -0
  106. package/types/e2e/src/driver/android/android-state.d.ts +10 -0
  107. package/types/e2e/src/driver/android/android-talkback.d.ts +57 -0
  108. package/types/e2e/src/driver/android/atf-device.d.ts +23 -0
  109. package/types/e2e/src/driver/android/atf-dex.generated.d.ts +7 -0
  110. package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +5 -5
  111. package/types/e2e/src/driver/android/uiad-ops.d.ts +27 -1
  112. package/types/e2e/src/driver/android/uiad-protocol.d.ts +8 -0
  113. package/types/e2e/src/driver/android/uiad-screen-reader.d.ts +33 -0
  114. package/types/e2e/src/driver/clock-recorder.d.ts +9 -0
  115. package/types/e2e/src/driver/device-settings.d.ts +10 -0
  116. package/types/e2e/src/driver/driver.types.d.ts +242 -1
  117. package/types/e2e/src/driver/element-actions.d.ts +16 -0
  118. package/types/e2e/src/driver/ios/device/capabilities.d.ts +3 -0
  119. package/types/e2e/src/driver/ios/device/device-driver.d.ts +49 -0
  120. package/types/e2e/src/driver/ios/device/devicectl.d.ts +38 -0
  121. package/types/e2e/src/driver/ios/device/runner-protocol.d.ts +21 -0
  122. package/types/e2e/src/driver/ios/hid-recovery.d.ts +15 -0
  123. package/types/e2e/src/driver/ios/inject.d.ts +124 -0
  124. package/types/e2e/src/driver/ios/ios-a11y.d.ts +73 -0
  125. package/types/e2e/src/driver/ios/ios-audit.d.ts +36 -0
  126. package/types/e2e/src/driver/ios/ios-driver.d.ts +50 -1
  127. package/types/e2e/src/driver/ios/ios-network.d.ts +51 -0
  128. package/types/e2e/src/driver/ios/ios-settings.d.ts +44 -0
  129. package/types/e2e/src/driver/ios/sim-runner.d.ts +41 -0
  130. package/types/e2e/src/driver/ios/simctl.d.ts +41 -1
  131. package/types/e2e/src/driver/ios/simd-transport.d.ts +11 -2
  132. package/types/e2e/src/driver/ios/type-budget.d.ts +6 -1
  133. package/types/e2e/src/driver/native-audit.d.ts +26 -0
  134. package/types/e2e/src/driver/network-proxy.d.ts +52 -0
  135. package/types/e2e/src/driver/open.d.ts +20 -0
  136. package/types/e2e/src/driver/physical-conditions.d.ts +11 -0
  137. package/types/e2e/src/driver/requirements.d.ts +14 -0
  138. package/types/e2e/src/driver/screen-reader.d.ts +26 -0
  139. package/types/e2e/src/driver/typing.d.ts +1 -1
  140. package/types/e2e/src/env.d.ts +47 -0
  141. package/types/e2e/src/expect/a11y-matchers.d.ts +33 -0
  142. package/types/e2e/src/expect/expect.d.ts +7 -2
  143. package/types/e2e/src/expect/matchers.d.ts +14 -0
  144. package/types/e2e/src/fixture-session.d.ts +4 -0
  145. package/types/e2e/src/fixtures.d.ts +6 -1
  146. package/types/e2e/src/index.d.ts +6 -0
  147. package/types/e2e/src/runner/callsite.d.ts +3 -1
  148. package/types/e2e/src/runner/scope.d.ts +1 -0
  149. package/types/e2e/src/runner/test.d.ts +6 -0
  150. package/types/e2e/src/screen/render.d.ts +8 -0
  151. package/types/e2e/src/traced-device.d.ts +2 -1
@@ -0,0 +1,53 @@
1
+ import { type AsyncResult } from "../../../types/src/result";
2
+ import type { NetworkCondition } from "../conditions/conditions.types";
3
+ import { type PidAlive } from "../liveness";
4
+ import type { Store } from "../store";
5
+ import type { Lease } from "../types";
6
+ import { type ProxyHandle } from "./control";
7
+ import type { ProxyConditions } from "./shaping";
8
+ /** Where lease proxies listen unless told otherwise. */
9
+ export declare const DEFAULT_PROXY_PORTS: {
10
+ readonly from: 19080;
11
+ readonly span: 200;
12
+ };
13
+ export type LeaseProxyDeps = {
14
+ store: Store;
15
+ env: Record<string, string | undefined>;
16
+ now: () => number;
17
+ pidAlive?: PidAlive;
18
+ isPortFree?: (port: number) => Promise<boolean>;
19
+ /** argv prefix that runs `netproxy.main.ts` (the JSON argument is appended). Default: `bun` + the script next to this file; a compiled `warden` passes its own hidden subcommand. */
20
+ command?: readonly string[];
21
+ /** start argv detached and return its pid; default: `child_process.spawn` detached with no stdio */
22
+ spawn?: (argv: readonly string[], env: Record<string, string | undefined>) => number | undefined;
23
+ kill?: (pid: number) => void;
24
+ ports?: {
25
+ from: number;
26
+ span: number;
27
+ };
28
+ readyTimeoutMs?: number;
29
+ };
30
+ /** The proxy a lease owns, and the port lease holding its port. */
31
+ export type LeaseProxy = {
32
+ handle: ProxyHandle;
33
+ portLeaseId: string;
34
+ reused: boolean;
35
+ };
36
+ export type EnsureProxyInput = {
37
+ /** what `recordCondition` stores for the `network` area */
38
+ condition: NetworkCondition;
39
+ /** shaping to apply now (already translated from `condition`) */
40
+ conditions: ProxyConditions;
41
+ /** seeds loss/jitter for a reproducible run */
42
+ seed?: number;
43
+ };
44
+ /**
45
+ * Make sure `lease` has a shaping proxy and apply `input.conditions` to it. Reuses a live one
46
+ * (reconfigured over its control channel, no restart); otherwise claims a port from the allocator,
47
+ * spawns a detached `netproxy.main.ts`, waits until it answers, and records the `network` condition
48
+ * with the proxy pid as an `OwnedProcess` so release / gc undo kills it. The port lease's pid is
49
+ * the proxy's, so a dead proxy frees its port. Binds 127.0.0.1 only.
50
+ */
51
+ export declare function ensureLeaseProxy(deps: LeaseProxyDeps, lease: Pick<Lease, "id" | "owner" | "ttlMs">, input: EnsureProxyInput): AsyncResult<LeaseProxy>;
52
+ /** Stop a lease's proxy (control channel, then SIGTERM) and free its port; true when one was recorded. */
53
+ export declare function releaseLeaseProxy(deps: Pick<LeaseProxyDeps, "store" | "env" | "kill" | "pidAlive">, leaseId: string): Promise<boolean>;
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ declare const argvSchema: z.ZodObject<{
3
+ leaseId: z.ZodString;
4
+ handleFile: z.ZodString;
5
+ port: z.ZodNumber;
6
+ conditions: z.ZodOptional<z.ZodObject<{
7
+ params: z.ZodOptional<z.ZodObject<{
8
+ latencyMs: z.ZodOptional<z.ZodNumber>;
9
+ jitterMs: z.ZodOptional<z.ZodNumber>;
10
+ downKbps: z.ZodOptional<z.ZodNumber>;
11
+ upKbps: z.ZodOptional<z.ZodNumber>;
12
+ lossPct: z.ZodOptional<z.ZodNumber>;
13
+ offline: z.ZodOptional<z.ZodEnum<{
14
+ blackhole: "blackhole";
15
+ refuse: "refuse";
16
+ }>>;
17
+ }, z.core.$strict>>;
18
+ faults: z.ZodOptional<z.ZodArray<z.ZodObject<{
19
+ match: z.ZodString;
20
+ action: z.ZodDiscriminatedUnion<[z.ZodObject<{
21
+ kind: z.ZodLiteral<"status">;
22
+ code: z.ZodNumber;
23
+ }, z.core.$strict>, z.ZodObject<{
24
+ kind: z.ZodLiteral<"reset">;
25
+ }, z.core.$strict>, z.ZodObject<{
26
+ kind: z.ZodLiteral<"delay">;
27
+ ms: z.ZodNumber;
28
+ }, z.core.$strict>, z.ZodObject<{
29
+ kind: z.ZodLiteral<"blackhole">;
30
+ }, z.core.$strict>], "kind">;
31
+ }, z.core.$strict>>>;
32
+ }, z.core.$strict>>;
33
+ seed: z.ZodOptional<z.ZodNumber>;
34
+ portLeaseId: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strict>;
36
+ /** Argv for this entry: one JSON argument (`NetproxyMainArgs`). */
37
+ export type NetproxyMainArgs = z.input<typeof argvSchema>;
38
+ /** Run a detached proxy: parse the JSON argument, listen, publish the handle file, exit on SIGTERM / `POST /shutdown`. */
39
+ export declare function runNetproxy(raw: string): Promise<void>;
40
+ export {};
@@ -0,0 +1,37 @@
1
+ import type { Socket } from "node:net";
2
+ export type PipeOptions = {
3
+ /** bandwidth cap read per slice, so a live change applies to the next one */
4
+ kbps: () => number | undefined;
5
+ /** delay for the next chunk, read per chunk */
6
+ delay: () => number;
7
+ now: () => number;
8
+ /** bytes written to `dest` */
9
+ onBytes: (n: number) => void;
10
+ };
11
+ /**
12
+ * One direction of a shaped connection. Chunks keep order; each waits out its own latency (pipelined,
13
+ * not serial), then goes out in slices at the bandwidth cap. Driven by one timer at a time, never polled.
14
+ */
15
+ export declare class Pipe {
16
+ private readonly src;
17
+ private readonly dest;
18
+ private readonly opts;
19
+ private queue;
20
+ private queued;
21
+ private lastNotBefore;
22
+ private timer;
23
+ private bucket;
24
+ private finished;
25
+ private stopped;
26
+ private draining;
27
+ constructor(src: Socket, dest: Socket, opts: PipeOptions);
28
+ push(data: Buffer): void;
29
+ /** The source ended: close `dest` once everything queued is out. */
30
+ finish(): void;
31
+ stop(): void;
32
+ private wake;
33
+ /** What the head of the queue needs next: nothing, a wait (latency or bandwidth), or a send. */
34
+ private next;
35
+ private send;
36
+ private pump;
37
+ }
@@ -0,0 +1,48 @@
1
+ import { type ConditionsUpdate, type ProxyHandle, type ProxyStats } from "./control";
2
+ import { type ProxyConditions, type Rng } from "./shaping";
3
+ /** Something notable the proxy did; `https-status-as-reset` is the CONNECT fallback for a `status` fault. */
4
+ export type ProxyEvent = {
5
+ kind: "fault";
6
+ host: string;
7
+ action: "status" | "reset" | "delay" | "blackhole";
8
+ note?: "https-status-as-reset";
9
+ } | {
10
+ kind: "loss";
11
+ outcome: "reset" | "stall";
12
+ host: string;
13
+ } | {
14
+ kind: "offline";
15
+ mode: "refuse" | "blackhole";
16
+ };
17
+ export type StartProxyOptions = {
18
+ /** listen port; 0 (default) = random */
19
+ port?: number;
20
+ conditions?: Partial<ProxyConditions>;
21
+ /** drives loss and jitter; default `Math.random` */
22
+ rng?: Rng;
23
+ now?: () => number;
24
+ /** map the requested origin to where to actually connect (tests point fake hosts at local servers) */
25
+ resolve?: (host: string, port: number) => {
26
+ host: string;
27
+ port: number;
28
+ };
29
+ onEvent?: (event: ProxyEvent) => void;
30
+ /** fixed control token (default random) */
31
+ token?: string;
32
+ /** called after `POST /shutdown` has closed the proxy (a detached entry exits here) */
33
+ onShutdown?: () => void;
34
+ };
35
+ export type RunningProxy = {
36
+ handle: ProxyHandle;
37
+ conditions(): ProxyConditions;
38
+ setConditions(update: ConditionsUpdate): void;
39
+ stats(): ProxyStats;
40
+ /** resolves once listeners and every open connection are closed */
41
+ close(): Promise<void>;
42
+ };
43
+ /**
44
+ * Start a shaping forward proxy on loopback: plain-HTTP absolute-URI requests and HTTPS `CONNECT`
45
+ * tunnels (bytes only, no TLS interception). Conditions can change at any time via `setConditions`
46
+ * or the control channel in `handle`; new connections and the next slice of open ones follow.
47
+ */
48
+ export declare function startProxy(options?: StartProxyOptions): Promise<RunningProxy>;
@@ -0,0 +1,43 @@
1
+ import type { HostFault, NetworkCondition } from "../conditions/conditions.types";
2
+ import { type NetworkShaping } from "../conditions/network.presets";
3
+ /** How an `offline` proxy treats connections: close at accept, or accept and never answer. */
4
+ export type OfflineMode = "refuse" | "blackhole";
5
+ /**
6
+ * What the proxy does to traffic: the shared `NetworkShaping` fields (all optional here; unset / 0 = no
7
+ * limit, delay or loss) plus `offline`. `latencyMs` is the added round trip: the full amount at connection
8
+ * open, half on each direction's chunks. `jitterMs` spreads each delay by +/- that much (halved per chunk).
9
+ * `lossPct` (0-100) resets or stalls (half each) a connection at open.
10
+ */
11
+ export type ShapingParams = Partial<NetworkShaping> & {
12
+ offline?: OfflineMode;
13
+ };
14
+ /** Proxy params for a condition (presets table); `offline` refuses connections. Faults are separate. */
15
+ export declare function shapingFromCondition(condition: NetworkCondition): ShapingParams;
16
+ /** The live state of a proxy: what `setProxyConditions` replaces. */
17
+ export type ProxyConditions = {
18
+ params: ShapingParams;
19
+ faults: readonly HostFault[];
20
+ };
21
+ export declare const NO_CONDITIONS: ProxyConditions;
22
+ /** Uniform [0, 1); injectable so loss and jitter are reproducible in tests. */
23
+ export type Rng = () => number;
24
+ /** mulberry32: tiny seeded PRNG. */
25
+ export declare function seededRng(seed: number): Rng;
26
+ /** `(latencyMs ± jitterMs) * scale`, floored at 0. */
27
+ export declare function delayMs(params: ShapingParams, rng: Rng, scale?: number): number;
28
+ /** `*` matches any run of characters; case-insensitive; the whole host must match. */
29
+ export declare function hostMatches(glob: string, host: string): boolean;
30
+ /** First fault whose glob matches `host`. */
31
+ export declare function faultFor(faults: readonly HostFault[], host: string): HostFault | undefined;
32
+ /**
33
+ * Serialises sends at a fixed rate: `waitMs` says how long until the next `bytes` may go out, and
34
+ * `take` books them. The rate is read per call, so a live change applies to the next slice.
35
+ */
36
+ export declare class TokenBucket {
37
+ private nextFree;
38
+ /** ms until `bytes` may be sent at `kbps` (0 = now); unlimited when `kbps` is unset or ≤ 0 */
39
+ waitMs(now: number): number;
40
+ take(bytes: number, kbps: number | undefined, now: number): void;
41
+ }
42
+ /** Slice size giving ~50 ms granularity at `kbps`, within 256 B – 64 KB. */
43
+ export declare function sliceSize(kbps: number | undefined): number;
@@ -0,0 +1,44 @@
1
+ import type { Store } from "./store";
2
+ /**
3
+ * Ownership marker: warden writes `.warden-owned.json` into the device dir of every iOS simulator it creates,
4
+ * naming the host instance (a random id kept in the store `meta`) that made it. Destructive paths (shutdown on
5
+ * release, erase, delete) refuse a sim whose marker names another instance: a second `WARDEN_HOME` or user
6
+ * account on the same machine owns it. A sim with no marker keeps today's rules. Android emulators are AVDs
7
+ * launched from a shared definition, not created per claim, so they carry no marker.
8
+ */
9
+ export declare const MARKER_FILE = ".warden-owned.json";
10
+ export declare const INSTANCE_META_KEY = "instance_id";
11
+ export type OwnershipMarker = {
12
+ instanceId: string;
13
+ name: string;
14
+ createdAt: number;
15
+ };
16
+ /** The filesystem the marker lives on, injectable for tests. */
17
+ export type MarkerFs = {
18
+ readText: (path: string) => string | undefined;
19
+ writeText: (path: string, text: string) => void;
20
+ isDir: (path: string) => boolean;
21
+ };
22
+ export declare const realMarkerFs: MarkerFs;
23
+ type Env = Record<string, string | undefined>;
24
+ /** This host's stable instance id: created on first use inside a transaction, then read from `meta`. */
25
+ export declare function instanceId(store: Store): string;
26
+ /** `~/Library/Developer/CoreSimulator/Devices/<udid>` (the default device set); undefined without `HOME`. */
27
+ export declare function simDeviceDir(env: Env, udid: string): string | undefined;
28
+ export type MarkerRead = {
29
+ kind: "absent";
30
+ } | {
31
+ kind: "present";
32
+ marker: OwnershipMarker;
33
+ } | {
34
+ kind: "unreadable";
35
+ };
36
+ export declare function readMarker(fs: MarkerFs, env: Env, udid: string): MarkerRead;
37
+ /** Best-effort: only into a device dir that exists, and a failure never fails the create. */
38
+ export declare function writeMarker(fs: MarkerFs, env: Env, store: Store, udid: string, name: string, now: number): boolean;
39
+ /**
40
+ * Why a destructive step must leave this sim alone, else undefined: its marker names another warden instance.
41
+ * A missing or unreadable marker blocks nothing (sims made before markers existed).
42
+ */
43
+ export declare function markerBlock(fs: MarkerFs, env: Env, store: Store, udid: string): string | undefined;
44
+ export {};
@@ -1,5 +1,7 @@
1
1
  import { type AsyncResult, type Result } from "../../../types/src/result";
2
+ import { type CameraMode } from "../camera-mode";
2
3
  import { type PidAlive } from "../liveness";
4
+ import { type Store } from "../store";
3
5
  import type { Lease } from "../types";
4
6
  import type { DeviceProvider, ProviderDeps } from "./provider.types";
5
7
  /** Emulator console ports: even, 5554–5584; the adb port is console + 1. */
@@ -16,7 +18,24 @@ export type AdbEmulator = {
16
18
  port: number;
17
19
  state: string;
18
20
  };
19
- /** Parse `adb devices -l`, keeping only `emulator-<port>` serials (physical devices ignored). */
21
+ /** One `adb devices -l` row: a console emulator, or anything else (a phone, or a network emulator until `classifyOther`). */
22
+ export type AdbListEntry = ({
23
+ kind: "emulator";
24
+ } & AdbEmulator) | {
25
+ kind: "other";
26
+ serial: string;
27
+ state: string;
28
+ transport: "usb" | "wifi";
29
+ model?: string;
30
+ product?: string;
31
+ };
32
+ /**
33
+ * Parse `adb devices -l`. A state may be several words (`no permissions (…); see [url]`), so the state is
34
+ * the first token unless the row says `no permissions`. Transport: a `usb:` attribute is usb; `ip:port` and
35
+ * `_adb-tls-connect._tcp` serials (and anything else without `usb:`) are wifi.
36
+ */
37
+ export declare function parseAdbDeviceList(stdout: string): AdbListEntry[];
38
+ /** Parse `adb devices -l`, keeping only `emulator-<port>` serials (phones and network emulators ignored). */
20
39
  export declare function parseAdbDevices(stdout: string): AdbEmulator[];
21
40
  /** Parse `emulator -list-avds`, dropping the emulator's own `INFO | …` / `WARNING | …` log lines. */
22
41
  export declare function parseAvdList(stdout: string): string[];
@@ -38,8 +57,24 @@ export declare const LIST_AVDS_TIMEOUT_MS = 15000;
38
57
  export declare function listAvds(deps: ExecDeps, opts?: {
39
58
  fresh?: boolean;
40
59
  }): AsyncResult<string[]>;
60
+ /** Every row of `adb devices -l`: emulators, phones and network emulators. */
61
+ export declare function listAdbDevices(deps: ExecDeps): AsyncResult<AdbListEntry[]>;
41
62
  /** Running emulators from `adb devices -l`. */
42
63
  export declare function listEmulators(deps: ExecDeps): AsyncResult<AdbEmulator[]>;
64
+ export type OtherClass = {
65
+ emulator: boolean;
66
+ hwSerial?: string;
67
+ model?: string;
68
+ sdk?: string;
69
+ };
70
+ /**
71
+ * Is a non-`emulator-N` adb serial a real phone? `localhost:5555` / Genymotion / a network emulator set
72
+ * `ro.boot.qemu` or `ro.kernel.qemu` to 1: a foreign emulator, never physical. One bounded read; a failure
73
+ * is an error, not a guess.
74
+ */
75
+ export declare function classifyOther(deps: ExecDeps, serial: string): AsyncResult<OtherClass>;
76
+ /** Is `id` a console emulator serial; anything else is a phone (or a network emulator), which warden never boots, kills or writes settings to. */
77
+ export declare function isEmulatorSerial(id: string): boolean;
43
78
  /** Launch `cmd` so it outlives warden, stdout/stderr appended to `logPath`. */
44
79
  export type SpawnDetached = (cmd: readonly string[], logPath: string) => AsyncResult<{
45
80
  pid: number;
@@ -66,6 +101,8 @@ export type AndroidProviderOptions = {
66
101
  };
67
102
  /** One `adb shell` that returns once the guest has booted; POSIX `sh` (toybox `[`). */
68
103
  export declare const BOOT_WAIT_SCRIPT = "while [ \"$(getprop sys.boot_completed)\" != 1 ]; do sleep 0.2; done";
104
+ /** One shell: qemu props (emulator tell-tales), hardware serial, model, SDK level — one value per line. */
105
+ export declare const PHONE_PROBE_SCRIPT = "getprop ro.boot.qemu; getprop ro.kernel.qemu; getprop ro.serialno; getprop ro.product.model; getprop ro.build.version.sdk";
69
106
  /** The last 3 lines of an emulator log, one line. */
70
107
  export declare function logTail(logPath: string): string;
71
108
  /**
@@ -80,7 +117,12 @@ export declare function rejectedOption(error: string): boolean;
80
117
  /** A claim's emulator: headless, `-read-only` (one AVD backs many), from `snapshot` when given, never saving on exit. */
81
118
  export declare function claimEmulatorArgv(tools: AndroidTools, env: Record<string, string | undefined>, avd: string, port: number, snapshot?: string,
82
119
  /** `false` leaves out `headlessEmulatorFlags` (an emulator that rejected them) */
83
- headless?: boolean): string[];
120
+ headless?: boolean,
121
+ /** a `+cam` device's feeds: each camera plays `mode`'s video, else reads `<facing>.png` from `dir` */
122
+ camera?: {
123
+ dir: string;
124
+ mode: CameraMode;
125
+ }): string[];
84
126
  export type BootWaitDeps = ExecDeps & {
85
127
  now: () => number;
86
128
  sleep: (ms: number) => Promise<void>;
@@ -115,6 +157,8 @@ export declare const GOLDEN_PORT_LABEL = "emulator golden ";
115
157
  * and unverified on a device; a successful load logs `Successfully loaded snapshot …`, which does not match.
116
158
  */
117
159
  export declare function snapshotLoadFailed(log: string): boolean;
160
+ /** The bookkeeping of an emulator that is gone: its console-port lease and its `devices` record. */
161
+ export declare function forgetAndroidEmulator(store: Store, serial: string): void;
118
162
  /**
119
163
  * Global settings applied after every boot. On a fresh playstore image the first `adb install` can pop a Play
120
164
  * Protect consent dialog (a GMS activity the e2e alert detector cannot classify), and every adb install otherwise
@@ -1,5 +1,6 @@
1
1
  import type { AsyncResult } from "../../../types/src/result";
2
2
  import type { Exec } from "../exec";
3
+ import type { MarkerFs } from "../ownership";
3
4
  import type { Store } from "../store";
4
5
  import type { InventoryDevice, Owner, Platform } from "../types";
5
6
  import type { Timer } from "./health";
@@ -36,6 +37,12 @@ export interface DeviceProvider {
36
37
  * shutdown reuse), never for an already-booted one. A failure is logged by the provider, never fatal.
37
38
  */
38
39
  prepareBoot?(id: string): Promise<void>;
40
+ /**
41
+ * Restart a running warden device in place (same id, same console port, device record and port lease kept) so
42
+ * launch-time settings apply again; the caller then waits with `waitReady`. Android camera emulators only
43
+ * (the camera source is fixed at launch). Absent = the platform cannot.
44
+ */
45
+ relaunch?(id: string): AsyncResult<void>;
39
46
  /** Called once the device is ready, before the health probe. Best effort. */
40
47
  afterReady?(id: string): Promise<void>;
41
48
  }
@@ -60,4 +67,6 @@ export type ProviderDeps = {
60
67
  timer?: (ms: number) => Timer;
61
68
  /** progress notes (e.g. "cloning from golden…") — stderr in the CLI */
62
69
  log?: (line: string) => void;
70
+ /** where the iOS ownership marker is written (default: the real filesystem) */
71
+ markerFs?: MarkerFs;
63
72
  };
@@ -0,0 +1,43 @@
1
+ import { type Result } from "../../types/src/result";
2
+ import type { CapacityReason } from "./claim-errors";
3
+ import type { QueueEntry } from "./types";
4
+ export declare const PRIORITY: {
5
+ readonly interactive: 30;
6
+ readonly high: 20;
7
+ readonly normal: 10;
8
+ readonly low: 0;
9
+ };
10
+ export type PriorityName = keyof typeof PRIORITY;
11
+ /** `high` for 20, the number itself for a custom priority. */
12
+ export declare const priorityName: (priority: number) => string;
13
+ /** Gap between adjacent named priorities (`low` → `normal` → `high` → `interactive`). */
14
+ export declare const PRIORITY_STEP = 10;
15
+ /**
16
+ * One priority level (`PRIORITY_STEP` points) is earned per this much waiting, so a `low` row overtakes
17
+ * a fresh `high` one after 20 min, inside the default 30 min `--wait`.
18
+ */
19
+ export declare const AGING_MS: number;
20
+ export declare function parsePriority(input: string): Result<number>;
21
+ /** `priority + PRIORITY_STEP × floor(age / 10 min)`; clock skew (now < enqueuedAt) earns nothing. */
22
+ export declare function effectivePriority(entry: QueueEntry, now: number): number;
23
+ /** Head first: effective priority desc, then enqueued_at asc, then id. */
24
+ export declare function queueOrder(entries: readonly QueueEntry[], now: number): QueueEntry[];
25
+ /** 1-based position, or undefined when `id` is not queued. */
26
+ export declare function queuePosition(entries: readonly QueueEntry[], id: string, now: number): number | undefined;
27
+ export declare function isHead(entries: readonly QueueEntry[], id: string, now: number): boolean;
28
+ /** Why a polling claim has no device yet although the host has room: nothing to reserve. */
29
+ export declare const DEVICE_WAIT_REASONS: readonly ["device-limit", "requester-limit", "waiting-for-device"];
30
+ export type DeviceWaitReason = (typeof DEVICE_WAIT_REASONS)[number];
31
+ /**
32
+ * Why a claim is not running yet. Queued for host capacity: behind an earlier claim, or the resource the
33
+ * host is out of. Polling for a device: the pool is at `--max`, the owner is at its limit, or a physical
34
+ * device is not free / ready.
35
+ */
36
+ export type WaitReason = "behind-earlier" | CapacityReason | DeviceWaitReason;
37
+ export declare const isWaitReason: (s: string) => s is WaitReason;
38
+ export declare const isDeviceWaitReason: (reason: WaitReason | undefined) => reason is DeviceWaitReason;
39
+ /**
40
+ * Rows that take part in admission order. A claim polling for a device holds a row so `warden queue` can
41
+ * show why it waits, but it needs no boot slot: it is never ahead of, or counted against, a claim that does.
42
+ */
43
+ export declare const admissionQueue: (entries: readonly QueueEntry[]) => QueueEntry[];
@@ -1,9 +1,21 @@
1
1
  import { Database } from "bun:sqlite";
2
+ import { type EventFilter, type LeaseEvent, type NewLeaseEvent } from "./lease-events";
2
3
  import { type PidAlive } from "./liveness";
4
+ import { type WaitReason } from "./queue";
3
5
  import type { Lease, Owner, Platform, QueueEntry, Resource } from "./types";
4
6
  /** `$WARDEN_HOME`, else `~/.warden`. */
5
7
  export declare function wardenHome(env?: Record<string, string | undefined>): string;
6
8
  export declare function defaultDbPath(env?: Record<string, string | undefined>): string;
9
+ /** An owner's idempotency key and the claim it created (one row per key, however many leases the claim holds). */
10
+ export type ClaimKey = {
11
+ ownerKey: string;
12
+ key: string;
13
+ claimId: string;
14
+ platform: string;
15
+ profile: string;
16
+ count: number;
17
+ ready: boolean;
18
+ };
7
19
  /**
8
20
  * Ordered schema migrations; index + 1 = `PRAGMA user_version` after applying. Append only — never
9
21
  * edit an entry that has shipped.
@@ -12,6 +24,11 @@ export declare const MIGRATIONS: readonly string[];
12
24
  /** A queued claim that has not refreshed within this long is dead (its waiter polls every few seconds). */
13
25
  export declare const QUEUE_TTL_MS = 60000;
14
26
  export declare function ownerKey(owner: Owner): string;
27
+ /** Why leases are being deleted when it is history worth logging (see `Store.deleteLeases`). */
28
+ export type LeaseEnd = {
29
+ type: "released" | "expired";
30
+ now: number;
31
+ };
15
32
  export type NewLease = {
16
33
  resource: Resource;
17
34
  owner: Owner;
@@ -20,6 +37,7 @@ export type NewLease = {
20
37
  pid?: number;
21
38
  bootedByOwner?: boolean;
22
39
  shared?: boolean;
40
+ claimId?: string;
23
41
  };
24
42
  /** A device warden created (sim) or launched (emulator). */
25
43
  export type DeviceRecord = {
@@ -35,6 +53,8 @@ export type DeviceRecord = {
35
53
  quarantineReason?: string;
36
54
  /** set while the simulator's daemons are switched off (`sim slim`); cleared by `--restore` */
37
55
  slimmedAt?: number;
56
+ /** set when `warden dev` took the sim: kept on shutdown (not ephemeral) so installs and logins survive */
57
+ persistentAt?: number;
38
58
  };
39
59
  export declare function newLeaseId(): string;
40
60
  /**
@@ -61,7 +81,29 @@ export declare class Store {
61
81
  listLeases(): Lease[];
62
82
  listLeasesByOwner(owner: Owner): Lease[];
63
83
  findLeaseByResource(resource: Resource): Lease | undefined;
64
- deleteLeases(ids: readonly string[]): number;
84
+ /**
85
+ * Delete leases. With `end`, a device lease that was granted (has a `granted` event) also logs `end.type`
86
+ * in the same transaction. Without it nothing is logged: rollbacks, create placeholders, eviction holds,
87
+ * ports and build locks are not lease history.
88
+ */
89
+ deleteLeases(ids: readonly string[], end?: LeaseEnd): number;
90
+ private recordLeaseEnd;
91
+ /** Append one lease event (join the caller's transaction when there is one). */
92
+ recordEvent(event: NewLeaseEvent): void;
93
+ /** `queued` is logged once per claim call (its queue row is refreshed every round): false when already there. */
94
+ recordQueuedOnce(event: Extract<NewLeaseEvent, {
95
+ type: "queued";
96
+ }>): boolean;
97
+ /**
98
+ * A holder's heartbeat found these leases gone: log `heartbeat_lost` for those no longer held, copying
99
+ * who/what from their `granted` event. Returns how many were logged.
100
+ */
101
+ recordHeartbeatLost(ids: readonly string[], now: number): number;
102
+ /** The `heartbeat_lost` event of a granted lease (who/what copied from its grant); undefined for one never granted (ports, builds). */
103
+ private heartbeatLostEvent;
104
+ listEvents(filter?: EventFilter): LeaseEvent[];
105
+ /** Delete events older than `before` (ms). */
106
+ pruneEvents(before: number): number;
65
107
  /**
66
108
  * Stamp `heartbeat_at = now` on `ids`; returns how many leases were updated (fewer than `ids` = some
67
109
  * were released). `periodMs` (the holder's heartbeat interval) marks them supervised: with a pid,
@@ -72,8 +114,17 @@ export declare class Store {
72
114
  reclaimStale(now: number, pidAlive: PidAlive): string[];
73
115
  /** Insert a queue row; an existing id keeps its `enqueuedAt` (queue age) and updates the rest. */
74
116
  enqueue(entry: QueueEntry): QueueEntry;
75
- /** Stamp `heartbeat_at`; false when the row is gone (reclaimed or dequeued). */
76
- refreshQueue(id: string, now: number): boolean;
117
+ /** Stamp `heartbeat_at` and the live wait `reason`; false when the row is gone (reclaimed or dequeued). */
118
+ refreshQueue(id: string, now: number, reason?: WaitReason): boolean;
119
+ /** The claim an owner's idempotency key points at (caller holds the transaction), if any. */
120
+ getClaimKey(ownerKey: string, key: string): ClaimKey | undefined;
121
+ /** Point (owner, key) at a claim, replacing a stale mapping; `ready` stays false until the claim is granted. */
122
+ setClaimKey(entry: Omit<ClaimKey, "ready">, now: number): void;
123
+ /** The claim's leases are all granted: a repeat of its key now returns them. */
124
+ markClaimKeyReady(claimId: string): void;
125
+ deleteClaimKey(ownerKey: string, key: string): void;
126
+ /** Forget the keys of a claim that failed (its leases are gone). */
127
+ deleteClaimKeysOf(claimId: string): void;
77
128
  dequeue(id: string): boolean;
78
129
  /** Raw insertion order (`enqueued_at`, id); use `queueOrder` for admission order. */
79
130
  listQueue(): QueueEntry[];
@@ -83,11 +134,16 @@ export declare class Store {
83
134
  listDevices(platform?: Platform): DeviceRecord[];
84
135
  touchDevice(platform: Platform, id: string, now: number): void;
85
136
  /** Quarantine a warden device (recording it first when only its name marked it ours). */
86
- quarantineDevice(device: Omit<DeviceRecord, "createdAt" | "lastUsedAt" | "quarantinedAt" | "quarantineReason" | "slimmedAt">, reason: string | undefined, now: number): void;
137
+ quarantineDevice(device: Omit<DeviceRecord, "createdAt" | "lastUsedAt" | "quarantinedAt" | "quarantineReason" | "slimmedAt" | "persistentAt">, reason: string | undefined, now: number): void;
87
138
  /** Clear quarantine on every recorded device with this id (any platform); returns how many were cleared. */
88
- unquarantineDevice(id: string): number;
139
+ unquarantineDevice(id: string, now: number): number;
89
140
  /** Record (`at` set) or clear (`at` undefined) that a recorded device's daemons are slimmed. */
90
141
  setSlimmed(platform: Platform, id: string, at: number | undefined): void;
142
+ /**
143
+ * Mark a recorded device persistent (never deleted as ephemeral). Update-only: a record is what makes a device
144
+ * warden-created (`shouldShutdown`), so a sim that was already running when leased must not gain one here.
145
+ */
146
+ markPersistent(platform: Platform, id: string, now: number): void;
91
147
  forgetDevice(platform: Platform, id: string): void;
92
148
  /**
93
149
  * What a `warden gc` pass could find to do, from the store alone (no simulator is asked):
@@ -1,9 +1,39 @@
1
+ import type { WaitReason } from "./queue";
1
2
  export type Platform = "ios" | "android";
3
+ /** Facts about a real phone/tablet, from its provider. Only physical devices carry one. */
4
+ export type PhysicalInfo = {
5
+ platform: "android";
6
+ transport: "usb" | "wifi";
7
+ adbState: string;
8
+ hwSerial?: string;
9
+ model?: string;
10
+ } | {
11
+ platform: "ios";
12
+ transport: "wired" | "localNetwork";
13
+ deviceType: string;
14
+ osVersion: string;
15
+ developerMode: boolean;
16
+ ddi: boolean;
17
+ tunnelIp?: string;
18
+ /** devicectl's pairing state when it is not `paired` (the reason the device is not ready) */
19
+ pairingState?: string;
20
+ /** devicectl's `tunnelState` (`connected` = reachable now); a `localNetwork` phone is often `disconnected` until something talks to it */
21
+ tunnelState?: string;
22
+ /** why a paired phone cannot be reached right now (its `lockState` probe failed); set means never auto-picked */
23
+ unreachable?: string;
24
+ /** `lockState.passcodeRequired` from the reachability probe (true = locked); only set when that probe ran and answered */
25
+ locked?: boolean;
26
+ };
2
27
  export type DeviceResource = {
3
28
  kind: "device";
4
29
  platform: Platform;
5
30
  id: string;
6
31
  name: string;
32
+ /** set on leases of real devices; id stays the transport id (adb serial / CoreDevice UDID) */
33
+ physical?: {
34
+ transport: string;
35
+ hwSerial?: string;
36
+ };
7
37
  };
8
38
  export type PortResource = {
9
39
  kind: "port";
@@ -55,6 +85,11 @@ export type Lease = {
55
85
  * attaches to it instead of refusing with `device_busy`, and never releases it. Other owners still see it as foreign.
56
86
  */
57
87
  shared?: boolean;
88
+ /**
89
+ * The `claimDevices` call(s) that took this lease: a wrapper's legs share one id, so the per-owner limit counts
90
+ * claims, not devices. Absent on leases an older binary wrote (each counts as its own claim).
91
+ */
92
+ claimId?: string;
58
93
  };
59
94
  export type DeviceState = "booted" | "shutdown" | "booting";
60
95
  /** A device as seen by a provider, enriched with warden's own bookkeeping. */
@@ -73,6 +108,8 @@ export type InventoryDevice = {
73
108
  golden?: boolean;
74
109
  /** reported bad: never allocated until `warden sim unquarantine` */
75
110
  quarantined?: boolean;
111
+ /** a real phone/tablet: never booted, shut down, created, adopted, counted in capacity or recorded as a warden device */
112
+ physical?: PhysicalInfo;
76
113
  };
77
114
  export type DeviceRequest = {
78
115
  platform: Platform;
@@ -83,6 +120,12 @@ export type DeviceRequest = {
83
120
  max: number;
84
121
  /** allow allocating foreign (non-warden) devices that match the profile */
85
122
  adopt?: boolean;
123
+ /** opt-in: only physical devices (optionally just these ids), never create or boot */
124
+ physical?: {
125
+ ids?: string[];
126
+ /** allowlist: with no `ids`, only these devices are picked (`physical.allow` / `WARDEN_PHYSICAL_IDS`); `ids` beat it */
127
+ allow?: string[];
128
+ };
86
129
  };
87
130
  export type AllocationStep = {
88
131
  action: "reuse";
@@ -123,4 +166,18 @@ export type QueueEntry = {
123
166
  owner: Owner;
124
167
  label?: string;
125
168
  pid?: number;
169
+ /** why the claim waits, rewritten every round (absent on rows an older binary wrote) */
170
+ reason?: WaitReason;
171
+ /** the claim this row belongs to (absent on rows an older binary wrote) */
172
+ claimId?: string;
126
173
  };
174
+ /** The one constructor of a device lease resource: carries `physical` for real devices. */
175
+ export declare function deviceResourceOf(d: InventoryDevice): DeviceResource;
176
+ /** An iPhone/iPad hardware UDID: `XXXXXXXX-XXXXXXXXXXXXXXXX` or 40 hex (older devices); a simulator UUID is neither. */
177
+ export declare function isPhysicalIosId(id: string): boolean;
178
+ /** A device warden may not boot, shut down, create, erase or count in host capacity. */
179
+ export declare function isPhysicalDevice(d: Pick<InventoryDevice, "physical">): boolean;
180
+ /** Is a physical device usable now: an Android phone in adb state `device`, an iOS device its provider reports `booted` (paired and available). */
181
+ export declare function isPhysicalReady(d: InventoryDevice): boolean;
182
+ /** Why a physical device cannot be used now (undefined = ready, or not a phone), as `<label>: <hint>`; `devices` and a failed claim print it. */
183
+ export declare function physicalNotReadyReason(d: InventoryDevice): string | undefined;