@delacour/warden-e2e 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.
Files changed (86) hide show
  1. package/{chunk-yb1hcjqm.mjs → chunk-empqr9n0.mjs} +1364 -53
  2. package/index.mjs +34 -4
  3. package/package.json +2 -2
  4. package/preload.mjs +4008 -335
  5. package/types/core/src/affected/affected.schema.d.ts +93 -0
  6. package/types/core/src/allocate.d.ts +2 -1
  7. package/types/core/src/android-keyguard.d.ts +34 -0
  8. package/types/core/src/batch/preset.schema.d.ts +1 -0
  9. package/types/core/src/biometrics.d.ts +15 -0
  10. package/types/core/src/builds/builds.defaults.d.ts +15 -0
  11. package/types/core/src/builds/config.d.ts +5 -1
  12. package/types/core/src/builds/config.schema.d.ts +182 -0
  13. package/types/core/src/builds/define-config.d.ts +104 -0
  14. package/types/core/src/builds/install-device.d.ts +37 -0
  15. package/types/core/src/builds/install.d.ts +17 -3
  16. package/types/core/src/builds/kinds.d.ts +21 -3
  17. package/types/core/src/builds/lock.d.ts +38 -0
  18. package/types/core/src/builds/resolve.d.ts +12 -2
  19. package/types/core/src/conditions/conditions.config.d.ts +117 -0
  20. package/types/core/src/conditions/conditions.d.ts +22 -0
  21. package/types/core/src/conditions/conditions.types.d.ts +141 -0
  22. package/types/core/src/conditions/inject-state.d.ts +13 -0
  23. package/types/core/src/conditions/launch-settings.d.ts +14 -0
  24. package/types/core/src/conditions/network.presets.d.ts +42 -0
  25. package/types/core/src/conditions/text-size.d.ts +19 -0
  26. package/types/core/src/golden/android-biometrics.d.ts +35 -0
  27. package/types/core/src/ios-runner/classify.d.ts +7 -0
  28. package/types/core/src/ios-runner/runner-build.types.d.ts +76 -0
  29. package/types/core/src/netproxy/control.d.ts +75 -0
  30. package/types/core/src/netproxy/decide.d.ts +22 -0
  31. package/types/core/src/netproxy/handle-file.d.ts +12 -0
  32. package/types/core/src/netproxy/lease-proxy.d.ts +53 -0
  33. package/types/core/src/netproxy/netproxy.main.d.ts +40 -0
  34. package/types/core/src/netproxy/pipe.d.ts +37 -0
  35. package/types/core/src/netproxy/proxy.d.ts +48 -0
  36. package/types/core/src/netproxy/shaping.d.ts +43 -0
  37. package/types/core/src/providers/android.d.ts +36 -1
  38. package/types/core/src/types.d.ts +47 -0
  39. package/types/e2e/src/alert-buttons.d.ts +9 -0
  40. package/types/e2e/src/app.d.ts +3 -1
  41. package/types/e2e/src/conditions/condition-events.d.ts +63 -0
  42. package/types/e2e/src/conditions/device-conditions.d.ts +170 -0
  43. package/types/e2e/src/conditions/geo.d.ts +15 -0
  44. package/types/e2e/src/conditions/gpx.d.ts +8 -0
  45. package/types/e2e/src/conditions/lease-recorder.d.ts +27 -0
  46. package/types/e2e/src/conditions/lease-store.d.ts +9 -0
  47. package/types/e2e/src/conditions/location-plan.d.ts +17 -0
  48. package/types/e2e/src/conditions/location-scenarios.data.d.ts +17 -0
  49. package/types/e2e/src/conditions/route-ticker.d.ts +53 -0
  50. package/types/e2e/src/conditions/scheduler.d.ts +83 -0
  51. package/types/e2e/src/device.d.ts +57 -3
  52. package/types/e2e/src/driver/android/android-app.d.ts +4 -0
  53. package/types/e2e/src/driver/android/android-argv.d.ts +19 -0
  54. package/types/e2e/src/driver/android/android-clock.d.ts +28 -0
  55. package/types/e2e/src/driver/android/android-device.d.ts +8 -0
  56. package/types/e2e/src/driver/android/android-driver.d.ts +17 -0
  57. package/types/e2e/src/driver/android/android-network.d.ts +66 -0
  58. package/types/e2e/src/driver/android/android-phone.d.ts +47 -0
  59. package/types/e2e/src/driver/android/android-settings.d.ts +34 -0
  60. package/types/e2e/src/driver/android/android-state.d.ts +10 -0
  61. package/types/e2e/src/driver/clock-recorder.d.ts +9 -0
  62. package/types/e2e/src/driver/device-settings.d.ts +10 -0
  63. package/types/e2e/src/driver/driver.types.d.ts +176 -1
  64. package/types/e2e/src/driver/ios/device/capabilities.d.ts +3 -0
  65. package/types/e2e/src/driver/ios/device/device-driver.d.ts +49 -0
  66. package/types/e2e/src/driver/ios/device/devicectl.d.ts +38 -0
  67. package/types/e2e/src/driver/ios/device/runner-protocol.d.ts +21 -0
  68. package/types/e2e/src/driver/ios/hid-recovery.d.ts +15 -0
  69. package/types/e2e/src/driver/ios/inject.d.ts +110 -0
  70. package/types/e2e/src/driver/ios/ios-driver.d.ts +29 -0
  71. package/types/e2e/src/driver/ios/ios-network.d.ts +46 -0
  72. package/types/e2e/src/driver/ios/ios-settings.d.ts +44 -0
  73. package/types/e2e/src/driver/ios/simctl.d.ts +41 -1
  74. package/types/e2e/src/driver/ios/type-budget.d.ts +6 -1
  75. package/types/e2e/src/driver/network-proxy.d.ts +52 -0
  76. package/types/e2e/src/driver/open.d.ts +20 -0
  77. package/types/e2e/src/driver/physical-conditions.d.ts +11 -0
  78. package/types/e2e/src/driver/requirements.d.ts +14 -0
  79. package/types/e2e/src/driver/typing.d.ts +1 -1
  80. package/types/e2e/src/env.d.ts +32 -0
  81. package/types/e2e/src/fixture-session.d.ts +4 -0
  82. package/types/e2e/src/fixtures.d.ts +6 -1
  83. package/types/e2e/src/index.d.ts +2 -0
  84. package/types/e2e/src/runner/scope.d.ts +1 -0
  85. package/types/e2e/src/runner/test.d.ts +6 -0
  86. package/types/e2e/src/traced-device.d.ts +2 -1
@@ -0,0 +1,76 @@
1
+ import type { AsyncResult } from "../../../types/src/result";
2
+ import type { BuildLockDeps } from "../builds/lock";
3
+ import type { Exec } from "../exec";
4
+ /** Codes `classify.ts` and `signing.ts` give a failure that has a known fix; every one comes with a `hint`. */
5
+ export declare const CLASSIFIED_RUNNER_ERRORS: readonly ["no_account", "no_team", "ios_team_ambiguous", "bundle_id_taken", "device_locked", "developer_mode_off", "runner_untrusted", "ui_automation_disabled", "device_busy", "ddi_unavailable", "unsupported_os"];
6
+ export type ClassifiedRunnerErrorCode = (typeof CLASSIFIED_RUNNER_ERRORS)[number];
7
+ /** A team the keychain holds an `Apple Development` identity for. */
8
+ export type SigningTeam = {
9
+ teamId: string;
10
+ identities: string[];
11
+ };
12
+ type RunnerErrorBase = {
13
+ message: string;
14
+ hint: string;
15
+ };
16
+ /**
17
+ * Why the on-device runner cannot be built / signed / started. `hint` is the one thing to do about it.
18
+ * `ios_team_ambiguous` carries the teams to pick from; `build_failed` is anything unclassified (with the
19
+ * tail of the output).
20
+ */
21
+ export type RunnerBuildError = (RunnerErrorBase & {
22
+ code: Exclude<ClassifiedRunnerErrorCode, "ios_team_ambiguous">;
23
+ }) | (RunnerErrorBase & {
24
+ code: "ios_team_ambiguous";
25
+ teams: SigningTeam[];
26
+ }) | (RunnerErrorBase & {
27
+ code: "build_failed";
28
+ output: string;
29
+ });
30
+ export type RunnerBuildErrorCode = RunnerBuildError["code"];
31
+ /** Where the signing team came from. */
32
+ export type TeamSource = "input" | "env" | "config" | "keychain";
33
+ /** The runner sources as a map of path (relative to the staged root, `/`-separated) to text. */
34
+ export type RunnerSourceFiles = Readonly<Record<string, string>>;
35
+ export type EnsureRunnerBuildInput = {
36
+ /** the phone the runner must be signed for (`-destination id=`; registers it in the provisioning profile) */
37
+ udid: string;
38
+ /** signing team; default: `WARDEN_IOS_TEAM_ID`, then `ios.teamId`, then the keychain */
39
+ team?: string | undefined;
40
+ /** `ios.teamId` of `warden.config.ts` */
41
+ configTeamId?: string | undefined;
42
+ /** `ios.runnerBundleId` of `warden.config.ts` */
43
+ configBundleId?: string | undefined;
44
+ env: Record<string, string | undefined>;
45
+ /** `$WARDEN_HOME` (default: from `env`) */
46
+ home?: string | undefined;
47
+ exec: Exec;
48
+ /** warden's own version (part of the staged-source directory name) */
49
+ version: string;
50
+ /** the embedded runner sources (blob in a compiled binary, the checkout when run from source) */
51
+ loadSources: () => AsyncResult<RunnerSourceFiles>;
52
+ /** serialises builds of one (team, Xcode, sources): the build lease */
53
+ lock: BuildLockDeps;
54
+ now?: () => number;
55
+ /** progress lines (xcodebuild's own output is not forwarded) */
56
+ log?: (line: string) => void;
57
+ /** `xcodebuild` is stopped after this long (default 15 min) */
58
+ timeoutMs?: number;
59
+ /** after xcodebuild asks to unlock the phone, wait this long before giving up with `device_locked` (default 45 s) */
60
+ lockGraceMs?: number;
61
+ };
62
+ /** A runner build ready for `xcodebuild test-without-building -xctestrun <xctestrun>`. */
63
+ export type RunnerBuild = {
64
+ /** the `.xctestrun` inside `derivedData` */
65
+ xctestrun: string;
66
+ derivedData: string;
67
+ /** the host app's bundle id (the UI-test bundle is `<id>.uitests`, its runner app `<id>.uitests.xctrunner`) */
68
+ bundleId: string;
69
+ team: string;
70
+ teamSource: TeamSource;
71
+ /** false: built just now */
72
+ cached: boolean;
73
+ /** why it was (re)built when `cached` is false */
74
+ rebuiltBecause?: "none" | "new_udid" | "profile_expiring" | "bundle_id" | "missing";
75
+ };
76
+ export {};
@@ -0,0 +1,75 @@
1
+ import { type AsyncResult } from "../../../types/src/result";
2
+ import { z } from "zod";
3
+ import type { HostFault } from "../conditions/conditions.types";
4
+ import type { ProxyConditions, ShapingParams } from "./shaping";
5
+ /** Where a running proxy listens and how to steer it. Persisted as JSON so another process can find it. */
6
+ export type ProxyHandle = {
7
+ pid: number;
8
+ /** loopback address the proxy and control channel bind */
9
+ host: string;
10
+ /** the forward proxy: what a device's HTTP proxy setting points at */
11
+ port: number;
12
+ /** loopback HTTP control channel */
13
+ controlPort: number;
14
+ /** required as `x-warden-token` on every control call */
15
+ token: string;
16
+ };
17
+ export type ProxyStats = {
18
+ connections: number;
19
+ resets: number;
20
+ stalls: number;
21
+ faulted: number;
22
+ bytesUp: number;
23
+ bytesDown: number;
24
+ };
25
+ export type ProxyStatus = ProxyConditions & {
26
+ pid: number;
27
+ stats: ProxyStats;
28
+ };
29
+ /** Replace `params` and/or `faults`; an omitted field keeps its current value. */
30
+ export type ConditionsUpdate = {
31
+ params?: ShapingParams;
32
+ faults?: readonly HostFault[];
33
+ };
34
+ export declare const conditionsUpdateSchema: z.ZodObject<{
35
+ params: z.ZodOptional<z.ZodObject<{
36
+ latencyMs: z.ZodOptional<z.ZodNumber>;
37
+ jitterMs: z.ZodOptional<z.ZodNumber>;
38
+ downKbps: z.ZodOptional<z.ZodNumber>;
39
+ upKbps: z.ZodOptional<z.ZodNumber>;
40
+ lossPct: z.ZodOptional<z.ZodNumber>;
41
+ offline: z.ZodOptional<z.ZodEnum<{
42
+ blackhole: "blackhole";
43
+ refuse: "refuse";
44
+ }>>;
45
+ }, z.core.$strict>>;
46
+ faults: z.ZodOptional<z.ZodArray<z.ZodObject<{
47
+ match: z.ZodString;
48
+ action: z.ZodDiscriminatedUnion<[z.ZodObject<{
49
+ kind: z.ZodLiteral<"status">;
50
+ code: z.ZodNumber;
51
+ }, z.core.$strict>, z.ZodObject<{
52
+ kind: z.ZodLiteral<"reset">;
53
+ }, z.core.$strict>, z.ZodObject<{
54
+ kind: z.ZodLiteral<"delay">;
55
+ ms: z.ZodNumber;
56
+ }, z.core.$strict>, z.ZodObject<{
57
+ kind: z.ZodLiteral<"blackhole">;
58
+ }, z.core.$strict>], "kind">;
59
+ }, z.core.$strict>>>;
60
+ }, z.core.$strict>;
61
+ /** What the control server needs from the proxy it steers. */
62
+ export type ControlApi = {
63
+ status(): ProxyStatus;
64
+ update(update: ConditionsUpdate): void;
65
+ shutdown(): void;
66
+ };
67
+ /** Loopback HTTP control channel: `GET /status`, `PUT /conditions`, `POST /shutdown`; token-guarded. */
68
+ export declare function serveControl(api: ControlApi, token: string, host: string): Bun.Server<undefined>;
69
+ /** Live-reconfigure a running proxy without dropping its listener or open connections. */
70
+ export declare function setProxyConditions(handle: ProxyHandle, update: ConditionsUpdate): AsyncResult<ProxyStatus>;
71
+ export declare function getProxyStatus(handle: ProxyHandle): AsyncResult<ProxyStatus>;
72
+ /** Ask the proxy to close its listeners and exit. */
73
+ export declare function stopProxy(handle: ProxyHandle): AsyncResult<{
74
+ ok: true;
75
+ }>;
@@ -0,0 +1,22 @@
1
+ import type { ProxyEvent } from "./proxy";
2
+ import { type ProxyConditions, type Rng } from "./shaping";
3
+ /** What to do with one request once its target host is known. */
4
+ export type Verdict = {
5
+ act: {
6
+ kind: "reset";
7
+ } | {
8
+ kind: "hold";
9
+ } | {
10
+ kind: "status";
11
+ code: number;
12
+ } | {
13
+ kind: "go";
14
+ };
15
+ /** delay before the act: params latency (+ a `delay` fault) */
16
+ waitMs: number;
17
+ event?: ProxyEvent;
18
+ /** which counter to bump */
19
+ count?: "faulted" | "resets";
20
+ };
21
+ /** Faults first (first glob match wins), then connection loss, then plain latency. */
22
+ export declare function decide(conditions: ProxyConditions, host: string, isConnect: boolean, rng: Rng): Verdict;
@@ -0,0 +1,12 @@
1
+ import type { ProxyHandle } from "./control";
2
+ /** `$WARDEN_HOME/netproxy/<leaseId>.json`: how another warden process finds a lease's proxy. */
3
+ export declare function handlePath(leaseId: string, env?: Record<string, string | undefined>): string;
4
+ /** What `netproxy.main.ts` writes once it is listening. `portLeaseId` is added by `ensureLeaseProxy`. */
5
+ export type HandleFile = ProxyHandle & {
6
+ leaseId: string;
7
+ portLeaseId?: string;
8
+ };
9
+ /** Atomic (tmp + rename) and owner-only: the file carries the control token. */
10
+ export declare function writeHandleFile(path: string, data: HandleFile): void;
11
+ export declare function readHandleFile(path: string): HandleFile | undefined;
12
+ export declare function removeHandleFile(path: string): void;
@@ -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;
@@ -16,7 +16,24 @@ export type AdbEmulator = {
16
16
  port: number;
17
17
  state: string;
18
18
  };
19
- /** Parse `adb devices -l`, keeping only `emulator-<port>` serials (physical devices ignored). */
19
+ /** One `adb devices -l` row: a console emulator, or anything else (a phone, or a network emulator until `classifyOther`). */
20
+ export type AdbListEntry = ({
21
+ kind: "emulator";
22
+ } & AdbEmulator) | {
23
+ kind: "other";
24
+ serial: string;
25
+ state: string;
26
+ transport: "usb" | "wifi";
27
+ model?: string;
28
+ product?: string;
29
+ };
30
+ /**
31
+ * Parse `adb devices -l`. A state may be several words (`no permissions (…); see [url]`), so the state is
32
+ * the first token unless the row says `no permissions`. Transport: a `usb:` attribute is usb; `ip:port` and
33
+ * `_adb-tls-connect._tcp` serials (and anything else without `usb:`) are wifi.
34
+ */
35
+ export declare function parseAdbDeviceList(stdout: string): AdbListEntry[];
36
+ /** Parse `adb devices -l`, keeping only `emulator-<port>` serials (phones and network emulators ignored). */
20
37
  export declare function parseAdbDevices(stdout: string): AdbEmulator[];
21
38
  /** Parse `emulator -list-avds`, dropping the emulator's own `INFO | …` / `WARNING | …` log lines. */
22
39
  export declare function parseAvdList(stdout: string): string[];
@@ -38,8 +55,24 @@ export declare const LIST_AVDS_TIMEOUT_MS = 15000;
38
55
  export declare function listAvds(deps: ExecDeps, opts?: {
39
56
  fresh?: boolean;
40
57
  }): AsyncResult<string[]>;
58
+ /** Every row of `adb devices -l`: emulators, phones and network emulators. */
59
+ export declare function listAdbDevices(deps: ExecDeps): AsyncResult<AdbListEntry[]>;
41
60
  /** Running emulators from `adb devices -l`. */
42
61
  export declare function listEmulators(deps: ExecDeps): AsyncResult<AdbEmulator[]>;
62
+ export type OtherClass = {
63
+ emulator: boolean;
64
+ hwSerial?: string;
65
+ model?: string;
66
+ sdk?: string;
67
+ };
68
+ /**
69
+ * Is a non-`emulator-N` adb serial a real phone? `localhost:5555` / Genymotion / a network emulator set
70
+ * `ro.boot.qemu` or `ro.kernel.qemu` to 1: a foreign emulator, never physical. One bounded read; a failure
71
+ * is an error, not a guess.
72
+ */
73
+ export declare function classifyOther(deps: ExecDeps, serial: string): AsyncResult<OtherClass>;
74
+ /** Is `id` a console emulator serial; anything else is a phone (or a network emulator), which warden never boots, kills or writes settings to. */
75
+ export declare function isEmulatorSerial(id: string): boolean;
43
76
  /** Launch `cmd` so it outlives warden, stdout/stderr appended to `logPath`. */
44
77
  export type SpawnDetached = (cmd: readonly string[], logPath: string) => AsyncResult<{
45
78
  pid: number;
@@ -66,6 +99,8 @@ export type AndroidProviderOptions = {
66
99
  };
67
100
  /** One `adb shell` that returns once the guest has booted; POSIX `sh` (toybox `[`). */
68
101
  export declare const BOOT_WAIT_SCRIPT = "while [ \"$(getprop sys.boot_completed)\" != 1 ]; do sleep 0.2; done";
102
+ /** One shell: qemu props (emulator tell-tales), hardware serial, model, SDK level — one value per line. */
103
+ 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
104
  /** The last 3 lines of an emulator log, one line. */
70
105
  export declare function logTail(logPath: string): string;
71
106
  /**
@@ -1,9 +1,38 @@
1
1
  export type Platform = "ios" | "android";
2
+ /** Facts about a real phone/tablet, from its provider. Only physical devices carry one. */
3
+ export type PhysicalInfo = {
4
+ platform: "android";
5
+ transport: "usb" | "wifi";
6
+ adbState: string;
7
+ hwSerial?: string;
8
+ model?: string;
9
+ } | {
10
+ platform: "ios";
11
+ transport: "wired" | "localNetwork";
12
+ deviceType: string;
13
+ osVersion: string;
14
+ developerMode: boolean;
15
+ ddi: boolean;
16
+ tunnelIp?: string;
17
+ /** devicectl's pairing state when it is not `paired` (the reason the device is not ready) */
18
+ pairingState?: string;
19
+ /** devicectl's `tunnelState` (`connected` = reachable now); a `localNetwork` phone is often `disconnected` until something talks to it */
20
+ tunnelState?: string;
21
+ /** why a paired phone cannot be reached right now (its `lockState` probe failed); set means never auto-picked */
22
+ unreachable?: string;
23
+ /** `lockState.passcodeRequired` from the reachability probe (true = locked); only set when that probe ran and answered */
24
+ locked?: boolean;
25
+ };
2
26
  export type DeviceResource = {
3
27
  kind: "device";
4
28
  platform: Platform;
5
29
  id: string;
6
30
  name: string;
31
+ /** set on leases of real devices; id stays the transport id (adb serial / CoreDevice UDID) */
32
+ physical?: {
33
+ transport: string;
34
+ hwSerial?: string;
35
+ };
7
36
  };
8
37
  export type PortResource = {
9
38
  kind: "port";
@@ -73,6 +102,8 @@ export type InventoryDevice = {
73
102
  golden?: boolean;
74
103
  /** reported bad: never allocated until `warden sim unquarantine` */
75
104
  quarantined?: boolean;
105
+ /** a real phone/tablet: never booted, shut down, created, adopted, counted in capacity or recorded as a warden device */
106
+ physical?: PhysicalInfo;
76
107
  };
77
108
  export type DeviceRequest = {
78
109
  platform: Platform;
@@ -83,6 +114,12 @@ export type DeviceRequest = {
83
114
  max: number;
84
115
  /** allow allocating foreign (non-warden) devices that match the profile */
85
116
  adopt?: boolean;
117
+ /** opt-in: only physical devices (optionally just these ids), never create or boot */
118
+ physical?: {
119
+ ids?: string[];
120
+ /** allowlist: with no `ids`, only these devices are picked (`physical.allow` / `WARDEN_PHYSICAL_IDS`); `ids` beat it */
121
+ allow?: string[];
122
+ };
86
123
  };
87
124
  export type AllocationStep = {
88
125
  action: "reuse";
@@ -124,3 +161,13 @@ export type QueueEntry = {
124
161
  label?: string;
125
162
  pid?: number;
126
163
  };
164
+ /** The one constructor of a device lease resource: carries `physical` for real devices. */
165
+ export declare function deviceResourceOf(d: InventoryDevice): DeviceResource;
166
+ /** An iPhone/iPad hardware UDID: `XXXXXXXX-XXXXXXXXXXXXXXXX` or 40 hex (older devices); a simulator UUID is neither. */
167
+ export declare function isPhysicalIosId(id: string): boolean;
168
+ /** A device warden may not boot, shut down, create, erase or count in host capacity. */
169
+ export declare function isPhysicalDevice(d: Pick<InventoryDevice, "physical">): boolean;
170
+ /** Is a physical device usable now: an Android phone in adb state `device`, an iOS device its provider reports `booted` (paired and available). */
171
+ export declare function isPhysicalReady(d: InventoryDevice): boolean;
172
+ /** Why a physical device cannot be used now (undefined = ready, or not a phone), as `<label>: <hint>`; `devices` and a failed claim print it. */
173
+ export declare function physicalNotReadyReason(d: InventoryDevice): string | undefined;
@@ -39,3 +39,12 @@ export declare const ANDROID_ALLOW_IDS: readonly string[];
39
39
  * `AlertDialog`'s negative `button2` (after the names).
40
40
  */
41
41
  export declare const ANDROID_DENY_IDS: readonly string[];
42
+ /**
43
+ * The ids of the Android autofill "Save password?" sheet's buttons (`autofill_save_no` "Never", `autofill_save_yes`
44
+ * "Continue"). Either one marks the sheet, in any locale. Its two answers write to the user's account ("Never" a
45
+ * permanent opt-out, "Continue" a stored password), so `device.alert` never presses them: both `accept` and `dismiss`
46
+ * close the sheet with the Back key.
47
+ */
48
+ export declare const ANDROID_AUTOFILL_SAVE_IDS: readonly string[];
49
+ /** Whether `buttons` belong to the Android autofill save sheet. */
50
+ export declare function isAutofillSaveSheet(buttons: readonly Element[]): boolean;
@@ -58,5 +58,7 @@ export type AppDeps = {
58
58
  log?: (line: string) => void;
59
59
  /** time for the `wait: "quiet"` content poll; default the system clock */
60
60
  clock?: Clock;
61
+ /** whether an iOS launch loads the warden network dylib (the suite declares a network or clock condition, or one is active) */
62
+ injectWanted?: () => boolean;
61
63
  };
62
- export declare function createApp({ session, driver, env, log, clock }: AppDeps): App;
64
+ export declare function createApp({ session, driver, env, log, clock, injectWanted }: AppDeps): App;
@@ -0,0 +1,63 @@
1
+ import type { A11yCondition, BatteryCondition, ClockCondition, HostFault, LocaleCondition, LocationCondition, NetworkCondition, PermissionCondition } from "../../../core/src/conditions/conditions.types";
2
+ /**
3
+ * One applied condition transition, the evidence line that lets a failure show which network/location was
4
+ * active. Written to `results.jsonl` beside the test results (`readResults` skips lines that are not a
5
+ * `TestResult`, so the two kinds share the file).
6
+ */
7
+ export type ConditionEvent = {
8
+ type: "condition";
9
+ area: "network";
10
+ value: NetworkCondition;
11
+ at: number;
12
+ } | {
13
+ type: "condition";
14
+ area: "location";
15
+ value: LocationCondition;
16
+ at: number;
17
+ } | {
18
+ type: "condition";
19
+ area: "clock";
20
+ value: ClockCondition;
21
+ at: number;
22
+ } | {
23
+ type: "condition";
24
+ area: "permission";
25
+ value: PermissionCondition;
26
+ at: number;
27
+ } | {
28
+ type: "condition";
29
+ area: "locale";
30
+ value: LocaleCondition;
31
+ at: number;
32
+ } | {
33
+ type: "condition";
34
+ area: "a11y";
35
+ value: A11yCondition;
36
+ at: number;
37
+ } | {
38
+ type: "condition";
39
+ area: "battery";
40
+ value: BatteryCondition;
41
+ at: number;
42
+ } | {
43
+ type: "condition";
44
+ area: "faults";
45
+ value: {
46
+ kind: "faults";
47
+ rules: readonly HostFault[];
48
+ };
49
+ at: number;
50
+ };
51
+ export type ConditionSink = (event: ConditionEvent) => void;
52
+ /** A sink that appends each event as one JSON line (`appendFileSync`: a crash never loses an earlier line). */
53
+ export declare function openConditionSink(path: string): ConditionSink;
54
+ /** Condition events in file order; every other line (test results, blank, truncated) is skipped. */
55
+ export declare function readConditionEvents(text: string): ConditionEvent[];
56
+ /** What was active per area at epoch ms `at`: the latest event at or before it. */
57
+ export declare function activeConditionsAt(events: readonly ConditionEvent[], at: number): {
58
+ network?: NetworkCondition;
59
+ location?: LocationCondition;
60
+ clock?: ClockCondition;
61
+ };
62
+ /** The `kind` of an event's value (`offline`, `point`, ...); the area name for a setting that has none (`permission`, `a11y`). */
63
+ export declare function eventKind(event: ConditionEvent): string;